线程的调度、优先级和亲缘性——Windows核心编程学习手札系列之七
線程的調度、優先級和親緣性
——Windows核心編程學習手札系列之七
每個線程都擁有一個上下文結構,在線程的內核對象中,記錄線程上次運行時該線程的CPU寄存器狀態。Windows會每隔20ms左右查看當前存在的所有線程內核對象,在這些對象中,選擇可調度的對象,將其上下文結構(內核對象中)加載到CPU的寄存器中,其值就是上次保存在線程環境中的值,此為上下文切換。Windows保存了一個記錄,說明每個線程獲得調度的機會,Microsoft的Spy++工具,可以查看這個。操作系統只調度可以調度的線程,實際中,大多數線程是不可調度的,如暫停的線程(CREATE_SUSPENDED標志)以及等待事件觸發的線程等。
線程內核對象的內部有線程的暫停計數值,當調用CreateProcess或CreateThread函數時,就創建了線程的內核對象,且它的暫停計數被初始化為1,防止線程被調度到CPU中,因為線程初始化需要時間,需要在準備好開始執行線程。當線程完全初始化后,CreateProcess或CreateThread要查看是否傳遞了CREATE_SUSPENDED標志,如果傳遞了給標志,那函數返回,新線程處于暫停狀態;如未傳遞該標志,那函數將線程的暫停計數遞減為0,此時如果線程沒有等待事件,那么該線程處在可調度的狀態。在暫停狀態中創建一個線程,就可以在線程有機會執行前改變線程的運行環境(如優先級)。要恢復線程的可調度性,可調用函數ResumeThread,將調用CreateThread函數時返回的線程句柄傳遞給它。DWORD ResumeThread(HANDLE hThread)運行成功將返回線程的前一個暫停計數,否則返回0xFFFFFFFF值,單個線程可以暫停若干次,如一個線程序暫停了3次需要恢復3次才可以被分配給一個CPU。創建線程,除傳遞CREATE_SUSPENDED標志外,還可以調用DWORD SuspendThread(HANDLE hThread)函數來暫停線程的運行。任何線程都可以調用該函數來暫停另一個線程的運行(只要有線程的句柄),線程可以自行暫停運行,但不能自行恢復運行。與ResumeThread一樣,SuspendThread返回的是線程的一個暫停計數,暫停計數最多是MAXIMUM_SUSPEND_COUNT次(在WinNT.h中定義為127),SuspendThread與內核方式是異步運行的,但在線程恢復運行之前,不會發生用戶方式的執行。使用SuspendThread函數暫停線程應該在確切知道目標線程正在做什么情況下,并采取措施避免因暫停線程的運行而帶來的問題或死鎖,因為如果線程正試圖從堆棧中分配內存,該線程會在該堆棧上設置鎖,當其他線程訪問該堆棧時將被停止,直到暫停線程的恢復。
Windows中不存在暫停或恢復進程的概念,允許一個進程暫停另一個進程中所有線程的運行,但從事暫停操作的進程必須是個調試程序,也需要調用WaitForDebugEvent和ContinueDebugEvent之類的函數。總言,Windows沒有提供方法暫停進程中的所有線程運行。這里有一個作者寫的暫停進程函數作為要暫停進程的參考用。
Void SuspendProcess(DWORD dwProcessID,BOOL fSuspend){
?????? //get the list of threads in the system
?????? HANDLE hSnapshot=CreateToolhelp32Snapshot(
?????????????????????????????????????????????????????????????? TH32CS_SNAPTHREAD,dwProcessID);
?????? if(hSnapshot != INVALID_HANDLE_VALUE){
????????????? //walk the list of threads
????????????? THREADENTRY32 te={sizeof(te)};
????????????? BOOL fOk=Thread32First(hSnapshot,&te);
????????????? for(;fOk;fOk=Thread32Next(hSnapshot,&te)){
???????????????????? //Is this thread in the desired process?
???????????????????? if(te.th32OwnerProcessID = = dwProcessID){
??????????????????????????? //attempt to convert the thread ID into a handle
??????????????????????????? HANLDE hThread=OpenThread(THREAD_SUSPEND_RESUME,FALSE,
??????????????????????????????????????????????????????????????????????????????????? te.th32ThreadID);
??????????????????????????? if(hThread!=NULL){
?????????????????????????????????? //suspend or resume the thread
?????????????????????????????????? if(fSuspend)
????????????????????????????????????????? SuspendThread(hThread);
?????????????????????????????????? Else
????????????????????????????????????????? ResumeThread(hThread);
??????????????????????????? }
??????????????????????????? CloseHandle(hThread);
???????????????????? }
????????????? }
????????????? CloseHandle(hSnapShot);
?????? }
}
該函數使用ToolHelp函數枚舉系統中的線程列表,當找到作為指定進程的組成部分的線程時,就調用HANDLE OpenThread(DWORD dwDesiredAccess,BOOL bInheritHandle,DWORD dwThreadID)函數找出匹配線程ID的線程內核對象,對內核對象的使用計數進行遞增,然后返回對象的句柄,運用這個句柄可調用SuspendThread或ResumeThread來暫停或恢復線程的運行。
如果線程不想愛某個時間段被調度,可以調用Sleep函數來實現:void sleep(DWORD dwMillisecondes),這個函數使線程暫停自己運行,知道dwMillisecondes后,該函數需要注意的是:1)調用sleep函數,使線程自愿放棄它的剩余時間片;2)系統將在大約的指定毫秒內使線程不可調度;3)可為sleep函數的參數dwMillisecondes傳遞INFINITE,告訴系統永不調度該線程,這不提倡,最好讓線程退出,還原其堆棧和內核對象;4)可將0傳遞給sleep,這樣調用線程將釋放剩余時間片,并迫使系統調度另一個線程。
函數BOOL SwitchToThread()可轉換到另一個線程。當調用這個函數時,系統查看是否存在一個迫切需要CPU時間的線程,如沒有,SwitchToThread就會立即返回,如果存在,SwitchToThread就對該線程進行調度。這個函數允許一個需要資源的線程強制另一個優先級較低、而目前卻擁有該資源的線程放棄該資源。如果調用SwitchToThread函數時沒有其他線程能夠運行,則返回FALSE,否則返回非0值。與sleep函數相似,區別在于SwitchToThread允許優先級較低的線程運行,即使低優先級線程迫切需要CPU時間,sleep也能夠立即對調用線程重新進行調度。
要獲取線程的運行時間需要調用GetThreadTimes函數,這個返回線程得到的CPU時間數量。具體實現代碼如下:
#include <windows.h>
?
__int64 FileTimeToQuadWord(PFILETIME pft)
{
?????? return(Int64ShllMod32(pft->dwHighDateTime,32) | pft->dwLowDateTime);
}
?
int main(int argc, char* argv[])
{
?????? FILETIME ftKernelTimeStart,ftKernelTimeEnd;
?????? FILETIME ftUserTimeStart,ftUserTimeEnd;
?????? FILETIME ftDummy;
?????? __int64 qwKernelTimeElapsed,qwUserTimeElapsed,qwTotalTimeElapsed;
?
?????? //Get start times
?????? GetThreadTimes(GetCurrentThread(),&ftDummy,&ftDummy,&ftKernelTimeStart,&ftUserTimeStart);
?
?????? //perform complex algorithm here.
?????? //Get ending times.
?????? GetThreadTimes(GetCurrentThread(),&ftDummy,&ftDummy,&ftUserTimeEnd,&ftUserTimeEnd);
?
?????? //Get the elapsed kernel and user times by converting the start and end times form FILETIMEs
?????? //to quad words and then subtract the start times from the end times.
?????? qwKernelTimeElapsed=FileTimeToQuadWord(&ftKernelTimeEnd)-FileTimeToQuadWord(&ftKernelTimeStart);
?
?????? qwUserTimeElapsed=FileTimeToQuadWord(&ftUserTimeEnd)-FileTimeToQuadWord(&ftUserTimeStart);
?
?????? //Get total time duration by adding the kernel and user times.
?????? qwTotalTimeElapsed=qwKernelTimeElapsed+qwUserTimeElapsed;
?
?????? // the total elapsed time is in qwTotalTimeElapsed and display in console
?????? printf("The executing times of thread is %d /n",qwTotalTimeElapsed);
?????? //printf("Hello World!/n");
?????? return 0;
}
環境結構使系統保留線程的狀態,在下次線程擁有CPU時,能夠回到上次中斷運行的地方。Windows允許查看線程內核對象的內部情況,以便抓取它當前的一組CPU寄存器,若要執行該項操作,可調用GetThreadContext函數:
BOOL GetThreadContext(HANDLE hThread,PCONTEXT pContext);
調用該函數,只需指定一個CONTEXT結構,對某些標志(該結構中的ContextFlags成員)進行初始化,指明想要收回那些寄存器,并將該結構的地址傳遞給函數,函數會將數據填入到所要求的成員中。在調用GetThreadContext函數前,應調用SuspendThread,否則線程可能被調度,且線程的環境與所收回的不同。一個線程實際有兩個環境,一個是用戶方式,一個是內核方式。GetThreadContext只能返回線程的用戶方式環境,如調用SuspendThread來停止線程的運行,但該線程目前正運行在內核方式下,那么即使SuspendThread尚未暫停該線程的運行,它的用戶方式仍然處于穩定狀態。線程在恢復用戶方式之前,無法執行更多的用戶方式代碼,因此可放心將線程視為處于暫停狀態,GetThreadContext函數將能正常運行。CONTEXT結構的ContextFlags成員并不與任何CPU寄存器對應。無論是何種CPU結構,該成員存在于CONTEXT結構定義中。ContextFlags成員用于向GetThreadContext函數指明想要檢索那些寄存器。如想獲得線程的控制寄存器,可以用如下代碼:
//Create a CONTEXT structure.
?????? CONTEXT Context;
?????? //Tell the system that we are interested in only the control registers.
?????? Context.ContextFlags=CONTEXT_CONTROL;
?????? //Tell the system to get the registers associated with a thread.
?????? GetThreadContext(hThread,&Context);
在調用GetThreadContext之前,須對CONTEXT結構中的ContextFlags成員進行初始化,如想獲得線程的控制寄存器和整數寄存器,需要進行下面的ContextFlags初始化:
Context.ContextFlags=CONTEXT_CONTROL | CONTEXT_INTEGER;
也可以獲得線程的所有重要的寄存器(Mcirosoft認為最常用的寄存器):
Context.ContextFlags=CONTEXT_FULL;
當GetThreadContext返回時,可容易查看線程的任何寄存器值,要編寫與CPU相關的代碼。Windows可修改CONTEXT結構中的成員,然后通過SetThreadContext將新寄存器值放回線程的內核對象中:
BOOL SetThreadContext(HANDLE hThread,CONST CONTEXT *pContext);
修改其環境的線程前應先暫停線程,否則結果不得而知。下面的代碼是演示:
//Create a CONTEXT structure.
?????? CONTEXT Context;
?????? //stop the thead from running
?????? SuspendThread(hThread);
??????
?????? //Get the thread's control registers.
?????? Context.ContextFlags=CONTEXT_CONTROL;
?????? //Tell the system to get the registers associated with a thread.
?????? GetThreadContext(hThread,&Context);
?
?????? //Make the instruction pointer point to the address of your choice.
?????? //Here I've arbitrarily set the address instruction pointer to 0x00010000
#if defined(_ALPHA_)
?????? Context.Fir=0x00010000;
#elif defined(_X86_)
?????? Context.Eip=0x00010000;
#else
#error Module contains CPU-specific code;modify and recompile.
#endif
?
?????? //Set the thread's registers to reflect the changed values.
?????? //It's not really necessary to reset the ControlFlags memeber because it was set earlier.
?????? Context.ContextFlags=CONTEXT_CONTROL;
?????? SetThreadContext(hThread,&Context);
?
?????? //Resuming the thread will cause it to begin execution at address 0x00010000.
?????? RusumeThread(hThread);
如此處理,可能導致遠程線程中的違規,向用戶顯示未處理的異常消息框,同時遠程進程終止運行。GetThreadContext和SetThreadContext函數可以對線程進行多方面控制,但要慎用。
線程被賦予不同的優先級,決定系統調度程序選擇調度哪個線程來運行(使其擁有CPU)。每個線程都被賦予一個從0(最低)到31(最高)的優先級號碼。當系統引導時,會創建一個特殊的線程,稱為0頁線程,該線程被賦予優先級為0,是整個系統中唯一的一個在優先級0上運行的線程。當系統中沒有任何需要執行操作時,0頁線程負責將系統中的所有空閑RAM頁面置0。Windows支持6個優先級類:空閑、低于正常、正常、高于正常、高和實時,一般程序都處在正常這個級別。Windows Explorer是在高優先級上運行的,大多數時間Explorer線程是暫停的,等待用戶按下操作鍵或點擊鼠標按照時被喚醒。當Explorer的線程處于暫停狀態時,系統不分配CPU給它的線程,這樣次優先級的線程可以得到調度。但一旦用戶有按鍵操作,系統就會喚醒Explorer線程,如果低優先級線程正在運行,系統會立即搶在這些線程之前讓Explorer的線程優先運行。應該避免使用實時這個最高的優先級類,因為它可能干涉操作系統任務的運行,可能阻止必要的磁盤I/O信息和網絡信息的產生。
當調用CreateProcess時,fdwCreate參數可以傳遞需要的優先級類。可通過調用SetPriorityClass來改變優先級類:
BOOL SetPriorityClass(HANDLE hProcess,DWORD fdwPriority);
該函數將hProcess標識的優先級改為fdwPriority參數中設定的值。由于該函數帶有一個進程句柄,所以只要擁有進程的句柄和足夠的訪問權,就可以改變系統中運行的任何進程的優先級類。檢索進程的優先級類函數:DWORD GetPriorityClass(HANDLE hProcess)。如果使用Start命令來啟動程序,可以使用一個開關來設定應用程序的起始優先級,如在命令外殼輸入如下命令可使系統啟動Calculator,并在開始時按空閑優先級來運行它:
C:/>START /LOW CALC.EXE
Start命令還能識別/BELOWNORMAL、/NORMAL、/ABOVENORMAL、/HIGT和/REALTIME等開關,以便按它們各自的優先級啟動執行一個應用程序。當然,一旦應用程序啟動運行,可以通過調用SetPriorityClass函數改變自己的優先級。
當系統將線程分配給處理器時,Windows2000使用軟親緣性來進程操作,這意味著如果所有其他因素相同的話,它將設法在它上次運行的哪個處理器上運行線程,讓線程留在單個處理器上,有助于重復使用仍然在處理器的內存高速緩存中的數據。Windows2000允許設置進程和線程的親緣性,可控制哪個CPU運行某些線程,稱為硬親緣性。計算機在引導時,要確定機器中有多少個CPU可供使用。通過調用GetSystemInfo函數,應用程序可查詢機器中的CPU數量。按照默認設置,任何線程都可以調度到這些CPU中的任何一個上去運行。為限制在可用CPU的子集上運行的單個進程中的線程數量,可調用:
BOOL SetProcessAffinityMask(HANDLE hProcess,DWORD_PTR dwProcessAffinityMask);
第一個參數hProcess用于指明影響的是哪個進程;第二個參數dwProcessAffinityMask是位屏蔽,用于指明線程可以在那些CPU上運行,如傳遞0x00000005(二進制0101,0和2位是真值)表示該進程中的線程可以在CPU0和CPU2上運行,但是不能愛CPU1和CPU3至31上運行。子進程可以繼承進程的親緣性。同時可通過下面函數返回進程的親緣性屏蔽:
BOOL GetProcessAffinityMask(HANDLE hProcess,
???????????????????????????????????????????????? PDWORD_PTR pdwProcessAffinityMask,
???????????????????????????????????????????????? PDWORD_PTR pdwSystemAffinityMask);
傳遞親緣性屏蔽的進程句柄,函數將填入pdwProcessAffinityMask變量,同時返回系統的親緣性屏蔽(pdwSystemAffinityMask指向的變量中)。系統的親緣性屏蔽用于指明系統的那個CPU能夠處理線程,進程的親緣性始終是一個系統的親緣性屏蔽的正確子集。上面談到的是將進程的多個線程限制到一組CPU上運行,那么同樣可以設置將進程中的一個線程限制到一組CPU上去運行。如包含4個線程的進程,在擁有4個CPU的計算機上運行,如為線程中的一個線程(正在執行非常重要的操作)增加某個CPU始終供它使用,則需要對其他三個線程限制不能在CPU0上運行,而只能在CPU1、CPU2、CPU3上運行。
通過調用SetThreadAffinityMask函數,能為各個線程設置親緣性屏蔽:
DWORD_PTR SetThreadAffinityMask(HANDLE hThread,
DWORD_PTR dwThreadAffinityMask);
函數中hThread參數用于指明要限制的線程,dwThreadAffinityMask用于指明線程能夠運行在那個CPU上,dwThreadAffinityMask是進程親緣性的相應子集,返回值是線程的前一個親緣性屏蔽。上面例子中將3個線程限制在CPU1、CPU2、CPU3上運行的代碼:
//Thread 0 can only on CPU0.
SetThreadAffinityMask(hThread0,0x00000001);
//Threads1/2/3 run on CPUs 1/2/3
SetThreadAffinityMask(hThread1,0x0000000E);
SetThreadAffinityMask(hThread2,0x0000000E);
SetThreadAffinityMask(hThread3,0x0000000E);
?????????????????? 如非 2008-12-22
總結
以上是生活随笔為你收集整理的线程的调度、优先级和亲缘性——Windows核心编程学习手札系列之七的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 泛架构之于外包IT工程
- 下一篇: 线程与内核对象的同步——Windows核