讀Windows核心編程 - 7

         windows是搶佔式的,我們無法保證線程在某個事件的某個時間段內開始運行。系統只調度可以調度的線程,但實際情況是,系統中的大多數線程是不可調度的。除了暫停的線程外,還有其他許多線程也是不可調度的,因爲它們正在等待某些事情的發生。例如,如果運行Notepad,但是並不輸入任何數據,那麼Notepad的線程就沒有什麼事情要做。系統不給無事可做的線程分配CPU時間。當我們移動Notepad窗口或者輸入的時候系統就會自動使Notepad線程成爲可調度線程。但這並不意味着Notepad線程立即獲得CPU時間。

        當調用CreateProcess或CreateThread後,就創建了線程的內核對象,並且它的暫停計數爲1。當線程初始化結束後,CreateProcess或CreateThread要查看是否已經傳遞了CREATR_SUSPENDED標誌,如果沒有傳遞,暫停計數變爲0。當暫停計數變爲0的時候,除非線程正在等待他們某種事情的發生,否則該線程處於可調度狀態。要使線程從不可調度變成可調度狀態,可以調用ResumeThread函數,將線程句柄傳遞給它。它將返回線程前一個暫停計數。注意,如果一個線程暫停了三次,那麼必須調用三次ResumeThread函數纔可使線程變爲可調度狀態。除了使用CREATE_SUSPENDED標誌外,還可以使用SuspendThread函數來暫停線程的運行。不用說,線程可以自行暫停運行,但是不能自行恢復運行。與ResumeThread一樣,SuspendThread也返回前一次暫停計數。在實際環境中,調用SuspendThread需要小心,因爲線程可能正試圖從堆裏分配內存,那麼該線程將在該堆上設置一個鎖。當其他線程試圖訪問該堆時,這些線程的訪問就被停止,直到第一個線程恢復運行。

        一般情況下,windows不允許一個進程暫停另一個進程所有線程的運行。但是有一種特殊情況,就是從事暫停操作的進程必須是個調試程序。特別是,進程必須調用WaitForDebugEvent和ContinueDebugEvent之類的函數。雖然無法創建絕對完美的SuspendProcess函數,但是可以創建一個不那麼完美的SuspendProcess函數:

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.
            HANDLE 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);
   }

}

         OpenThread函數負責找出帶有匹配線程ID的線程內核對象,對內核對象的使用計數進行遞增。這個函數是windows2000後才引入的。但是SuspendProcess並不總是可行的,當我調用了CreateToolhelp32Snapshot後,一個新進程可能出現在目標進程中,那麼我的函數將無法暫停這個線程。更有甚者,如果正好調用了CreateToolhelp32Snapshot之後,一個線程撤銷了,又一個線程創建了,而新創建的線程的ID跟被撤銷線程的ID一樣,那麼我們就有可能暫停一個目標進程之外的線程。

我們可以調用Sleep函數使線程暫停自己的運行,調用該函數有幾個重要問題值得注意:

1. 調用Sleep,可使線程自願放棄剩餘的時間片。

2. 系統將在大約的指定毫秒數內使線程不可調度。

3. 調用Sleep,並且將參數設爲INFINITE,就告訴了系統用於不要調度該線程。這不是一件值得去做的事。

4. 可以將0傳遞給Sleep。這將告訴系統,調用線程將釋放剩餘的時間片,並迫使系統調用另外一個線程。

        系統提供了一個稱爲SwitchToThread函數,使得另一個線程可調度。當調用這個函數的時候,系統要查看是否存在一個迫切需要CPU時間的線程。如果沒有線程迫切需要CPU時間,SwitchToThread就會立即返回。如果存在一個迫切需要CPU的線程,SwitchToThread就對該線程進行調度。該函數允許一個需要資源的線程強制另一個優先級較低,而目前卻擁有該資源的線程放棄該資源。如果調用SwitchToThread函數時沒有其他線程可以運行,那麼該函數返回FALSE。它與Sleep函數的差別是,SwitchToThread允許優先級較低的線程運行。

        有時我們需要計算某個任務的執行時間。許多人採取的方法是編寫類似下面的代碼:

DWORD dwStartTime = GetTickCount(); ......DWORD dwElapsedTime = GetTickCount - dwStartTime;

