Windows事件等待学习笔记(二)—— 线程等待与唤醒
要点回顾
之前学习了如何自己实现临界区以及什么是自旋锁,这两种同步方案在线程无法进入临界区时都会让当前线程进入等待状态。
一种是通过Sleep函数实现的,一种是通过让当前的CPU“空转”实现的,但这两种等待方式都有局限性:
- 通过Sleep函数进行等待,等待时间该如何确定呢?
- 通过“空转”的方式进行等待,只有等待时间很短的情况下才有意义,否则对CPU资源是种浪费,并且自旋锁只能在多核的环境下才有意义
思考:有没有更加合理的等待方式,只有在条件成熟的时候才将当前线程唤醒?
等待与唤醒机制
描述:在Windows中,一个线程可以通过等待一个或者多个可等待对象,从而进入等待状态,另一个线程可以在某些时刻唤醒等待这些对象的其他线程
可等待对象
在Windbg中查看以下结构体:
dt _KPROCESS
dt _KTHREAD
dt _KTIMER
dt _KSEMAPHORE
dt _KEVENT
dt _KMUTANT
dt _FILE_OBJECT
总结
- 可等待对象正常情况下都是以 _DISPATCHER_HEADER 结构体开头的,但是有一些特殊的结构体并不是以 _DISPATCHER_HEADER 开头的(如 _FILE_OBJECT),但是windows又希望把它们也变成所谓的可等待对象,因此在它们内部嵌入一个 _DISPATCHER_HEADER 这样的结构体
- 只要是包含 _DISPATCHER_HEADER 结构体的对象,都可以看作是可等待对象,都可以使用 WaitForSingleObject 和 WaitForMultipleObjects 这两个函数进入等待状态
可等待对象的差异
WaitForSingleObject(3环)
↓
NtWaitForSingleObject(内核)
- 通过3环用户提供的句柄,找到等待对象的内核地址。
- 如果是以 _DISPATCHER_HEADER 开头,直接使用。
- 如果不是以 _DISPATCHER_HEADER 开头的对象,则找到在其中嵌入的 _DISPATCHER_HEADER 对象
↓
KeWaitForSingleObject(内核) //核心功能
线程与等待对象
描述:一个线程可以等待一个对象,也可以等待多个对象
一个线程等待一个对象
实验
第一步:编译并运行以下代码
#include <stdio.h>
#include <windows.h>
HANDLE hEvent[2];
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); //创建一个可等待对象 _KEVENT
::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, NULL);
getchar();
return 0;
}
第二步:在WinDbg中找到该进程
!process 0 0
第三步:查看线程信息
查看 _KTHREAD
查看 _KWAIT_BLOCK
kd> dt _KWAIT_BLOCK 0x85f49278
nt_400000!_KWAIT_BLOCK
+0x000 WaitListEntry : _LIST_ENTRY [ 0x86177180 - 0x86177180 ]
+0x008 Thread : 0x85f49208 _KTHREAD //所属线程
+0x00c Object : 0x86177178 Void //被等待对象的地址
+0x010 NextWaitBlock : 0x85f49278 _KWAIT_BLOCK //单向循环链表,只有一个时指向自己
+0x014 WaitKey : 0 //等待块的索引(下标)
+0x016 WaitType : 1 //只等待一个等待块时置1
//等待所有等待对象符合条件时,置0
一个线程等待多个对象
实验
第一步:编译并运行以下代码
#include <stdio.h>
#include <windows.h>
HANDLE hEvent[2];
DWORD WINAPI ThreadProc(LPVOID lpParamter)
{
::WaitForMultipleObjects(2, hEvent, FALSE, -1);
printf("ThreadProc函数执行\n");
return 0;
}
int main(int argc, char* argv[])
{
hEvent[0] = ::CreateEvent(NULL, TRUE, FALSE, NULL); //创建可等待对象
hEvent[1] = ::CreateEvent(NULL, TRUE, FALSE, NULL); //创建可等待对象
::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, NULL);
getchar();
return 0;
}
第二步:在WinDbg中找到该进程
!process 0 0
第三步:查看线程信息
dt _KTHREAD 862b3340
共有两个等待块:
注意:此时WaitType
字段的值仍是1,这是因为只要有一个对象满足条件,当前线程就可以被唤醒(详见第一步代码部分)
第四步:查看等待块具体细节
代码中创建的对象为 _KEVENT,所以 _KWAIT_BLOCK +0x00C Object 指向 _KEVENT +0x000 _DISPATCHER_HEADER
kd> dt _DISPATCHER_HEADER 0x86015db8
nt_400000!_DISPATCHER_HEADER
+0x000 Type : 0 ''
+0x001 Absolute : 0x21 '!'
+0x002 Size : 0x4 ''
+0x003 Inserted : 0x86 ''
+0x004 SignalState : 0n0
+0x008 WaitListHead : _LIST_ENTRY [ 0x862b33c8 - 0x862b33c8 ]
WaitListHead
:双向链表,圈着所有等待块的WaitListEntry
等待网
总结
- 等待中的线程,一定在等待链表中(KiWaitListHead),同时也一定在这张网上(KTHREAD +5C的位置不为空)
- 线程通过调用
WaitForSingleObject
/WaitForMultipleObjects
函数将自己挂到这张网上 - 线程什么时候会再次执行取决于其他线程何时调用相关函数,等待对象不同调用的函数也不同