Windows核心编程 第七章 线程的调度、优先级和亲缘性(上)
第7章?線程的調(diào)度、優(yōu)先級和親緣性
? ? 搶占式操作系統(tǒng)必須使用某種算法來確定哪些線程應該在何時調(diào)度和運行多長時間。本章將要介紹Microsoft Windows 98和Windows 2000使用的一些算法。
? ? 上一章介紹了每個線程是如何擁有一個上下文結(jié)構(gòu)的, 這個結(jié)構(gòu)維護在線程的內(nèi)核對象中。這個上下文結(jié)構(gòu)反映了線程上次運行時該線程的 C P U寄存器的狀態(tài)。每隔2 0 m s左右,Wi n d o w s要查看當前存在的所有線程內(nèi)核對象。在這些對象中,只有某些對象被視為可以調(diào)度的對象。Wi n d o w s選擇可調(diào)度的線程內(nèi)核對象中的一個,將它加載到 C P U的寄存器中,它的值是上次保存在線程的環(huán)境中的值。這項操作稱為上下文轉(zhuǎn)換。 Wi n d o w s實際上保存了一個記錄,它說明每個線程獲得了多少個運行機會。使用M i c r o s o f t?S p y + +這個工具,就可以了解這個情況。圖7 - 1顯示了一個線程的屬性。注意,該線程已經(jīng)被調(diào)度了37 379次。
? ? 目前,線程正在執(zhí)行代碼,并對它的進程的地址空間中的數(shù)據(jù)進行操作。再過 2 0 m s左右,Wi n d o w s就將C P U的寄存器重新保存到線程的上下文中。線程不再運行。系統(tǒng)再次查看其余的可調(diào)度線程內(nèi)核對象, 選定另一個線程的內(nèi)核對象,將該線程的上下文加載到C P U的寄存器中,然后繼續(xù)運行。當系統(tǒng)引導時,便開始加載線程的上下文,讓線程運行,保存上下文和重復這些操作,直到系統(tǒng)關(guān)閉。
? ? 總之,這就是系統(tǒng)對線程進行調(diào)度的過程。這很簡單,是不是? Wi n d o w s被稱為搶占式多線程操作系統(tǒng),因為一個線程可以隨時停止運行,隨后另一個線程可進行調(diào)度。如你所見,可以對它進行一定程度的控制,但是不能太多。記住,無法保證線程總是能夠運行,也不能保證線程能夠得到整個進程,無法保證其他線程不被允許運行等等。
?其實我自己以外發(fā)現(xiàn)vs上自帶這個工具,在工具里面。界面如下(vs2012的)
?
?
? ? 注意 程序員常常問我,如何才能保證線程在某個事件的某個時間段內(nèi)開始運行,比如,如何才能確保某個線程在數(shù)據(jù)從串行端口傳送過來的 1 m s內(nèi)開始運行呢?我的回答是,辦不到。實時操作系統(tǒng)才能作出這樣的承諾,但是Wi n d o w s不是實時操作系統(tǒng)。實時操作系統(tǒng)必須清楚地知道它是在什么硬件上運行,這樣它才能知道它的硬盤控制器和鍵盤等的等待時間。M i c r o s o f t對Wi n d o w s規(guī)定的目標是,使它能夠在各種不同的硬件上運行,即能夠在不同的 C P U、不同的驅(qū)動器和不同的網(wǎng)絡(luò)上運行。簡而言之,Wi n d o w s沒有設(shè)計成為一種實時操作系統(tǒng)。
? ? 盡管應強調(diào)這樣一個概念,即系統(tǒng)只調(diào)度可以調(diào)度的線程,但是實際情況是,系統(tǒng)中的大多數(shù)線程是不可調(diào)度的線程。例如,有些線程對象的暫停計數(shù)大于 1。這意味著該線程已經(jīng)暫停運行,不應該給它安排任何 C P U時間。通過調(diào)用使用 C R E AT E _ S U S P E N D E D標志的C r e a t e P r o c e s s或C r e a t e T h r e a d函數(shù),可以創(chuàng)建一個暫停的線程。 (本章后面還要介紹 S u s p e n dT h r e a d和R e s u m e T h r e a d函數(shù)。 )
? ? 除了暫停的線程外,其他許多線程也是不可調(diào)度的線程,因為它們正在等待某些事情的發(fā)生。例如,如果運行N o t e p a d,但是并不鍵入任何數(shù)據(jù),那么N o t e p a d的線程就沒有什么事情要做。系統(tǒng)不給無事可做的線程分配C P U時間。當移動N o t e p a d的窗口時,或者N o t e p a d的窗口需要刷新它的內(nèi)容,或者將數(shù)據(jù)鍵入N o t e p a d,系統(tǒng)就會自動使N o t e p a d的線程成為可調(diào)度的線程。
這并不意味著N o t e p a d的線程立即獲得了C P U時間。它只是表示N o t e p a d的線程有事情可做,系統(tǒng)將設(shè)法在某個時間(不久的將來)對它進行調(diào)度。
7.1 暫停和恢復線程的運行
????在線程內(nèi)核對象的內(nèi)部有一個值,用于指明線程的暫停計數(shù)。當調(diào)用 C r e a t e P r o c e s s或C r e a t e T h r e a d函數(shù)時,就創(chuàng)建了線程的內(nèi)核對象,并且它的暫停計數(shù)被初始化為 1。這可以防止線程被調(diào)度到C P U中。當然,這是很有用的,因為線程的初始化需要時間,你不希望在系統(tǒng)做好充分的準備之前就開始執(zhí)行線程。
當線程完全初始化好了之后, C r e a t e P r o c e s s或C r e a t e T h r e a d要查看是否已經(jīng)傳遞了C R E ATE_ SUSPENDED標志。如果已經(jīng)傳遞了這個標志,那么這些函數(shù)就返回,同時新線程處于暫停狀態(tài)。如果尚未傳遞該標志,那么該函數(shù)將線程的暫停計數(shù)遞減為 0。當線程的暫停計數(shù)是 0的時候,除非線程正在等待其他某種事情的發(fā)生,否則該線程就處于可調(diào)度狀態(tài)。
? ? 在暫停狀態(tài)中創(chuàng)建一個線程,就能夠在線程有機會執(zhí)行任何代碼之前改變線程的運行環(huán)境(如優(yōu)先級) 。一旦改變了線程的環(huán)境,必須使線程成為可調(diào)度線程。要進行這項操作,可以調(diào)用R e s u m e T h r e a d,將調(diào)用 C r e a t e T h r e a d函數(shù)時返回的線程句柄傳遞給它(或者是將傳遞給C r e a t e P r o c e s s的p p i P r o c I n f o參數(shù)指向的線程句柄傳遞給它) :
DWORD ResumeThread(HANDLE hThread);
? ? 如果 R e s u m e T h r e a d函數(shù)運行成功,它將返回線程的前一個暫停計數(shù),否則返回0 x F F F F F F F F。
? ? 單個線程可以暫停若干次。如果一個線程暫停了 3次,它必須恢復3次,然后它才可以被分配給一個C P U。當創(chuàng)建線程時,除了使用 C R E AT E _ S U S P E N D E D外,也可以調(diào)用 S u s p e n dT h r e a d函數(shù)來暫停線程的運行:
? ? DWORD SuspendThread(HANDLE hThread);
? ? 任何線程都可以調(diào)用該函數(shù)來暫停另一個線程的運行(只要擁有線程的句柄) 。不用說,線程可以自行暫停運行,但是不能自行恢復運行。與 R e s u m e T h r e a d一樣,S u s p e n d T h r e a d返回的是線程的前一個暫停計數(shù)。線程暫停的最多次數(shù)可以是 M A X I M U M _ S U S P E N D _ C O U N T次(在Wi n N T. h中定義為1 2 7) 。注意,S u s p e n d T h r e a d與內(nèi)核方式的執(zhí)行是異步進行的,但是在線程恢復運行之前,不會發(fā)生用戶方式的執(zhí)行。
? ? 在實際環(huán)境中,調(diào)用S u s p e n d T h r e a d時必須小心,因為不知道暫停線程運行時它在進行什么操作。如果線程試圖從堆棧中分配內(nèi)存,那么該線程將在該堆棧上設(shè)置一個鎖。當其他線程試圖訪問該堆棧時,這些線程的訪問就被停止,直到第一個線程恢復運行。只有確切知道目標線程是什么(或者目標線程正在做什么) ,并且采取強有力的措施來避免因暫停線程的運行而帶來的問題或死鎖狀態(tài),S u s p e n d T h r e a d才是安全的(死鎖和其他線程同步問題將在第8、9和1 0章介紹) 。
?7.2 暫停和恢復進程的運行
? ? 對于Wi n d o w s來說,不存在暫停或恢復進程的概念,因為進程從來不會被安排獲得 C P U時間。但是,曾經(jīng)有人無數(shù)次問我如何暫停進程中的所有線程的運行。 Wi n d o w s確實允許一個進程暫停另一個進程中的所有線程的運行,但是從事暫停操作的進程必須是個調(diào)試程序。特別是,進程必須調(diào)用Wa i t F o r D e b u g E v e n t和C o n t i n u e D e b u g E v e n t之類的函數(shù)。
? ? 由于競爭的原因,Wi n d o w s沒有提供其他方法來暫停進程中所有線程的運行。例如,雖然許多線程已經(jīng)暫停,但是仍然可以創(chuàng)建新線程。從某種意義上說,系統(tǒng)必須在這個時段內(nèi)暫停所有新線程的運行。M i c r o s o f t已經(jīng)將這項功能納入了系統(tǒng)的調(diào)試機制。
? ? 雖然無法創(chuàng)建絕對完美的S u s p e n d P r o c e s s函數(shù),但是可以創(chuàng)建一個該函數(shù)的實現(xiàn)代碼,它能夠在許多條件下出色地運行。下面是我的S u s p e n d P r o c e s s函數(shù)的實現(xiàn)代碼:
?
? ? 我的S u s p e n d P r o c e s s函數(shù)使用To o l H e l p函數(shù)來枚舉系統(tǒng)中的線程列表。當我找到作為指定進程的組成部分的線程時,我調(diào)用O p e n T h r e a d:
?
????這個新Windows 2000函數(shù)負責找出帶有匹配的線程 I D的線程內(nèi)核對象,對內(nèi)核對象的使用計數(shù)進行遞增,然后返回對象的句柄。運用這個句柄,我調(diào)用 S u s p e n d T h r e a d (或R e s u m e T h r e a d )。由于O p e n T h r e a d在Windows 2000中是個新函數(shù),因此我的S u s p e n d P r o c e s s函數(shù)在Windows 95或Windows 98上無法運行,在Windows NT 4.0或更早的版本上也無法運行。
? ? 也許你懂得為什么S u s p e n d P r o c e s s不能總是運行,原因是當枚舉線程組時,新線程可以被創(chuàng)建和撤消。因此,當我調(diào)用C r e a t e To o l h e l p 3 2 S n a p s h o t后,一個新線程可能會出現(xiàn)在目標進程中,我的函數(shù)將無法暫停這個新線程。過了一些時候,當調(diào)用 S u s p e n d P r o c e s s函數(shù)來恢復線程的運行時,它將恢復它從未暫停的一個線程的運行。更糟糕的是,當枚舉線程 I D時,一個現(xiàn)有的線程可能被撤消,一個新線程可能被創(chuàng)建,這兩個線程可能擁有相同的 I D。這將會導致該函數(shù)暫停任意些個(也許在目標進程之外的一個進程中的)線程的運行。
? ? 當然,這些情況不太可能出現(xiàn)。如果非常了解目標進程是如何運行的,那么這些問題也許根本不是問題。我提供這個函數(shù)供酌情使用。
7.3 睡眠方式
? ? 線程也能告訴系統(tǒng),它不想在某個時間段內(nèi)被調(diào)度。這是通過調(diào)用 S l e e p函數(shù)來實現(xiàn)的:
? ? ?VOID Sleeo(DWORD dwMilliseconds);
? ? 該函數(shù)可使線程暫停自己的運行,直到 d w M i l l i s e c o n d s過去為止。關(guān)于S l e e p函數(shù),有下面幾個重要問題值得注意:
? 調(diào)用S l e e p,可使線程自愿放棄它剩余的時間片。
? 系統(tǒng)將在大約的指定毫秒數(shù)內(nèi)使線程不可調(diào)度。不錯,如果告訴系統(tǒng),想睡眠 1 0 0 m s,那么可以睡眠大約這么長時間,但是也可能睡眠數(shù)秒鐘或者數(shù)分鐘。記住, Wi n d o w s不是個實時操作系統(tǒng)。雖然線程可能在規(guī)定的時間被喚醒,但是它能否做到,取決于系統(tǒng)中還有什么操作正在進行。
? 可以調(diào)用S l e e p,并且為d w M i l l i s e c o n d s參數(shù)傳遞I N F I N I T E。這將告訴系統(tǒng)永遠不要調(diào)度該線程。這不是一件值得去做的事情。最好是讓線程退出,并還原它的堆棧和內(nèi)核對象。
? 可以將0傳遞給S l e e p。這將告訴系統(tǒng),調(diào)用線程將釋放剩余的時間片,并迫使系統(tǒng)調(diào)度另一個線程。但是,系統(tǒng)可以對剛剛調(diào)用 S l e e p的線程重新調(diào)度。如果不存在多個擁有相同優(yōu)先級的可調(diào)度線程,就會出現(xiàn)這種情況。
7.4 轉(zhuǎn)換到另一個線程
? ? 系統(tǒng)提供了一個稱為 S w i t c h To T h r e a d的函數(shù),使得另一個可調(diào)度線程(如果存在能夠運行) :
? ? BOOL SwitchThread();
? ? 當調(diào)用這個函數(shù)的時候,系統(tǒng)要查看是否存在一個迫切需要 C P U時間的線程。如果沒有線程迫切需要C P U時間,S w i t c h To T h r e a d就會立即返回。如果存在一個迫切需要C P U時間的線程,S w i t c h To T h r e a d就對該線程進行調(diào)度(該線程的優(yōu)先級可能低于調(diào)用 S w i t c h To T h r e a d的線程) 。這個迫切需要C P U時間的線程可以運行一個時間段,然后系統(tǒng)調(diào)度程序照常運行。
? ? 該函數(shù)允許一個需要資源的線程強制另一個優(yōu)先級較低、而目前卻擁有該資源的線程放棄該資源。如果調(diào)用S w i t c h To T h r e a d函數(shù)時沒有其他線程能夠運行,那么該函數(shù)返回 FA L S E,否則返回一個非0值。
調(diào)用S w i t c h To T h r e a d函數(shù)與調(diào)用S l e e p是相似的,并且傳遞給它一個 0 m s的超時。差別是S w i t c h To T h r e a d允許優(yōu)先級較低的線程運行。即使低優(yōu)先級線程迫切需要 C P U時間,S l e e p也能夠立即對調(diào)用線程重新進行調(diào)度。
? ? Windows 98 Windows 98沒有配備該函數(shù)的非常有用的實現(xiàn)代碼。
7.5 線程的運行時間
? ? 有時想要計算線程執(zhí)行某個任務需要多長的時間。許多人采取的辦法是編寫類似下面的代碼:
?
? ? 這個代碼做了一個簡單的假設(shè):即它不會被中斷。但是,在搶占式操作系統(tǒng)中,永遠無法知道線程何時被賦予C P U時間。當取消線程的C P U時間時,就更難計算線程執(zhí)行不同任務時所用的時間。我們需要一個函數(shù),以便返回線程得到的 C P U時間的數(shù)量。幸運的是,Wi n d o w s提供了一個稱為G e t T h r e a d Ti m e s的函數(shù),它能返回這些信息:
?
?
? ? 使用這個函數(shù),可以通過使用下面的代碼確定執(zhí)行復雜的算法時需要的時間量:
?
注意,G e t P r o c e s s Ti m e s是個類似G e t T h r e a d Ti m e s的函數(shù),適用于進程中的所有線程:
?
? ? G e t P r o c e s s Ti m e s返回的時間適用于某個進程中的所有線程(甚至是已經(jīng)終止運行的線程) 。例如,返回的內(nèi)核時間是所有進程的線程在內(nèi)核代碼中經(jīng)過的全部時間的總和。
? ? Windows 98 遺憾的是,G e t T h r e a d Ti m e s和G e t P r o c e s s Ti m e s這兩個函數(shù)在Wi n d o w s9 8中不起作用。在Windows 98中,沒有一個可靠的機制可供應用程序來確定線程或進程已經(jīng)使用了多少C P U時間。
對于高分辨率的配置文件來說,G e t T h r e a d Ti m e s并不完美。Wi n d o w s確實提供了一些高分辨率性能函數(shù):
?
?
? ? ?雖然這些函數(shù)認為,正在執(zhí)行的線程并沒有得到搶占的機會,但是高分辨率的配置文件是為短期存在的代碼塊設(shè)置的。為了使這些函數(shù)運行起來更加容易一些,我創(chuàng)建了下面這個 C + +類:
?
使用這個類如下:
?
總結(jié)
以上是生活随笔為你收集整理的Windows核心编程 第七章 线程的调度、优先级和亲缘性(上)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: AsSystemRum 系统提权工具 实
- 下一篇: Windows核心编程 第七章 线程的调