但是這個代碼做了一個假設:即它不會被中斷。但是,在搶佔式系統中,系統無法知道線程何時被賦予CPU時間。我們需要一個函數,以便返回線程得到的CPU時間的數量。幸運的是,windows提供了一個稱爲GetThreadTimes的函數:

BOOL GetThreadTimes(
     HANDLE hThread,                 
     PFILETIME pftCreationTime,  
//線程創建時間
     PFILETIME pftExitTime,          //線程退出時間
     PFILETIME pftkKernelTime,    //線程執行的操作系統代碼已經經過了多少個100ns的CPU時間
     PFILETIME  fptUserTime);       //線程執行的應用程序代碼已經經過了多少個100ns的CPU時間

         同樣,還有一個GetProcessTimes函數來獲得進程中所有線程的時間信息。例如,返回內核時間是指所有進程中線程在內核代碼中經過的全部時間的總和。對於高分辨率的配置文件來說,GetThreadTimes並不完美。windows確實提供了一些高分辨率性能的函數(具體用法參看核心編程page148):

BOOL QueryPerformanceFrequency(LARGE_INTEGER* pliFrequency);

BOOL QueryPerformanceCounter(LARGE_INTEGER* pliCount);

        接下來要說的是前面有一章中提過的線程上下文,也就是CONTEXT結構。CONTEXT結構是windows定義的數據結構中唯一一個特定於CPU的數據結構。比如,如果在x86計算機上,數據成員是Eax,Ebx,Ecx,Edx等等。而在Alpha處理器上,那麼數據成員包括IntV0,IntT0,IntT1,IntRa,IntZero等等。CONTEXT結構可以包含若干部分:

CONTEXT_CONTROL:包含CPU控制寄存器,比如指令指針,堆棧指針,標誌和函數返回地址。

CONTEXT_INTEGER:用於標識CPU的整數寄存器。CONTEXT_FLOATING_POINT:浮點寄存器。

CONTEXT_SEGMENTS:CPU段寄存器(僅x86)。CONTEXT_DEBUG_REGISTER:CPU調試寄存器。(僅x86)

CONTEXT_EXTENDED_REGISTERS:CPU擴展寄存器。(僅x86)

        windows允許查看線程內核對象的內部情況,以便抓取它當前的一組CPU寄存器。若要執行這項操作,只需調用GetThreadContext(HANDLE hThread, PCONTEXT pContext)函數。在調用CetThreadContext之前,應該調用SuspendThread,否則,線程可能被調度,而且線程的環境可能與你得到的不同。一個線程實際上有兩個環境,一個是用戶方式,一個是內核方式。GetThreadContext只能返回線程的用戶方式環境。如果調用SuspendThread來停止線程的運行,但是該線程目前正在調用內核方式運行,那麼,即使SuspendThread實際上尚未暫停該線程的運行,它的用戶方式扔處於穩定狀態。線程在恢復用戶方式之前,它無法執行更多的用戶方式代碼,因爲可以放心的將線程視爲暫停狀態,GetThreadContext能正常運行。(?)

        調用GetThreadContext之前,必須對CONTEXT結構中的ContextFlags成員進行初始化,指明你想要獲得的寄存器信息,比如你想要獲得控制寄存器和整數寄存器,應該像下面這樣對ContextFlags進程初始化:

Context.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER;然後在調用GetThreadContext(hThread, &Context);另外windows還提供了SetThreadContext函數使你能將修改後的CONTEXT結構放回線程的內核對象中。同樣,調用該函數前應該先調用SuspendThread並對ContextFlags進行初始化。雖然這兩個函數能使你對線程進程許多方面的控制,但是在使用時應該非常小心,針對不同的CPU可能要寫不同的代碼。如:#if defind(_ALPHA_) Context.Fir = 0x00010000; #elif defined(_x86_) Context.Eip = 0x00010000 #else...實際上,幾乎沒有應用程序調用這些函數。增加這些函數是爲了增強調試程序和其他工具的功能。

