Windows XP 多核同步(二)
ʕ •ᴥ•ʔ ɔ:
1 信号量SEMAPHORE
信号量(Semaphore)与事件(Event)的不同在于:信号量可以让多个线程同时进入自己的临界区执行代码,而事件只允许一个线程同一时刻进入自己的临界区执行代码。
信号量结构如下:
1 | kd> dt _KSEMAPHORE |
3环常用操作信号量相关函数:
- CreateSemaphore(创建一个信号量)
- OpenSemaphore(为现有的一个已命名信号量对象创建一个新句柄)
- ReleaseSemaphore(递增信号量的当前资源计数)
1.1 3环实例
搞个3环例子看看。
1 |
|

1.2 CreateSemaphore
CreateSemaphore函数:创建或打开命名或未命名信号量对象。
1 | HANDLE WINAPI CreateSemaphore( |
上述参数对应内核结构体相关成员:
1 | kd> dt _KSEMAPHORE |
CreateSemaphore函数调用过程如下:
1 | kernel32.dll |
1.3 NtCreateSemaphore
源码如下(base\ntos\ex\semphore.c):
1 | NTSTATUS __stdcall NtCreateSemaphore ( |
逆向分析代码如下:
1 | PAGE:0053D702 ; =============== S U B R O U T I N E ======================================= |
1.4 KeInitializeSemaphore
1 | .text:00425ECE ; =============== S U B R O U T I N E ======================================= |
1.5 ReleaseSemaphore
ReleaseSemaphore函数将指定信号量对象的计数增加指定数量。
设置$NewSignalState = OldSignalState + N(参数)$
1 | BOOL WINAPI ReleaseSemaphore( |
ReleaseSemaphore函数调用过程如下:
1 | kernel32.dll |
关于KeReleaseSemaphore的参数Wait:指定对KeReleaseSemaphore的调用后是否应立即调用KeWaitXxx例程之一。如果为真,KeReleaseSemaphore调用后必须调用KeWaitForMultipleObjects、KeWaitForMutexObject或KeWaitForSingleObject。
KeReleaseSemaphore例程可能会暂时提高IRQL。如果Wait参数为FALSE,则例程在返回之前,将IRQL恢复到调用开始时的原始值。
如果Wait = TRUE,则例程返回而不降低IRQL。在这种情况下,KeReleaseSemaphore调用后必须立即进行KeWaitXxx调用。通过设置Wait = TRUE,调用者可以防止KeReleaseSemaphore调用和KeWaitXxx调用之间发生不必要的上下文切换。KeWaitXxx例程在返回之前,在KeReleaseSemaphore调用开始时将IRQL恢复到原始值。虽然IRQL禁用了两个调用之间的上下文切换,但这些调用不能可靠地用作原子操作的开始和结束。例如,在这两个调用之间,在另一个处理器上同时运行的线程可能会更改信号量对象或等待目标的状态。
1.6 KeReleaseSemaphore
1 | LONG __stdcall KeReleaseSemaphore( |
函数作用:
- 设置SignalState = SignalState + N(参数)
- 通过WaitListHead找到所有线程,并从等待链表中摘掉。
设置$NewSignalState = OldSignalState + N(参数)$
源代码如下(base\ntos\ke\semphobj.c):
1 | LONG __stdcall KeReleaseSemaphore ( |
逆向分析代码如下:
1 | .text:00425EFA ============== S U B R O U T I N E ======================================= |
2 互斥体MUTANT
互斥体也是内核对象,结构如下:
1 | kd> dt _KMUTANT |
- MutantListEntry:类似于
_DISPATCHER_HEADER.WaitListHead与_KWAIT_BLOCK.WaitListEntry一样。链表的头是_KTHREAD.MutantListHead,_KMUTANT + 0x10 == _KTHREAD.MutantListHead.Flink。
1 | HANDLE CreateMutexA( |
2.1 MutantListEntry
互斥体也是内核对象,结构如下:
1 | kd> dt _KMUTANT |
MutantListEntry:类似于
_DISPATCHER_HEADER.WaitListHead与_KWAIT_BLOCK.WaitListEntry一样。链表的头是
_KTHREAD.MutantListHead,_KMUTANT + 0x10 == _KTHREAD.MutantListHead.Flink。
为了验证:KMUTANT.MutantListEntry圈着所有当前线程创建的 互斥体对象+0x10,有如下代码:
1 |
|
如上代码,线程ThreadProc_1创建了两个互斥体,且bInitialOwner == TRUE,说明两个互斥体都属于当前线程。下面就通过对比线程ThreadProc_1和ThreadProc_2,说明KMUTANT.MutantListEntry圈着所有当前线程创建的 互斥体对象+0x10。
找到对应的线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS 860a0788 SessionId: 0 Cid: 0d64 Peb: 7ffde000 ParentCid: 0a10
DirBase: 06e403e0 ObjectTable: e281f858 HandleCount: 18.
Image: 220518_Mutant.exe
kd> !process 0x860a0788
PROCESS 860a0788 SessionId: 0 Cid: 0d64 Peb: 7ffde000 ParentCid: 0a10
DirBase: 06e403e0 ObjectTable: e281f858 HandleCount: 18.
Image: 220518_Mutant.exe
//主线程
THREAD 863a85b0 Cid 0d64.0d68 Teb: 7ffdd000 Win32Thread: 00000000 WAIT: (WrLpcReply) UserMode Non-Alertable
//ThreadProc_1
THREAD 860e0790 Cid 0d64.0d6c Teb: 7ffdc000 Win32Thread: 00000000 RUNNING on processor 0
//ThreadProc_2
THREAD 8617e790 Cid 0d64.0d70 Teb: 7ffdb000 Win32Thread: 00000000 WAIT: (UserRequest) UserMode Non-Alertable
//ThreadProc_3
THREAD 8639b790 Cid 0d64.0d74 Teb: 7ffda000 Win32Thread: 00000000 WAIT: (UserRequest) UserMode Non-Alertable
...分析线程
ThreadProc_1也是互斥体的拥有者。1
2
3
4
5kd> dt _KTHREAD 0x860e0790
ntdll!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x010 MutantListHead : _LIST_ENTRY [ 0x862f6ef0 - 0x86271e40 ]
...这里需要注意的是,
MutantListHead在_KTHREAD + 0x10的位置,MutantListEntry也在_KMUTANT + 0x10的位置。这里是链表头。
开始分析。
第一轮循环:
1
2
3
4
5
6
7
8
9
10
11PLIST_ENTRY ListHead;
PLIST_ENTRY ListEntry;
PVOID Object;
ListHead = &_KTHREAD->MutantListHead; //ListHead == 0x860e0790 + 0x10 == 0x860e07a0
ListEntry = ListHead->Flink; //ListEntry == 0x862f6ef0
while(ListHead != ListEntry) //0x860e07a0 != 0x862f6ef0
{
Object = ListEntry - 0x10; //Object == 0x862f6ef0 - 0x10 = 0x862f6ee0
ListEntry = ListEntry->Flink;
}然后看一下
Object == 0x862f6ee0是什么?1
2
3
4
5
6
7
8kd> dt _DISPATCHER_HEADER 0x862f6ee0
ntdll!_DISPATCHER_HEADER
+0x000 Type : 0x2 '' //互斥体 MutantObject
+0x001 Absolute : 0 ''
+0x002 Size : 0x8 ''
+0x003 Inserted : 0 ''
+0x004 SignalState : 0n0 //无信号
+0x008 WaitListHead : _LIST_ENTRY [ 0x8617e800 - 0x8639b800 ]第二轮循环:
继续往下分析
ListEntry->Flink:1
2
3
4
5
6
7
8
9
10
11ListEntry == 0x862f6ef0 //ListHead == 0x860e07a0
kd> dd 0x862f6ef0
862f6ef0 86271e40 860e07a0 860e0790 00000000
ListEntry = ListEntry->Flink == 0x86271e40;
while(ListHead != ListEntry) //0x860e07a0 != 0x86271e40
{
Object = ListEntry - 0x10; //Object == 0x86271e40 - 0x10 = 0x86271e30
ListEntry = ListEntry->Flink;
}然后看一下
Object == 0x86271e30:1
2
3
4
5
6
7
8kd> dt _DISPATCHER_HEADER 0x86271e30
ntdll!_DISPATCHER_HEADER
+0x000 Type : 0x2 '' //互斥体 MutantObject
+0x001 Absolute : 0 ''
+0x002 Size : 0x8 ''
+0x003 Inserted : 0 ''
+0x004 SignalState : 0n0 //无信号
+0x008 WaitListHead : _LIST_ENTRY [ 0x86271e38 - 0x86271e38 ]第三轮循环:
继续往下分析
ListEntry->Flink:1
2
3
4
5
6
7
8
9
10
11ListEntry == 0x86271e40 //ListHead == 0x860e07a0
kd> dd 0x86271e40
86271e40 860e07a0 862f6ef0 860e0790 00000000
WaitEntry = ListEntry->Flink == 0x860e07a0;
while(ListHead != ListEntry) //0x860e07a0 == 0x860e07a0
{
Object = ListEntry - 0x10; //Object == 0x860e07a0 - 0x10 = 0x860e0790
ListEntry = ListEntry->Flink;
}然后看一下
Object == 0x860e0790:1
2
3
4
5
6
7
8kd> dt _DISPATCHER_HEADER 0x860e0790
ntdll!_DISPATCHER_HEADER
+0x000 Type : 0x6 '' //线程 ThreadObject
+0x001 Absolute : 0 ''
+0x002 Size : 0x70 'p'
+0x003 Inserted : 0 ''
+0x004 SignalState : 0n0
+0x008 WaitListHead : _LIST_ENTRY [ 0x860e0798 - 0x860e0798 ]此轮循环不会执行,因为
ListHead == ListEntry == 0x860e07a0。分析线程
ThreadProc_2。1
2
3
4
5kd> dt _KTHREAD 0x8617e790
ntdll!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x010 MutantListHead : _LIST_ENTRY [ 0x8617e7a0 - 0x8617e7a0 ]
...可以看到,
ListHead == 0x8617e790 + 0x10 == 0x8617e7a0 == ListHead->Flink,说明ThreadProc_2线程没有创建属于自己的互斥体,尽管自己在等待两个内核对象。
完整过程如下:
1 | ThreadProc_1 |
2.2 互斥体和其他内核对象差异
互斥体(MUTANT)与事件(EVENT)和信号量(SEMAPHORE)的相似与区别:
- 相同点:都是内核对象,都可以用来进行线程的同步控制。
- 区别:可以用来解决事件与信号量做不到的事,如防止线程突然终止造成的死锁、线程临界区重入(多次进入)等。
一、互斥体解决线程突然终止造成死锁的问题
有如下伪代码:
1 | A进程的 X 线程 |
- 事件/信号量:如果 A 进程的 X 线程还没有来得及调用修改SignalState的函数(如SetEvent)线程就突然终止,那么被等待对象ObjectM将被遗弃,这也就意味着 Y 线程将永远等下去直到进程 A 结束系统对其资源释放!
- 互斥体:由于互斥体有成员KMUTANT.Abandoned,当线程意外终止时,系统会修改SignalState。
二、互斥体解决线程重复进入临界区的问题
有如下代码:
1 | WaitForSingleObject(A) |
- 事件/信号量:当线程执行到
WaitForMultipleObjects时对象A一直没有得到信号,则会一直等待,就造成了死锁。 - 互斥体:当线程执行到
WaitForMultipleObjects时对象A不管信号<=0还是>0,都会继续执行,主要是利用KMUTANT.OwnerThread成员。
2.3 实验:互斥体的不同
R3函数CreateMutexA创建或打开一个命名或未命名的互斥对象。
1 | HANDLE CreateMutexA( |
先举例说明一下:
1 |
|
情况一:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19ThreadProc_1
{
hMutant = CreateMutex(NULL,TRUE,"TestMutant"); //TRUE:hMutant.SignalState = 0
while(1)
{
continue;
}
printf("ThreadProc_1 END !\n");
}
ThreadProc_2
{
WaitForSingleObject(hMutant,-1);
printf("ThreadProc_2 END !\n");
while(1)
{
continue;
}
}控制台输出:无输出。
情况二:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19ThreadProc_1
{
hMutant = CreateMutex(NULL,FALSE,"TestMutant"); //FALSE:hMutant.SignalState += 1
while(1)
{
continue;
}
printf("ThreadProc_1 END !\n");
}
ThreadProc_2
{
WaitForSingleObject(hMutant,-1);
printf("ThreadProc_2 END !\n");
while(1)
{
continue;
}
}控制台输出:
ThreadProc_2 END !
这个时候在Windbg里面查看
ThreadProc_2线程等待的互斥体对象hMutant.OwnerThread == ThreadProc_1。情况三:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15ThreadProc_1
{
hMutant = CreateMutex(NULL,TRUE,"TestMutant"); //FALSE:hMutant.SignalState = 0
printf("ThreadProc_1 END !\n");
}
ThreadProc_2
{
WaitForSingleObject(hMutant,-1);
printf("ThreadProc_2 END !\n");
while(1)
{
continue;
}
}控制台输出:
ThreadProc_1 END !ThreadProc_2 END !这里为什么会输出
ThreadProc_2 END !?就是因为线程ThreadProc_1终止了之后,由于互斥体有成员KMUTANT.Abandoned,当线程终止时,系统会修改SignalState。
情况四:为了对比情况三。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15ThreadProc_1
{
hEvent = CreateEvent(NULL,FALSE,FALSE,NULL); //参数三FALSE:hEventt.SignalState = 0
printf("ThreadProc_1 END !\n");
}
ThreadProc_2
{
WaitForSingleObject(hEvent,-1);
printf("ThreadProc_2 END !\n");
while(1)
{
continue;
}
}控制台输出:
ThreadProc_1 END !
这里没有输出
ThreadProc_2 END !就是因为线程ThreadProc_1终止了之后,系统并不会修改事件的SignalState,ThreadProc_2会一直等下去。情况五:为了对比情况一,也为了说明
KMUTANT.OwnerThread允许重入的意义。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21ThreadProc_1
{
hMutant = CreateMutex(NULL,TRUE,"TestMutant"); //TRUE:hMutant.SignalState = 0
WaitForSingleObject(hMutant,-1);
printf("ThreadProc_1 END !\n");
while(1)
{
continue;
}
}
ThreadProc_2
{
WaitForSingleObject(hMutant,-1);
printf("ThreadProc_2 END !\n");
while(1)
{
continue;
}
}控制台输出:
ThreadProc_1 END !
由于KMUTANT.OwnerThread的存在,即使
hMutant没有信号,但是KMUTANT.OwnerThread == ThreadProc_1会继续执行,也就是允许重入,后面会降到为什么,答案在函数KeWaitForSingleObject里面。
2.4 解决线程重入
互斥体可解决解决线程重入临界区的问题(重入的效果可参见2.3实验情况五。)
互斥体用OwnerThread成员来解决线程重入的问题。
1 | kd> dt _KMUTANT |
创建互斥体的3环函数A型为:
1 | HANDLE CreateMutexA( |
bInitialOwner == TRUE,OwnerThread指向创建者线程的_KTHREAD。bInitialOwner == FALSE,OwnerThread == NULL。
下面来分析一下函数KeWaitForSingleObject即可知道怎么解决重入问题:
1 | .text:00423F61 loc_423F61: |
可以发现:
1 | SignalState > 0, jg short loc_423F72 |
可以看到当互斥体有信号,和OwnerThread == CurrentThread处理逻辑一样,都是去让SignalState--。
可以重入的次数为0x80000000次。
2.5 解决线程终止造成死锁
解决线程终止造成死锁的问题
如2.3实验情况三,当创建互斥体的线程终止后,系统会将其创建的互斥体对象设置为有信号,直到进程结束。主要就是利用互斥体结构的Abandoned成员。
1 | kd> dt _KMUTANT |
释放互斥体信号的函数是ReleaseMutex,调用流程如下:
1 | BOOL WINAPI ReleaseMutex(HANDLE hMutex); |
当互斥体的创建线程终止后,系统会调用MmUnloadSystemImage --> KeReleaseMutant(X,Y,Abandon == TRUE,Z)函数然后来释放信号。
KeReleaseMutant函数过程大致如下:
1 | KeReleaseMutant(X,Y,Abandon,Z) |
当Abandon == TRUE时,直接将SignalState == 1而不是++,然后OwnerThread == NULL,很干脆。
2.6 ApcDisable
互斥体结构如下:
1 | kd> dt _KMUTANT |
互斥体可以在3环调用创建,也可以在0环调用创建,但是有不同:
- 3环调用创建:
ApcDisable == 0。 - 0环调用创建:
ApcDisable == 1。
在3环创建调用路径:
Mutant -> NtCreateMutant -> KeInitializeMutant(x, x),初始化时ApcDisable == 0,如下:1
2.text:00425845 mov [esi+_KMUTANT.Abandoned], 0
.text:00425849 mov [esi+_KMUTANT.ApcDisable], 0在0环创建调用:
Mutex ->.. KeInitializeMutex,初始化时ApcDisable == 1,如下:1
2
3.text:00425876 and [eax+_KMUTANT.OwnerThread], 0
.text:0042587A mov [eax+_KMUTANT.Abandoned], 0
.text:0042587E mov [eax+_KMUTANT.ApcDisable], 1
该成员KMUTANT.ApcDisable和线程的KTHREAD.KernelApcDisable(+0x0d4)配合使用。
KTHREAD.KernelApcDisable(+0x0d4)说明如下:
1 | //Win XP SP1中如下 |
- 源码:0 表示不禁止APC,负数表示禁止APC,正值表示驱动程序已启用特殊或普通 Apc 多次,
KernelApcDisable为0 的时候,该线程才允许插入或提交APC。 - WRK:
KernelApcDisable和SpecialApcDisable域,或者合并成一个CombinedApcDisable域。KernelApcDisable 和SpecialApcDisable 都是16 位的整数值,0 表示不禁止APC,负数表示禁止APC,正值表示驱动程序已启用特殊或普通 Apc 多次。一个线程在执行过程中可以有多种因素要禁止APC,这些因素以负值来表示,并累加起来,当因素消除的时候再减去相应的负值。只有当KernelApcDisable或SpecialApcDisable为0 的时候,该线程才允许插入或提交APC。这两个值分别控制普通的内核APC 和特殊的内核APC,关于这两种内核模式APC 请参考《Windows内核原理与实现》5.2.6 节。
逆向分析KeWaitForSingleObject函数有关代码如下:
1 | .text:00424121 loc_424121: |
KernelApcDisable != 0就禁用内核APC。
2.7 CreateMutexA
R3函数CreateMutexA创建或打开一个命名或未命名的互斥对象。
1 | HANDLE CreateMutexA( |
调用过程如下:
1 |
|
2.8 NtCreateMutant
源代码如下:
1 | NTSTATUS NtCreateMutant ( |
2.9 KeInitializeMutant
1 | .text:004257E8 ; =============== S U B R O U T I N E ======================================= |
2.a KeInitializeMutex
1 | .text:00425858 ; =============== S U B R O U T I N E ======================================= |
2.b ReleaseMutex
1 | BOOL __stdcall ReleaseMutex( [in] HANDLE hMutex ) |
2.c KeReleaseMutant
- 从逆向及实际测试效果来看,谁WaitForSingleObject到Mutant,就会修改Mutant->OwnerThread为自己(然后线程可以进行重入了)。
- 每次调用KeReleaseMutant当SignalState == 1,就修改Mutant->OwnerThread = NULL。
- 如果创建Mutant时bInitialOwner == TRUE,则谁创建了该互斥体,谁调用ReleaseMutex才会起作用,其他Mutant->OwnerThread != CurrentThread的线程调用了没用。引申到:只有Mutant->OwnerThread == CurrentThread的线程调用ReleaseMutex才能起作用(实际代码测试过的)。
详细说明如下(经过实际代码测试):
从逆向及实际测试效果来看,谁WaitForSingleObject到Mutant,就会修改Mutant->OwnerThread为自己(然后线程可以进行重入了)。有如下代码进行说明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30KeWaitForSingleObject
{
if (Objectx->Header.Type == MutantObject)
{
if ((Objectx->Header.SignalState > 0) || (Thread == Objectx->OwnerThread))
{
if (Objectx->Header.SignalState != MINLONG)
{
KiWaitSatisfyMutant(Objectx, Thread);
WaitStatus = (NTSTATUS)(Thread->WaitStatus);
goto NoWait;
}
}
}
}
(_Object_)->Header.SignalState -= 1;
if ((_Object_)->Header.SignalState == 0) {
(_Thread_)->KernelApcDisable -= (_Object_)->ApcDisable;
(_Object_)->OwnerThread = (_Thread_);
if ((_Object_)->Abandoned == TRUE) {
(_Object_)->Abandoned = FALSE;
(_Thread_)->WaitStatus = STATUS_ABANDONED;
}
InsertHeadList((_Thread_)->MutantListHead.Blink,
&(_Object_)->MutantListEntry);
}
}每次调用KeReleaseMutant当SignalState == 1,就修改Mutant->OwnerThread = NULL。有如下代码说明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21KeReleaseMutant
{
...
if (Mutant->Header.SignalState == 1) {
if (OldState <= 0) {
RemoveEntryList(&Mutant->MutantListEntry);
Thread->KernelApcDisable += Mutant->ApcDisable;
if ((Thread->KernelApcDisable == 0) &&
(IsListEmpty(&Thread->ApcState.ApcListHead[KernelMode]) == FALSE)) {
Thread->ApcState.KernelApcPending = TRUE;
KiRequestSoftwareInterrupt(APC_LEVEL);
}
}
Mutant->OwnerThread = (PKTHREAD)NULL;//重点
if (IsListEmpty(&Mutant->Header.WaitListHead) == FALSE) {
KiWaitTest(Mutant, Increment);
}
...
}如果创建Mutant时bInitialOwner == TRUE,则谁创建了该互斥体,谁调用ReleaseMutex才会起作用,其他Mutant->OwnerThread != CurrentThread的线程调用了没用。引申到:只有Mutant->OwnerThread == CurrentThread的线程调用ReleaseMutex才能起作用。
有如下代码可用来说明:
1
2
3
4
5
6
7
8
9
10
11
12KeReleaseMutant
{
...
//这里的ExRaiseStatus调用直接就退出函数KeReleaseMutant了
//为什么这么说呢。可以参考KeReleaseSemaphore函数
if (Mutant->OwnerThread != Thread) {
KiUnlockDispatcherDatabase(OldIrql);
ExRaiseStatus(Mutant->Abandoned ?
STATUS_ABANDONED : STATUS_MUTANT_NOT_OWNED);
}
...
}
源码欣赏:
1 | LONG KeReleaseMutant ( |
海哥说的调用结构如下:
1 | KeReleaseMutant(X,Y,Abandon,Z) |
3 队列QUEUE
队列对象,这是Windows内核中用于支持线程池的机制。
1 | kd> dt _KQUEUE |
- EntryListHead:是一个链表头,该链表包含了队列对象中待处理的项。
- MaximumCount:指定了队列对象中最多可以有多少个活动线程,以便控制通过该队列对象来处理任务的并发程度。
- CurrentCount:记录了当前有多少个活动线程。
- ThreadListHead:是一个链表头,该链表包含了当前已加入该队列对象的线程,每个节点都为线程KTHREAD对象的
QueueListEntry成员。
可参考:《Windows内核原理与实现5.4 P361》
3.1 线程池
线程池:池子里面有很多已经创建好的线程,用户需要的时候可以取出来用,用完之后又还回去,不需要销毁。
线程池是代表应用程序高效执行异步回调的工作线程的集合。线程池主要用于减少应用程序线程的数量,并提供工作线程的管理。应用程序可以将工作项排队,将工作与可等待的句柄相关联,根据计时器自动排队,并与I/O绑定。
要请求线程池中的线程处理工作项,调用QueueUserWorkItem函数。此函数将参数带到从线程池中选择的线程调用的函数。工作项目排队后无法取消。
线程池是在您首次调用QueueUserWorkItem或BindIoCompletionCallback时创建的,或者当计时器队列计时器或注册的等待操作将回调函数排队时创建。默认情况下,可以在线程池中创建的线程数量约为500个。每个线程使用默认堆栈大小,并以默认优先级运行。
3.2 QueueUserWorkItem
QueueUserWorkItem函数:Queues a work item to a worker thread in the thread pool.(将工作项排到线程池中的工作线程。)
1 | BOOL QueueUserWorkItem( |
一旦队列对象的线程池中正在处理队列项的线程进入等待状态或被终止,则队列对象的活动线程数量将减1,因而,只要活动线程数量小于最大允许值,并且队列项链表不空,而且有线程正在等待该队列对象,就可以唤醒一个等待线程。
队列对象可以看做是线程调度机制为支持线程池而做的一个扩展,所以,队列对象的主要逻辑都是在线程调度器中完成的。Windows使用队列对象来实现工作项目(WorkItem)的管理和分发,这里的工作项目是指:运行在System进程的系统线程中的一些过程,由于它们运行在PASSIVE_LEVEL上,所以它们可以像普通的例程一样等待分发器对象或者访问换页内存。DPC例程或者设备驱动程序可以利用工作项目来完成一些必须在PASSIVE_LEVEL上执行的工作。它们通过ExQueueWorkItem或IoQueueWorkItem把一个工作项目插入到系统中,由专门的系统线程来执行该工作项目中指定的一个过程。《Windows内核原理与实现5.4 P366》