再談Windows NT/2000環境切換
WebCrazy(tsu00@263.net)
線程是Windows NT/2000環境切換的最基本單位。在<<淺析Windows NT/2000環境切換>>(Nsfocus Magazine 12)一文中,我只對進程CR3切換進行了較詳細的討論,但未涉及線程調度的內容,本文將盡量講述這些部分內容。在這之前,還是先看看以下的代碼:
//-----------------------------------------------
//
// EnumThreads-information from KPEB and KTEB
// Only test on Windows 2000 Server Chinese Edition
// Build 2195(Free)!Programmed By WebCrazy
// (tsu00@263.net) on 5-23-2000!
//
//-----------------------------------------------
#define KTEBListOffsetKPEB 0x50
#define PIDOffset 0x9c
#define KPEBListOffset 0xa0
#define ProcessNameOffset 0x1fc
#define StackTopOffset 0x18
#define StackBtmOffset 0x1c
#define UserTEBOffset 0x20
#define StackPtrOffset 0x28
#define KTEBListOffset 0x1a4
#define KTEBPIDOffset 0x1e0
#define TIDOffset 0x1e4
void DisplayThreadFromKPEB(void *kpeb)
{
char ProcessName[16];
ULONG PID;
ULONG TID;
ULONG StackBtm,StackTop,StackPtr,UserTEB;
PLIST_ENTRY KTEBListHead, KTEBListPtr;
KTEBListHead=KTEBListPtr=(PLIST_ENTRY)((int)kpeb+KTEBListOffsetKPEB);
do
{
void *kteb;
kteb=(void *)((*(ULONG *)KTEBListPtr)-KTEBListOffset);
TID=*(ULONG *)(((char *)kteb)+TIDOffset);
StackBtm=*(ULONG *)(((char *)kteb)+StackBtmOffset);
StackTop=*(ULONG *)(((char *)kteb)+StackTopOffset);
StackPtr=*(ULONG *)(((char *)kteb)+StackPtrOffset);
UserTEB=*(ULONG *)(((char *)kteb)+UserTEBOffset);
memset(ProcessName, 0, sizeof(ProcessName));
memcpy(ProcessName, ((char *)kpeb)+ProcessNameOffset, 16);
PID=*(ULONG *)(((char *)kpeb)+PIDOffset);
// or PID=*(ULONG *)(((char *)kteb)+KTEBPIDOffset);
DbgPrint(" %04X %08X %08X %08X %08X %08X %s(%X)/n",
TID,kteb,StackBtm,StackTop,StackPtr,UserTEB,ProcessName,PID);
KTEBListPtr=KTEBListPtr->Flink;
}while (KTEBListPtr->Flink!=KTEBListHead);
}
void EnumThreads()
{
PLIST_ENTRY KPEBListHead, KPEBListPtr;
if(((USHORT)NtBuildNumber)!=2195){
DbgPrint("Only test on Windows 2000 Server Build 2195!/n");
return;
}
DbgPrint("/n TID KTEB Addr StackBtm StackTop StackPtr User TEB Process
Name (PID)");
DbgPrint("/n ---- -------- -------- -------- -------- -------- -------
----- -----/n");
KPEBListHead=KPEBListPtr=(PLIST_ENTRY)(((char *)PsInitialSystemProcess)+KPEBListOffset);
while (KPEBListPtr->Flink!=KPEBListHead) {
void *kpeb;
kpeb=(void *)(((char *)KPEBListPtr)-KPEBListOffset);
DisplayThreadFromKPEB(kpeb);
DbgPrint("/n");
KPEBListPtr=KPEBListPtr->Flink;
}
}
這段代碼列出的EnumThreads函數在Windows 2000 Server Build 2195中的輸出結果如下:
TID KTEB Addr StackBtm StackTop StackPtr User TEB Process Name(PID)
---- -------- -------- -------- -------- -------- -----------------
0004 FE4E19E0 F9019000 F901C000 F901B9C4 00000000 System(8)
000C FE4E0C80 F9021000 F9024000 F9023D34 00000000 System(8)
0010 FE4E0A00 F9025000 F9028000 F9027D34 00000000 System(8)
0014 FE4E0780 F9029000 F902C000 F902BD34 00000000 System(8)
0018 FE4E0500 F902D000 F9030000 F902FD34 00000000 System(8)
001C FE4E0280 F9031000 F9034000 F9033D34 00000000 System(8)
.
.(略)
.
從運行結果可知EnumThreads主要是實現將系統當前所有的線程列出,上面的輸出格式與SoftICE的thread命令一致。代碼中使用了一些Undocumented的KPEB/KTEB資料項目:
1.進程的線程鏈表
這是一個LIST_ENTRY結構的項(佔用兩個32位的指標即8位元組),位於KPEB後的50h處,上面代碼由KTEBListOffsetKPEB表示。
2.線程鏈表相對KTEB的位移
位於KTEB後1a4h處(KTEBListOffset定義)。
輸出結果中的如StackBtm、StackTop、StackPtr等請參閱<<SOFTICE COMMAND REFERENCE>>,它們在KTEB中的位置請直接看代碼前的定義。
在理解了EnumThreads程式段與我上次給出的實現EnumProcesses的底層代碼後,也差不多明白了Windows NT/2000是如何組織、管理進程與線程了,這對理解線程調度可是至關重要的。雖然如此但要討論區對話調度,還是再看看幾個重要的資料(我不再具體說明如何取得這些資料具體位置的方法了,如果您很想知道還是建議您再看看<<淺析Windows NT/2000環境切換>>):
1. 進程狀態(Status) //KPEB+65h UCHAR
典型的進程狀態有:Running、Ready與Idle。
應該說明的是在單一處理器的機子中處於Running狀態的進程只有一個,且其不受KPEB中的這個值約束,系統通過調用IoGetCurrentProcess核心常式獲得。我在<<再談Windows NT/2000內部資料結構>>對IoGetCurrentProcess進行了比較詳細的介紹。
當KPEB中Status值為0時,進程狀態為Ready;為1時進程狀態為Idle;為2時進程狀態為Transition等等。
正像前面所提及的線程是Windows NT/2000環境切換的基本單位,實際上系統並不執行進程,進程狀態和以下將要提及的進程優先順序是很抽象的概念,只是系統調用時對線程的範圍限制,Microsoft提出這些概念我想主要是隱藏系統內部Thread調度行為。但在有線程狀態的前提下其也不是說就隨便附個值即可。曾有次我將System進程的狀態從Ready改為Transition後,只能眼巴巴的看著螢幕上的程式碼,不能存檔。因為此時系統已經變得懶洋洋的,不再響應我的千呼萬喚了。
2. 線程狀態 //KTEB+2dh UCHAR
在KTEB中有一成員State主要是指出當前線程狀態,其位於KTEB+2d處(單位元組)。它主要有如下幾個值(值取自SoftICE的輸出結果):
0 - Initialized (表示State的值為0時,表示線程狀態為Initialized,以下類同)
1 - Ready
2 - Running
3 - StandBy
4 - Terminated
5 - Waiting
6 - Transition
在David Solomon與Mark Russinovich的<<INSIDE MICROSOFT WINDOWS 2000,THIRD EDITION>>中是如此描述的:
To quote:
--------
The thread states are as follows:
Ready
When looking for a thread to execute, the dispatcher considers only the pool of threads in the
ready state. These threads are simply waiting to execute.
Standby
A thread in the standby state has been selected to run next on a particular processor. When
the correct conditions exist,the dispatcher performs a context switch to this thread. Only one
thread can be in the standby state for each processor on the system.
Running
Once the dispatcher performs a context switch to a thread, the thread enters the running
state and executes. The thread's execution continues until the kernel preempts it to run a higher priority thread, its quantum ends, it terminates, or it voluntarily enters the wait state.
Waiting
A thread can enter the wait state in several ways: a thread can voluntarily wait on an object to synchronize its execution, the operating system (the I/O system, for example) can wait on the thread's behalf, or an environment subsystem can direct the thread to suspend itself. When the thread's wait ends, depending on the priority, the thread either begins running immediately or is
moved back to the ready state.
Transition
A thread enters the transition state if it is ready for execution but its kernel stack is
paged out of memory. For example, the thread's kernel stack might be paged out of memory. Once
its kernel stack is brought back into memory, the thread enters the ready state.
Terminated
When a thread finishes executing, it enters the terminated state. Once terminated, a thread
object might or might not be deleted. (The object manager sets policy regarding when to delete
the object.) If the executive has a pointer to the thread object, it can reinitialize the thread
object and use it again.
Initialized
Used internally while a thread is being created.
--------
我以下主要對waiting的狀態進行分析:
在Windows NT/2000中線程能調用KeWaitForSingleObject、KeWaitForMultipleObjects等自動放棄自己的執行時間總量(Quantum)。系統當前執行的線程由系統中的Processor Control Block(PRCB,注意與 Processor Control Region區別)中的CurrentThread成員指定。還記得我介紹過的如何得到當前線程嗎(取FS:124H中的DWORD值)?其實就是指向這個CurrentThread成員了。PRCB的定義KPRCB在ntddk.h中。系統通過如下函數獲得KPRCB指標:
_KeGetCurrentPrcb
0008:80465310 MOV EAX,[FFDFF020] 取KPCR(Processor Control Region)成員Prcb
0008:80465315 RET
系統當前線程狀態為Running。其它線程狀態由幾個(通常最大為THREAD_WAIT_OBJECTS+1個,否則就會出現BSOD,但Microsoft還定義了個MAXIMUM_WAIT_OBJECTS,這就要看您傳遞給系統的參數了)KWAIT_BLOCK結構表示,這些值以及以下將要談到的表示線程等待理由的KWAIT_REASON也均可從ntddk.h中找到。線程KWAIT_BLOCK結構資料處於KTEB+6ch處。上次我提到的發生context switch的兩種情況,要麼可以用event,semphore等同步對象,要麼可以用timer核心對象表示,這樣可以形成線程等待對列,來表示線程目前狀態。
由於KWAIT_BLOCK、KWAIT_REASON、還有event、timer等在Windows NT/2000中是少有的幾個Documented成員,您在知道KWAIT_BLOCK的具體位置後,大可以自己讀出線程等待隊列。不過SoftICE已經為你呈現了所有這些內部結構了。
從上分析對於線程狀態,牽涉到比較多的內容,我將一部分分析抄錄如下:
:u _KeReadStateThread
_KeReadStateThread
0008:8042F029 MOV EAX,[ESP+04]
0008:8042F02D MOV AL,[EAX+04]
0008:8042F030 RET 0004
:bpx _KeReadStateThread if (tid==_tid)
:bl //這命令後退出調試器
00) BPX _KeReadStateThread IF (TID==0x3BC)
Break due to BPX _KeReadStateThread IF (TID==0x3BC) (ET=22.28 seconds)
//分析一下_KeReadStateThread的第一個參數(也是唯一的參數)
:what dword(@(esp+04)) //您應該理解每個線程,每個時刻線程狀態由哪個核心對象確定都是不固定的吧
The value FF6811E0 is (a) Kernel Timer object (handle=0230) for explorer(398)
| |
|_Timer核心對象 |_這個對象在explorer進程中的控制代碼
:timer dword(@(esp+04))
Timer Object at FF6811E0
Dispatcher Type: 08
Dispatcher Size: 000A
Signal State: Signaled
.
.(略)
.
//SoftICE中的timer命令只是讀出timer對象資料
//所以你可以直接讀DISPATCHER_HEADER(Common dispatcher object header)中的SignalState成員(見ntddk.h)
//即下面這個命令
#byte(@(@(esp+04)+4))
00000001 0000000001 "" //1代表Signaled,試過將其從1改為0嗎?(從Signaled改為Not Signaled)
好了以上分析的這個線程目前狀態取決於timer對象(Object Pointer:0xFF6811E0)的狀態(Jeffrey Richter說Signaled表示When time comes due)。我已經是從最簡單的方面來分析了,很多線程目前狀態往往不僅僅取決於一個對象,SoftICE中Thread Wait List也即是這個概念。
談了這麼多讓線程等待的對象,現在來說說KWAIT_REASON,在KTEB中有專門表示thread wait reason的一個成員,它位於KTEB位移57h處,佔用一個CHAR的空間,嚴格的說這才是真正表示致使線程處於wait狀態的原因,上面的那麼多的討論只不過是解釋什麼核心對象造成這一wait reason的。DDK Documentation是這樣定義wait reason的
WaitReason
Specifies the reason for the wait. A driver should set this value to Executive, unless it is doing work on behalf of a user and is running in the context of a user thread, in which case it should set this value to UserRequest.
其中所提及的是否user thread是由KTEB另一個位於55h位移處的單字元成員,它由0代表核心模式,1代表使用者模式。上面提到了wait reason在驅動程式編程中最常見(並不是系統核心態代碼中最多見的)的兩個值:Executive與UserRequest,至於其其它值請參閱ntddk.h。
3.進程優先順序(KPEB+62h)、線程基本優先順序(BasePriority,KTEB+68h)、線程動態優先順序(Dyn Priority,KTEB+33h)
這三個值各自佔用一個位元組。其中Thread Dyn Priority在Spy++中顯示為Current Priority,而在Microsoft的WinDbg與Windows 2000 Server Resource Kit中的一些小工具,如pstat.exe等中則直接用Priority表示,但在SoftICE中則顯示為Dyn Priority。由於直接用Priority又不容易表達這麼多的優先順序。鑒於我文中所有內容都基於SoftICE的分析,我在本文中均沿用SoftICE中的名稱。其實Microsoft在KTEB結構中還提供PriorityDecrement等其它使系統隨時動態更改當前優先順序,這也是我比較喜歡使用Dyn Priority的一個原因之一。至於這些優先順序的詳細討論請參閱參考資料中的<<WINDOWS NT DEVICE DRIVER DEVELOPMENT>>,其對核心態的這些值的作用進行了比較多的說明。
4.線程親緣性(Affinity)
由於我目前尚未有條件測試多處理機的情況,我也不好在這多說,有條件的朋友我很希望您能說說。
5.線程擁有的時間總量(Thread Quantum)
單位元組,位於KTEB+6b處。指出CPU可以讓線程調度的時間總量(Quantum)。在Processor Control Block中系統存有三個_KTHREAD(KTEB)結構的成員CurrentThread、NextThread與IdleThread,分別代表系統當前處理器正在執行的線程、將要被調用的線程與系統空閑(Idle)線程,Idle線程通常只是簡單的調用KiIdleLoop,直到系統新的中斷來臨,以對其它線程進行調用。Windows NT/2000中調用KiQuantumEnd判斷當前線程是否使用完自己的時間總量,如果當前線程已執行完Quantum,則在KPRCB中NextThread非空時返回NextThread,作為系統調用的下一個線程。系統通過調用KiFindReadyThread尋找下一個處於Ready狀態的線程。
_KiQuantumEnd
0008:804315B9 PUSH EBP
0008:804315BA MOV EBP,ESP
0008:804315BC PUSH ECX
0008:804315BD PUSH EBX
0008:804315BE PUSH ESI
0008:804315BF PUSH EDI
0008:804315C0 MOV EAX,DS:[FFDFF020] ;KPRCB->EAX
0008:804315C6 MOV EDI,EAX ;KPRCB->EDI
0008:804315C8 MOV EAX,FS:[00000124] ;Current Thread's KTEB->EAX
0008:804315CE MOV ESI,EAX ;Current Thread's KTEB->ESI
0008:804315D0 CALL [__imp__KeRaiseIrqlToDpcLevel] ;將IRQL提升到DISPATCH_LEVEL,學過
;ddk的朋友應該都比較熟悉
0008:804315D6 XOR EBX,EBX
0008:804315D8 MOV [EBP-01],AL ;Save Old IRQL
0008:804315DB CMP [ESI+6B],BL ;判斷當前線程的Quantum
0008:804315DE JG 804315F2 ;在Quantum小於等於0時擷取NextThread
0008:804315E0 MOV EAX,[ESI+44]
0008:804315E3 CMP [EAX+69],BL
0008:804315E6 JZ 80431608
0008:804315E8 CMP BYTE PTR [ESI+33],10
0008:804315EC JL 80431608
0008:804315EE MOV BYTE PTR [ESI+6B],7F
0008:804315F2 MOV ESI,[EDI+08] ;KPRCB's NextThread->ESI
0008:804315F5 CMP ESI,EBX ;KPRCB's NextThread是否為空白
0008:804315F7 JNZ 80431601
0008:804315F9 MOV CL,[EBP-01]
0008:804315FC CALL @KiUnlockDispatcherDatabase
0008:80431601 MOV EAX,ESI ;將NextThread返回
0008:80431603 POP EDI
0008:80431604 POP ESI
0008:80431605 POP EBX
0008:80431606 LEAVE
0008:80431607 RET
0008:80431608 MOVSX EDX,BYTE PTR [ESI+33]
.
.(代碼很長,牽涉到調度演算法,看來只能您自己去認真看看了)
.
6.線程所屬進程的KPEB(KTEB+22ch處)
主要是更容易的在KTEB與KPEB間進行些資料交換。
其實上面部分內部資料在Linux中也可以找到實現對應功能的體現,如Windows NT/2000中的Thread Quantum對應Linux task_struct中counter成員等等。我在<<淺析Windows NT/2000環境切換>>中就指出過Windows NT/2000實際上發生任務切換的情況只有兩種,我也在上次給出了時間中斷的部分代碼,給出了SwapContext(主要是CR3切換)代碼。Windows中的每一個進程都分別擁有私人的記憶體空間,私人的核心對象(控制代碼表Handle Table)等等,這些都是在環境切換的基礎上實現的,也是一個作業系統Robustness and Reliability的基礎。http://www.research.microsoft.com/中有很多文章對這有過驗證,大可以翻翻看看。關於這部分的實現您還可以再看看如下的一些常式(限於篇幅我不再列出代碼):
⊙ KiFindReadyThread
⊙ KiReadyThread
⊙ KiSwapThread
⊙ SwapContext
其中SwapContext與KiSwapThread是系統真正切換的代碼,是不是經常在stack trace中看到這個函數,在此你應該可以比較容易的明白了吧。(關於stack trace請參閱DDK Documentation中的Anatomy of a Stack Trace段或SoftICE Command Reference中的stack與thread命令的解釋)
另一種線程自動放棄執行的情況,跟蹤KeWaitForSingleObject、KeSetEvent等就相對比較容易了,拿Event Object舉個例子吧,由於知道Event的結構,在您的實驗用機上您大可以隨便更改DISPATCHER_HEADER中的SignalState成員更改Object狀態,看您要它是Clear還是Signalled了(上面我給出了如何用SoftICE實現)。甚至在您理解了我開頭給出的代碼(其實說白了只是讀出一雙向鏈表中的資料,如果是在Linux中,我相信你也根本沒有耐心看到這兒了),還有理解了KWAIT_BLOCK所定義Thread Wait List後,只要給您一核心調試器,我相信您想阻塞哪個線程就哪個線程了,加上理解了線程優先順序後,您想讓哪個線程多佔用CPU時間都可以。不過我可不保證您這時候機子是否還Robustness and Reliability了。
還是順便提一下,Windows NT/2000中調度代碼運行在DISPATCH_LEVEL IRQL上,已防止通常運行在PASSIVE_LEVEL的普通代碼對其的中斷。
Jeffrey Richter在<<PROGRAMMING APPLICATIONS for MICROSOFT WINDOWS,FOURTH EDITION>>中曾指出:
⊙ Microsoft doesn't fully document the behavior of the scheduler.
⊙ Microsoft doesn't let applications take full advantage of the scheduler's features.
⊙ Microsoft tells you that the scheduler's algorithm is subject to change so that you
can code defensively.
從這幾點看Microsoft並沒將調度行為固定,實際上Windows NT 4.0與Windows 2000在調度演算法上就有不同,而本文所提及的所有代碼均取自或只在Windows 2000 Server Build 2195中測試過。
我想對DDK有過學習的朋友應該都知道PsSetCreateProcessNotifyRoutine與PsSetCreateThreadNotifyRoutine這兩個讓使用者註冊系統建立與刪除進程或線程時調用回呼函數的Fully Documented常式,知道它們就是Mark Russinovich的NTPMON的實現的最主要的兩個函數吧。其實在Windows 2000中Microsoft還提供實作類別似功能的與環境切換有關的函數,即KeSetSwapContextNotifyRoutine和KeSetThreadSelectNotifyRoutine,不過這兩個函數卻是Undocumented的,並且只能在Windows 2000的特定版本上運行(請查閱MSDN Magazine)。
在Linux中所有的代碼都是公開的,但真正要理解調度代碼還是很困難的,何況本身就比Linux複雜(我尚未瀏覽過Linux 2.4.X源碼,我這樣說純系個人目前感覺)而且只能在一大堆彙編代碼中搜尋的Windows NT/2000,其分析的難度真的是可想而知的。所以我希望您能將文中錯誤或遺漏說明的地方告訴我(tsu00@263.net),謝謝!
參考資料:
1.Jeffrey Richter
<<PROGRAMMING APPLICATIONS for MICROSOFT WINDOWS,FOURTH EDITION>>
2.Peter Viscarola and Anthony Mason <<WINDOWS NT DEVICE DRIVER DEVELOPMENT>>
3.Numega <<SOFTICE COMMAND REFERENCE>>
4.Windows 2000 DDK Documentation
5.David Solomon and Mark Russinovich <<INSIDE MICROSOFT WINDOWS 2000,THIRD EDITION>>