優先級云云:

        如果所有線程的優先級都相同,那麼每個線程只運行20ms,然後系統將調度另外一個可調度線程。但是,在現實中,線程被賦予不同的優先級。每個線程都會被賦予從0~30的優先級號碼。只要優先級爲31的線程是可調度的,那麼其他線程絕分不到CPU。這種情況稱爲渴求調度。但是這並不意味着低於這個優先級的線程將永遠得不到調度。比如,當優先級爲31的線程調用GetMessage函數而消息隊列中又沒有消息時,那麼系統就會暫停這個線程並調度其他可調度線程。另外,高優先級線程將搶佔低優先級線程的運行,例如,如果一個優先級爲6的線程正在運行,系統發現一個高優先級的線程準備要運行,那麼系統就會立即暫停低優先級的線程的運行,並且將一個完整的時間片賦予高優先級線程。但是有一點需注意,系統可能會根據情況動態對線程的優先級進行改變,以保障低優先級的線程不處於餓死狀態。這點稍後會提到。

        還有,當系統引導時,它會創建一個特殊的線程,稱爲0頁線程。該線程賦予優先級0,是整個系統唯一一個優先級是0的線程。當系統中沒有任何線程需要執行操作時,0頁線程負責將系統中所有空閒的RAM頁面置0。

       Windows支持6個優先級類:即空閒(屏保),低於正常,正常,高於正常,高(Task Manager)和實時(極端小心,該優先級甚至會搶佔操作系統組件的運行)。當然,正常優先級是最常用的,99%的應用程序都使用這個優先級。只有當絕對必要的時候,纔可以使用高優先級。你會驚奇的發現,Windows Explorer是在高優先級上運行的。大多數時間Explorer的線程是暫停的,等待用戶按下操作鍵或者點擊鼠標按鈕時被喚醒。當Explorer處於暫停狀態時,系統不將它的線程分配CPU。這將使低優先級的線程得以運行。但是如下用戶按下Ctrl+Esc組合鍵,即使有低優先級線程在運行,系統也會讓Explorer搶在那些線程前面運行。另外,如果沒有足夠的理由,千萬不要使用實時優先級。它會干擾操作系統任務的運行。

        一旦選定了優先級類後,就不必考慮你的應用程序與其他應用程序之間的關係,只需要集中考慮你各個線程的優先級。windows支持7個相對的線程優先級,即空閒、最低、低於正常、正常、高於正常、最高和關鍵時間優先級。這些優先級是相對於進程優先級的。以行作爲進程優先級以列作爲相對線程優先級就可以做出一張優先級矩陣表,比如進程優先級高,線程優先級正常對應的優先級就是13。(這個表可以在覈心編程page155中找到,優先級從1到31不等,有一些優先級是用戶方式的應用程序無法使用的,比如17、18等,但是如果編寫一個以內核方式運行的設備驅動程序,可以使用這些優先級),另外需要注意的是,實時優先級類的進程中的線程不能低於優先級16,而非實時優先級進程中的線程優先級不能高於15。

        一般來說,大多數時候高優先級的線程不應該處於可調度狀態。相反,低優先級線程可以保持可調度狀態。如果按照這個原則來辦,這個操作系統就能正確的對用戶響應。

        那麼,進程是如果賦予不同的優先級的呢?當調用CreateProcess的時候,一共有6種標誌符對應了6中進程的優先級。但是比較奇怪的是,創建子進程的進程負責子進程運行的優先級類。讓我們用Explorer爲例來說明這個問題,當使用Explorer來運行一個應用程序時,新進程按正常優先級運行。而Explorer不知道進程在做什麼,也不知道隔多久進程的線程需要調度。但是一旦子進程運行,它就可以調用SetPriorityClass來改變它自己的優先級類。這個函數有一個參數用於設定進程的句柄,也就是說,只要擁有該進程的句柄和足夠的訪問權,就能夠改變系統中運行的任何進程的優先級類,如果要改變自己的優先級類,只需傳遞GetCurrentProcess()即可。同樣,也有GetPriorityClass函數可以獲得進程的優先級類。

        當使用Explorer啓動一個進程時,該程序的起始優先級是正常優先級類。但是如果使用Start命令來啓動該程序,可以用一個設定開關來選擇應用程序的優先級類。比如:C:/> START /LOW CALC.EXE,另外還有其他開關:/BELOWNORMAL、/NORMAL、/ABOVENORMAL、/HIGH、/REALTIME等。當然,一旦應該程序啓動運行,它就可以調用SetPriorityClass設定自己的優先級。我們還可以使用Task Manager設定已啓動進程的優先級,選中進程,右擊鼠標即可看到。

        當一個線程剛剛創建的時候,它的相對優先級總是設定爲正常優先級。CreateThread沒有爲我們提供一個參數來設定線程的優先級。但是我們可以調用SetThreadPriority來設定線程的優先級。若要創建一個帶有相對優先級爲空閒的線程,可以按照以下順序調用:CreateThread(...CREATE_SUSPENDED)->SetPrioirtyClass->ResumeThread。

        有時候,系統常常要提高線程的優先級等級,以便對窗口消息或讀取磁盤等I/O事件作出響應。例如,在高優先級類進程中的一個正常優先級等級的線程的基本優先級等級是13。如果用戶按下一個操作鍵,系統就會將一個WM_KEYDOWN消息放入線程的隊列中。由於一個消息已經出現在線程的隊列中,因此該線程就是可調度線程。此外,鍵盤驅動程序也能告訴系統暫時提高線程的優先級等級。該線程的優先級等級可能提高兩級,其當前的優先級等級爲15。系統在優先級爲15時爲一個時間片對線程進程調度。一旦時間片結束系統,系統將線程的優先級遞減1,使下一個時間的優先級等級爲14。該線程的第三個時間片按優先級等級13來執行。如果線程要求執行更多的時間片,均按優先級等級13來執行。注意,線程的當前優先級等級絕不會低於線程的基本優先級等級。另外,系統只能爲基本優先級等級在1至15的之間的線程提高其優先級等級,這個範圍稱爲動態優先級範圍。此外,系統不會將線程的優先級等級提高到實時範圍(高於15),線程也不會動態提高實時範圍內的線程優先級等級。系統提高了兩個函數,可以讓我們自己選擇是否需要這種動態提高線程優先級等級的行爲,它們是SetProcessPriorityBoost和SetThreadPriorityBoost,分別對進程中的所有線程和某個線程進行控制。當然,也有Get形式的版本。另一種情況也會導致系統動態地提高線程的優先級等級,當一個低優先級等級的線程準備運行,但是卻有一個高優先級線程連續被調度時,系統會將該低優先級等級的線程動態提高到15,並讓該線程運行兩倍於它的時間片。當到了兩倍時間片的時候,該線程的優先級等級返回到它的基本優先級。

        另外,系統還會自動提高前臺程序的優先級等級,使他們能對用戶的輸入作出更快的反應。當前臺進程返回後臺運行時,該線程的優先級等級又恢復到基本優先級。

 親緣性

         軟親緣性:按照默認設置,當系統將線程分配給處理器時,Windows 2000使用軟親緣性來進行操作。這意味着如果所有其他因素相同的話,它將設法在它上次運行的那個處理器上運行線程。讓線程留在單個處理器上,有助於重複使用仍然在處理器的內存高速緩存中的數據。

        硬親緣性:Windows 2000允許設置進程和線程的親緣性。換句話說,可以控制哪個CPU能夠運行某些線程。這稱爲硬親緣性。

        通過調用GetSystemInfo,我們可以得到計算機擁有的CPU數量。爲了限制在可用CPU的子集上運行的單個進程中的線程數量,可以調用SetProcessAffinityMask(HANDLE hProcess, DWROD_PRT dwProcessAffinityMask),第一個參數指明要影響的進程。第二個參數是個屏蔽位,用於指明線程可以在哪些CPU上運行。例如,0x00000005表示可以在CPU0和CPU2上運行。子進程可以繼承父進程的親緣性,此外,可以使用作業對象將一組進程限制在要求的一組CPU上運行。當然,也有對應的GetProcessAffinityMask函數。

        有時,我們可能想要將進程中的一個線程限制在一組CPU上運行。同樣的,我們還有一個函數:GetThreadAffinityMask,參數跟上面那個相同,第一個用於指定線程句柄,第二個屏蔽位必須是進程的親緣性屏蔽的子集。返回值是線程的前一個親緣性屏蔽。若要爲線程設置一個理想CPU,可以調用SetThreadIdleProcessor(HANDLE hThread, DWORD dwIdealProcessor)函數,dwIdealProcessor不是屏蔽位,它是個從0到31的整數,用於指明供線程使用的首選CPU。

        也可以在一個可執行文件的頭上設置處理器親緣性。設置代碼在覈心編程page170中可以找到。另外,可以使用稱爲ImageCfg.exe的實用程序,以便改變可執行程序模塊頭上的某些標誌。這個不詳細介紹了。

發佈了25 篇原創文章 · 獲贊 0 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章