4.WaitForSingleObject函数分析

无论可等待对象是何种类型,线程都是通过:

WaitForSingleObject
WaitForMultipleObjects

进入等待状态的,这两个函数是理解线程等待与唤醒进制的核心

WaitForSingleObject参数说明

WaitForSingleObject对应的内核函数:

NTSTATUS stdcall NtWaitForSingleObject
( 
	HANDLE Handle,					
	BOOLEAN Alertable,				
	PLARGE_INTEGER Timeout	
)

Handle 用户层传递的等待对象的句柄 (具体细节参加句柄表专题)

Alertable 对应 KTHREAD 结构体的 Alertable 属性如果为1在插入用户APC时,该线程将被吵醒

Timeout 超时时间



NtWaitForSingleObject

  1. 调用ObReferenceObjectByHandle函数,通过对象句柄找到等待对象结构体地址。
  2. 调用KeWaitForSingleObject函数,进入关键循环。

KeWaitForSingleObject:上半部分

!process 89316020
dt _KTHREAD 892db020
dt _KWAIT_BLOCK 892db020+70

每个等待块大小为0x18。
4.WaitForSingleObject函数分析
如果等待3个对象的话它就会占用前3个等待块,最后一个是给定时器用的。
如果你有4个等待对象它就不会用这个位置了,它会一次性分配新的空间。

  1. 向KTHREAD(+70)位置的等待块赋值。
  2. 如果超时时间不为0, KTHREAD(+70)第四个等待块与第一个等待块关联起来:第一个等待块指向第四个等待块,第四个等待块指向第一个等待块。
  3. KTHREAD(+5C)指向第一个KWAIT_BLOCK
  4. 进入关键循环

KeWaitForSingleObject的关键循环

while(true)//每次线程被其他线程唤醒,都要进入这个循环
	{
		if(符合**条件)//1超时 2等待对象SignalState > 0
		{
		//1修改SignalState 
		//2退出循环
		}
		else//SignalState不大于0 也没超时
		{
			if(第一次执行)
			{
				//将当前线程的等待块挂到等待对象的链表 (WaitListHead) 中;
				//将自己挂入等待队列(KiaitListHead)
				//切换线程...再次获得CPU时,从这里开始执行
			}
		}

	}
1)线程将自己+5c位置清0
2)释放_KWAIT_BLOCK所占内存

WaitForSingleObject参数说明

kd> dt _DISPATCHER_HEADER
nt!_DISPATCHER_HEADER
   +0x000 Type			//类型
   +0x001 Absolute 
   +0x002 Size
   +0x003 Inserted
   +0x004 SignalState		//是否有信号 (值大于0就是有信号)
   +0x008 WaitListHead	//双向链表头,圈着所有等待块

不同的等待对象,用不同的方法来修改_DISPATCHER_HEADER(SignalState)比如:如果可等待对象是EVENT,其他线程通常使用SetEvent来设置SignalState= 1并且,将正在等待该对象的其他线程唤醒,也就是从等待链表(KiWaitListHead)中摘出来。但是, SetEvent函数并不会将线程从等待网上摘下来,是否要下来,由当前线程自己来决定。

关于强制唤醒
在APC专题讲过,当我们插入一个用户APC时(Alertable=1),当前线程是可以被唤醒的,但并不是真正的唤醒。
因为,如果当前的线程在等待网上,执行完用户APC后,仍然要进入等待状态。