Windows事件等待学习笔记(三)—— WaitForSingleObject函数分析
要点回顾
无论可等待对象是何种类型,线程都是通过
WaitForSingleObject
和WaitForMultipleObjects
进入等待状态的,这两个函数是理解线程等待与唤醒进制的核心
WaitForSingleObject
DWORD WaitForSingleObject(
HANDLE hHandle, // handle to object
DWORD dwMilliseconds // time-out interval);
对应的内核函数:NtWaitForSingleObject
NTSTATUS __stdcall NtWaitForSingleObject(
HANDLE Handle, //用户层传递的等待对象的句柄(具体细节参加句柄表专题)
BOOLEAN Alertable, //对应KTHREAD结构体的Alertable属性
//如果为1 在插入用户APC时,该线程将被吵醒
PLARGE_INTEGER Timeout //超时时间);
NtWaitForSingleObject
- 调用ObReferenceObjectByHandle函数,通过对象句柄找到等待对象结构体地址
- 调用KeWaitForSingleObject函数,进入关键循环
KeWaitForSingleObject:上半部分
- 准备等待块,当等待对象少于四个时,并不为等待对象分配新的空间,而是向 _KTHREAD(+70) 位置的等待块赋值,_KTHREAD(+5C) 指向第一个等待块的位置
注意:无论使用与否,_KTHREAD(+70)的第四个等待块被定时器占据,如果用的话,将会把定时器与第一个等待块相关联 - 如果超时时间不为0,KTHREAD(+70) 第四个等待块与第一个等待块关联起来:
第一个等待块指向第四个等待块,第四个等待块指向第一个等待块。 - KTHREAD(+5C) 指向第一个 _KWAIT_BLOCK。
- 进入关键循环
关键循环
- 判断当前被等待对象是否有信号
(每一个线程与等待对象是通过等待块进行关联的,但是对象有一个条件:至少有一个成员为 _DISPATCHER_HEADER 结构体)_DISPATCHER_HEADER +0x000 Type //类型 可通过IDA或WinDbg查看所需对象的类型 //IDA:分析二进制代码 //WinDbg:Wait一个对象,然后进行查看 +0x001 Absolute +0x002 Size +0x003 Inserted +0x004 SignalState //是否有信号(大于0表示有信号) +0x008 WaitListHead //双向链表头 圈着所有等待块
- 第一次循环时,若等待对象未超时,但是有信号,就不会将当前线程的等待块挂到等待对象的链表(WaitListHead)中,直接修改信号的值,退出循环
- 第一次循环时,若等待对象未超时,但是无信号,就将当前线程的等待块挂到等待对象的链表(WaitListHead)中,将线程自己挂入等待队列(KiWaitListHead),切换线程
- 当线程将自己挂入等待队列后,需要等待另一个线程将自己唤醒(设置等待对象信号量>0),当其它线程将自己唤醒后,再沿着等待网找是谁唤醒了自己,找到了之后将自己从等待链表(KiWaitListHead)中摘出,但并未从等待网中摘出
- 线程从哪里切换就从哪里复活
完整逻辑:
while(true)//每次线程被其他线程唤醒,都要进入这个循环
{
if(符合激活条件)//1、超时 2、等待对象SignalState>0
{
//1) 修改SignalState
//2) 退出循环
}
else
{
if(第一次执行)
将当前线程的等待块挂到等待对象的链表(WaitListHead)中;
//将自己挂入等待队列(KiWaitListHead)
//切换线程...再次获得CPU时,从这里开始执行
}
}
退出循环:
- 线程将自己+5C的位置清0(WaitBlockList)
- 释放 _KWAIT_BLOCK 所占用的内存
总结
-
不同的等待对象,用不同的方法来修改 _DISPATCHER_HEADER->SignalState
-
如果可等待对象是EVENT,其他线程通常使用SetEvent来设置SignalState = 1,并且,将正在等待该对象的其他线程唤醒,也就是从等待链表(KiWaitListHead)中摘出来,此时线程临时复活
-
SetEvent函数并不会将线程从等待网上摘下来,是否要下来,由当前线程自己来决定
-
若使用SetEvent这种函数直接将线程从等待网上摘下来,将会非常麻烦,因为可能有非常多的线程在等待一个对象,无法判断该将谁摘下(一个也线程可能等待着多个对象)
比如:线程A和线程B同时在等待着一个对象,这时如果有线程C调用了SetEvent(将等待对象的信号量置1),线程A和线程B会被临时唤醒(从KiWaitLkistHead摘下),并行进入关键循环,假设线程A先运行,线程A会设置等待对象的信号量<=0,然后将自己从等待网上摘下来,此时线程A彻底复活。线程B再去判断等待对象是否有信号量时,已经没有信号量了,这时线程B会将自己重新挂入等待链表中(有点绕,慢慢理解)
-
不同对象调用API修改信号个数只在细节上有差异,本质上都是一样的
关于强制唤醒
描述:在APC专题中讲过,当插入一个用户APC时(Alertable=1),当前线程是可以被唤醒的,但并不是真正的唤醒。因为如果当前的线程在等待网上,执行完用户APC后,线程仍然要进入等待状态
实验:证明等待块与等待块表的关系
第一步:编译并运行以下代码
#include <stdio.h>
#include <windows.h>
HANDLE hEvent[3];
DWORD WINAPI ThreadProc(LPVOID lpParamter)
{
::WaitForSingleObject(hEvent[0], -1);
printf("ThreadProc函数执行\n");
return 0;
}
int main(int argc, char* argv[])
{
hEvent[0] = ::CreateEvent(NULL, TRUE, FALSE, NULL); //创建可等待对象
::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, NULL);
getchar();
return 0;
}
第二步:再WinDbg中找到该进程
第三步:查看线程信息