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