Win32多线程编程(1) — 基础概念篇
內(nèi)核對象的基本概念
Windows系統(tǒng)是非開源的,它提供給我們的接口是用戶模式的,即User-Mode API。當我們調(diào)用某個API時,需要從用戶模式切換到內(nèi)核模式的I/O System Services API。例如我們調(diào)用Kernel32.dll中的CreateFile創(chuàng)建文件,最終將執(zhí)行ntdll.dll中的系統(tǒng)服務(wù)NtCreateFile。
內(nèi)核為我們創(chuàng)建的文件對象以內(nèi)核級數(shù)據(jù)結(jié)構(gòu)FILE_OBJECT存儲管理,內(nèi)核級文件信息數(shù)據(jù)結(jié)構(gòu)包括FILE_BASIC_INFORMATION、FILE_STANDARD_INFORMATION等,但是系統(tǒng)提供給我們在用戶模式下與這個文件對象交互的接口是一個文件句柄(HANDLE)。
內(nèi)核對象和普通的數(shù)據(jù)結(jié)構(gòu)間的最大區(qū)別在于其內(nèi)部數(shù)據(jù)結(jié)構(gòu)是隱藏的,我們無法(系統(tǒng)沒有操作接口)直接讀或改變對象內(nèi)部的數(shù)據(jù)結(jié)構(gòu)。這里的文件句柄實際是文件內(nèi)核對象在內(nèi)核分配的內(nèi)存中的一個索引,我們執(zhí)行后期的用戶模式下的文件操作調(diào)用都需傳入文件句柄參數(shù),內(nèi)核根據(jù)該句柄來查找(定位)我們要操作的對象。類似的Windows提供的創(chuàng)建線程的API—CreateThread創(chuàng)建的內(nèi)核對象也是以句柄(HANDLE)的形式返回給用戶,后期對該線程內(nèi)核對象的操作都需提供該句柄參數(shù)。
因為內(nèi)核對象的所有者是內(nèi)核,而不是進程,所以何時撤銷內(nèi)核對象由內(nèi)核決定,而內(nèi)核做這個決定的依據(jù)就是該內(nèi)核對象是否仍然被使用。那么如何判斷內(nèi)核對象是否被使用呢?何時釋放回收該內(nèi)核對象資源呢?這就引出了內(nèi)核對象的使用計數(shù)(Usage Count)問題。
內(nèi)核對象是進程內(nèi)的資源,使用計數(shù)屬性指明進程對特定內(nèi)核對象的引用次數(shù),當系統(tǒng)發(fā)現(xiàn)引用次數(shù)是?0?時,它就會自動關(guān)閉資源。事實上這種機制是很簡單的,一個進程在第一次創(chuàng)建內(nèi)核對象的時候,系統(tǒng)為進程分配內(nèi)核對象資源,并將該內(nèi)核對象的使用計數(shù)屬性初始化為1;以后每次打開這個內(nèi)核對象,系統(tǒng)就會將使用計數(shù)加?1,如果關(guān)閉它,系統(tǒng)將使用計數(shù)減1,減到?0?就說明進程對這個內(nèi)核對象的所有引用都已關(guān)閉,系統(tǒng)應(yīng)該釋放此內(nèi)核對象資源。
我們編寫程序都是在用戶模式下進行的,要做內(nèi)核級調(diào)試或者觀察內(nèi)部數(shù)據(jù)結(jié)構(gòu),需要下載系統(tǒng)符號(Symbols)。因為我們無法真正去操作其內(nèi)部數(shù)據(jù)結(jié)構(gòu),又無內(nèi)核源碼,所以使用Windbg等工具一窺Windows系統(tǒng)內(nèi)幕。結(jié)合具體內(nèi)核對象的屬性,通過觀察其內(nèi)部數(shù)據(jù)結(jié)構(gòu),感知其內(nèi)核機制及操作流程。
?
進程和線程的基本概念
進程(Process)是具有一定獨立功能的程序關(guān)于某個數(shù)據(jù)集合上的一次運行過程,是系統(tǒng)進行資源分配和調(diào)度的獨立單位。進程是由進程控制塊(PCB)、程序段、數(shù)據(jù)段三部分組成。其中進程控制塊是存放進程管理和控制信息的數(shù)據(jù)結(jié)構(gòu),是進程存在的唯一標志。
?“進程是一個正在運行的程序,它擁有自己的虛擬地址空間,擁有自己的代碼、數(shù)據(jù)和其他系統(tǒng)資源,如進程創(chuàng)建的文件、管道、同步對象等。一個進程也包含了一個或者多個運行在此進程內(nèi)的線程。”
進程是執(zhí)行程序的實例。例如,當你運行記事本程序notepad.exe時,你就創(chuàng)建了一個用來容納組成notepad.exe的代碼及其所需調(diào)用動態(tài)鏈接庫的進程。每個進程均運行在其專用且受保護的地址空間內(nèi)。因此,如果你同時運行記事本的兩個拷貝,該程序正在使用的數(shù)據(jù)在各自實例中是彼此獨立的。在記事本的一個拷貝中將無法看到該程序的第二個實例打開的數(shù)據(jù)。
在以上語境中,存儲在磁盤上的notepad.exe程序是一連串靜態(tài)的指令,而進程則是一個容器,它包含了一系列運行在這個程序?qū)嵗舷挛闹械木€程使用的資源。進程是不活潑的。一個進程要完成任何事情,必須有一個運行在它的地址空間上的線程。此線程負責執(zhí)行該進程地址空間的代碼。每個進程至少擁有一個在它的地址空間中運行的線程。對一個不包含任何線程的進程來說,它是沒有理由繼續(xù)存在下去的,系統(tǒng)會自動地銷毀此進程和它的地址空間。
線程是進程內(nèi)執(zhí)行代碼的獨立實體。線程(Thread)是進程內(nèi)的一個獨立執(zhí)行單元,是CPU調(diào)度和分派的基本單位。一個線程就是運行在一個進程上下文中的一個邏輯流,它描述了進程內(nèi)代碼的執(zhí)行路徑。每個線程都有自己的線程上下文(Context),包括唯一的整數(shù)線程id,棧(Stack),棧指針(Stack Pointer),程序計數(shù)器(Program Counter),通用目的寄存器。所有運行在一個進程中的線程共享該進程的整個虛擬地址空間。
線程內(nèi)核對象(Thread Kernel Object)和線程上下文(Thread Context)等概念,參考《線程的數(shù)據(jù)結(jié)構(gòu)》。在WinDbg中,可通過lkd> dt nt!_kthread查看線程內(nèi)核對象數(shù)據(jù)結(jié)構(gòu);通過lkd> dt nt!_teb查看線程TEB數(shù)據(jù)結(jié)構(gòu);通過lkd> dt nt!_context查看線程上下文數(shù)據(jù)結(jié)構(gòu)。
拋開線程實體,進程中的程序代碼是不可能執(zhí)行的。操作系統(tǒng)創(chuàng)建進程后,會創(chuàng)建一個線程執(zhí)行進程中的代碼。通常我們把這個線程稱為該進程的主線程,主線程在運行過程中可能會創(chuàng)建其他線程。一般將主線程創(chuàng)建的線程稱為該進程的輔助線程。
從從屬關(guān)系的角度來講,線程是屬于進程的,線程運行在進程空間內(nèi),同一進程所產(chǎn)生的線程共享同一內(nèi)存空間。一個進程可以包含若干線程,線程可以幫助應(yīng)用程序同時做幾件事(比如一個線程向磁盤寫入文件,另一個則接收用戶的按鍵操作并及時做出反應(yīng),互相不干擾),在程序被運行后中,系統(tǒng)首先要做的就是為該程序進程建立一個默認線程,然后程序可以根據(jù)需要自行添加或刪除相關(guān)的線程。
線程不像進程按照嚴格的父子關(guān)系來組織。和一個進程相關(guān)的線程組成一個對等線程池,一個線程可以殺死其任意對等線程。每個線程都能讀寫相同的共享數(shù)據(jù)。當進程退出時該進程所產(chǎn)生的線程都會被強制退出并清除。
?
Win32多線程架構(gòu)
主線程在運行過程中可以創(chuàng)建新的輔助線程,即所謂的多線程。多線程較之多進程的優(yōu)點在于,線程的上下文要比進程的上下文小的多,所以線程的上下文切換要比進程的上下文切換快得多。
進程的主線程的進入點為main函數(shù),輔助線程的進入點為線程函數(shù)(Thread Procedure)。
進程中同時可以有多個線程在執(zhí)行,為了使它們能夠“同時”運行,操作系統(tǒng)為每個線程輪流分配?CPU時間片。為了充分地利用?CPU,提高軟件產(chǎn)品的性能,一般情況下,Win32基于窗口的GUI應(yīng)用程序使用主線程接受用戶的輸入,顯示運行結(jié)果,而創(chuàng)建新的線程(稱為輔助線程)來處理長時間的操作,比如讀寫文件、訪問網(wǎng)絡(luò)等。這樣,即便是在程序忙于繁重的工作時也可以由專門的線程響應(yīng)用戶命令。
換句話說,程序的主線程是一個老板,而其它線程是老板的職員。老板將繁重的工作丟給職員處理,而他自己保持和外界的聯(lián)系。因為那些線程僅僅是職員,所以它們不會舉行記者招待會,它們會認真地完成分內(nèi)職務(wù),將結(jié)果報告給老板,并等待他們的下一個任務(wù)。而對外的一切事務(wù)談判都交由老板等管理層交涉。
一個程序中的線程是同一程序的不同部分,因此他們共享程序的資源,如內(nèi)存和打開的文件。因為線程共享程序的內(nèi)存,所以他們還共享靜態(tài)變量。然而,每個線程都有他們自己的堆棧,因此動態(tài)變量(自動變量)對每個線程是唯一的。每個線程還有各自的處理器狀態(tài)(和數(shù)學(xué)協(xié)處理器狀態(tài)),這個狀態(tài)在進行線程切換期間被儲存和恢復(fù),也即所謂的上下文切換。
多線程技術(shù)主要解決處理器單元內(nèi)多個線程執(zhí)行的問題,它可以顯著減少處理器單元的閑置時間,增加處理器單元的吞吐能力。理論上,安裝了N個CPU的PC,在某一時刻,系統(tǒng)底層所能并發(fā)執(zhí)行的線程個數(shù)為N。對于單核PC,多線程微觀串行,如果開辟的線程過多,則頻繁的線程上下文切換將會耗費較多的CPU時鐘周期。因此,多線程并不是多多益善,這便涉及到多線程的池化管理問題。
?
Win32線程消息隊列
與基于MS - DOS的應(yīng)用程序不同,Windows的應(yīng)用程序是事件(消息)驅(qū)動的。它們不會顯式地調(diào)用函數(shù)(如C運行時庫調(diào)用)來獲取輸入,而是等待windows向它們傳遞輸入。?windows系統(tǒng)把應(yīng)用程序的輸入事件傳遞給各個窗口,每個窗口有一個函數(shù),稱為窗口消息處理函數(shù)。窗口消息處理函數(shù)處理各種用戶輸入,處理完成后再將控制權(quán)交還給系統(tǒng)。窗口消息處理函數(shù)一般是在注冊一個窗口的時候指定的。
在Windows NT和Windows 98中,沒有消息隊列線程和無消息隊列線程的區(qū)別,每個線程在建立時都會有它自己的消息隊列,可通過lkd> dt nt!_kthread查看其中的_KTHREAD::_KQUEUE*對象Queue。應(yīng)用程序調(diào)用GetMessage/PeekMessage函數(shù)從調(diào)用線程消息隊列中取出指定窗口(HWND)的消息,調(diào)用SendMessage/PostMessage函數(shù)向調(diào)用線程消息隊列中壓入指定窗口(HWND)的消息。
?
書籍參考:
《Windows 2000系統(tǒng)編程》
《Windows核心編程》
《Win32多線程程序設(shè)計》
《C++面向?qū)ο蠖嗑€程編程》
?
專題參考:
《Windows進程/線程淺談》
《架構(gòu)設(shè)計:進程還是線程?》
?
《C++多線程》
《VC多線程編程》
《從單線程到多線程》
《Windows多線程編程總結(jié)》
《深入淺出Win32多線程程序設(shè)計》
《Multithreaded Programming with ThreadMentor》
?
《Chrome源碼剖析[1] - Chrome的多線程模型》
《Chrome源碼剖析[2] - Chrome的進程間通信》
《Chrome源碼剖析[3] - Chrome的進程模型》
總結(jié)
以上是生活随笔為你收集整理的Win32多线程编程(1) — 基础概念篇的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 线程的调度
- 下一篇: Win32多线程编程(2) — 线程控制