前言
前面学习了如何自己实现临界区以及什么是Windows自旋锁,这两种同步方案在线程无法进入临界区时都会让当前线程进入等待状态,一种是通过Sleep函数实现的,一种是通过让当前的CPU“空转”实现的,但这两种等待方式都有局限性:
- 通过Sleep函数进行等待,等待时间该如何确定?
- 通过“空转”的方式进行等待,只有等待时间很短的情况下才有意义,否则对CPU资源是种浪费。而且自旋锁只能在多核的环境下才有意义。
基于以上的局限性,本篇将学习一种更加合理的等待唤醒机制。
等待唤醒机制
基本概念
在Windows中,一个线程可以通过等待一个或者多个可等待对象,从而进入等待状态,另一个线程可以在某些时刻唤醒等待这些对象的其他线程。
如上图,A线程用WaitForSingleObject进入等待状态,B线程用SetEvent将线程唤醒,A线程与B线程便可以通过可等待对象联系到一起。
可等待对象
那什么是可等待对象呢?在Windbg中查看如下结构体
1 | 进程 dt _KPROCESS |
他们的第一个成员(文件除外)都是一个结构体:_DISPATCHER_HEADER。
当然,个别结构体不是,例如文件结构体_FILE_OBJECT
尽管不是第一个成员,但文件结构体中也依然包含_DISPATCHER_HEADER。
小结
线程与等待对象
在了解了线程可以通过等待对象建立起联系之后,这里进一步学习这种关系建立起来的内部细节。
一个线程等待一个对象
先从比较简单的一个线程等待一个对象的情形开始:
编译并运行如下代码(环境:Windows XP,Visual C++ 6.0)
c1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
HANDLE hEvent[2];
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
::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中找到该进程,并查看其内容,指令如下:
Code1
2kd>!process 0 0 //找到进程结构体的地址
kd>!process 0x???????? //根据进程结构体的地址查看其内容,并找到其线程结构体的地址根据进程内容,访问其所包含线程(KThread)
这里需要关注的是位于KThread+0x5C处的WaitBlockList等待块,这是一个_KWAIT_BLOCK结构,这个等待块将线程与被等待对象联系到了一起。
下面来看看这个等待块结构是怎样的。
- WaitListEntry:稍后作分析。
- Thread:当前线程。
- Object:被等待对象的地址(本次实验中是一个Event)。
- NextWaitBlock:单向循环链表,存的是与当前线程关联的多个等待块结构体。例如。当前线程只有一个等待对象时,该值指向自己。若存在n个等待对象,则1->2, 2->3 … n->1(单循环链表)。
- WaitKey:等待块索引,表明当前是第几个等待块。
- WaitType:等待类型。若只要有一个等待对象符合条件就可以被激活那么它的值就是1, 如果如果你等待多个对象必须全部符合条件才可以被激活它就是0。
一个线程等待一个对象的情况,可以用下图表示。
一个线程等待多个对象
接着来看一个线程等待多个对象的情况:
编译并运行如下代码(环境:Windows XP,Visual C++ 6.0)
c1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
HANDLE hEvent[2];
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
::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;
}这次创建了2个事件,并在调用线程回调函数时让它们进入等待状态,运行后,进入WinDbg查看细节。
还是跟之前一样,先找到线程,然后在线程结构体0x5C的位置,找到相应的等待块。
接下来观察等待块
由图,这次第一个等待块的NextWaitBlock指向的不是自己,而是下一个等待块。下一个等待块的NextWaitBlock指向了第一个等待块的地址。这里验证了一个线程等待多个可等待对象时,NextWaitBlock字段的作用。另一处,可以看到WaitKey字段也有相应的值。
前面说了这么多,依旧有一个字段没有解释,就是这个WaitListEntry这个字段,观察下图
首先根据等待块里的Object字段,找到被等待对象Event,根据Event结构,它只包含一个_DISPATCHER_HEADER结构,虽然我可以直接看该结构里的字段;在位于0x8处,有一个WaitListHead字段,这是一个链表,该链表指向的就是当前被等待对象Event对应的等待块(若包含多个等待块,则将这些等待块依次插入链表中)。这样线程,等待块,被等待对象之间的关系就理清了。
一个线程等待多个对象的情况可以参考下图:
等待网
如图,这是海哥根据等待关系构造的一张等待网,所有处于等待状态的线程,线程对应的等待块,以及被等待对象,都会位于类似的等待网上。
总结
- 等待中的线程,一定在等待链表中(KiWaitListHead),同时也一定位于等待网上(即KThread+5C的位置不为空)。
- 线程通过调用WaitForSingleObject或WaitForMultipleObjects函数将自己挂到等待网上(Sleep函数也可以,处理过程不太一样)。
- 线程什么时候会再次执行取决于其他线程何时调用相关函数,等待对象不同调用的函数也不同。
参考资料
参考课程:
- 滴水中级预习班课程
参考链接:
- https://blog.csdn.net/qq_41988448/article/details/104626764 (CSDN-lzyddf学习笔记)
- https://blog.csdn.net/weixin_42052102/article/details/83388129 (CSDN-My classmates学习笔记)