Windows事件等待学习笔记(四)—— 事件&信号量&互斥体
要点回顾
- 线程在进入临界区之前会调用WaitForSingleObject或者WaitForMultipleObjects
- 此时如果有信号,线程会从函数中退出并进入临界区;如果没有信号那么线程将自己挂入等待链表,然后将自己挂入等待网,最后切换线程
- 其它线程在适当的时候,调用方法修改被等待对象的SingleState,设置为有信号(不同的等待对象,会调用不同的函数),并将等待该对象的其它线程从等待链表中摘掉,这样,当前线程便会在WaitForSingleObject或者WaitForMultipleObjects恢复执行(在哪切换就在哪开始执行),如果符合唤醒条件,此时会修改SignalState的值,并将自己从等待网上摘下来,此时的线程才是真正的唤醒
- 被等待对象不同,主要差异在以下两点:
- 不同的被等待对象,修改SingnalState所调用的函数不同
- 当前线程一旦被临时唤醒后,会从原来进入等待状态的地方继续执行,不同的等待对象,判断是否符合激活条件和修改SignalState的具体操作不同
事件
创建事件对象API:CreateEvent(NULL, TRUE, FALSE, NULL);
_DISPATCHER_HEADER
+0x000 Type //CreateEvent的第二个参数决定了当前事件对象的类型
//TRUE:通知类型对象 FALSE:事件同步对象
+0x001 Absolute
+0x002 Size
+0x003 Inserted
+0x004 SignalState //CreateEvent的第三个参数决定了这里是否有值
+0x008 WaitListHead
实验:验证SignalState
第一步:编译并运行以下代码
#include <stdio.h>
#include <windows.h>
HANDLE g_hEvent;
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
//当事件变成已通知时
WaitForSingleObject(g_hEvent, INFINITE);
printf("Thread执行了!\n");
return 0;
}
int main()
{
//创建事件
//默认安全属性 对象类型 初始状态 名字
g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
//设置为有信号
//SetEvent(g_hEvent);
//创建线程
::CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
getchar();
return 0;
}
第二步:观察结果
无任何输出,g_hEvent句柄处于无信号状态
第三步:修改代码并执行
CreateEvent(NULL, FALSE, TRUE, NULL); //第三个参数由FALSE改为TRUE
第四步:观察结果
第五步:修改代码并执行
CreateEvent(NULL, FALSE, FALSE, NULL); //第三个参数由TRUE改为FALSE
SetEvent(g_hEvent); //新增一行,设置信号量
第六步:观察结果
总结
- CreateEvent函数的第三个参数决定了事件对象一开始是否有信号
- CreateEvent函数第三个参数为
TRUE
时,效果等同于在下一行调用了SetEvent();
实验二:验证Type
第一步:编译并运行以下代码
#include <stdio.h>
#include <windows.h>
HANDLE g_hEvent;
DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
//当事件变成已通知时
WaitForSingleObject(g_hEvent, INFINITE);
printf("Thread1执行了!\n");
return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
//当事件变成已通知时
WaitForSingleObject(g_hEvent, INFINITE);
printf("Thread2执行了!\n");
return 0;
}
DWORD WINAPI ThreadProc3(LPVOID lpParameter)
{
//当事件变成已通知时
WaitForSingleObject(g_hEvent, INFINITE);
printf("Thread3执行了!\n");
return 0;
}
int main()
{
//创建事件
//默认安全属性 对象类型 初始状态 名字
g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
HANDLE hThread[3];
//创建3个线程
hThread[0] = ::CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
hThread[1] = ::CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);
hThread[2] = ::CreateThread(NULL, 0, ThreadProc3, NULL, 0, NULL);
//设置事件为已通知
SetEvent(g_hEvent);
//等待线程结束 销毁内核对象
WaitForMultipleObjects(3, hThread, TRUE, INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
CloseHandle(hThread[2]);
CloseHandle(g_hEvent);
getchar();
return 0;
}
第二步:观察结果
三个线程都得到了执行
第三步:修改代码并执行
g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); //第三个参数由TRUE改为FALSE
第四步:观察结果
只有Thread1得到了执行
解释说明
- 当CreateEvent第二个参数为TRUE,系统将事件对象的Type设置成0,此时对象为通知类型类型
- 当CreateEvent第二个参数为FALSE,系统将事件对象的Type设置成1,此时对象为事件同步对象
- 当SetEvent函数将信号值(SignalState)设置为1时,如果对象Type为0,唤醒所有等待该状态的线程;如果对象Type为1,从链表头找到第一个并唤醒
分析WaitForSingleObject
总结:
- 当事件对象的Type为0时,WaitForSingleObject函数在处理时并不修改对象的信号量,原来是多少还是多少,因此所有线程都得以执行
- 当事件对象的Type为1时,WaitForSingleObject函数在处理时对象的信号量清零,因此只有一个线程能够得到执行
信号量
描述:
- 在事件中,当一个线程进入临界区时,其它所有事件都无法进入临界区
- 信号量允许多个线程进入临界区
优点:举个例子,在生产者与消费者的问题中,若生产者只有三份,那么开五个消费者线程是没有意义的,信号量的存在正是为了解决这种问题
创建信号量API:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount, //发放信号量的数量
LONG lMaximumCount, //信号量发放数量的最大值
LPCTSTR lpName);
对应内核结构体:
kd> dt _KSEMAPHORE
ntdll!_KSEMAPHORE
+0x000 Header : _DISPATCHER_HEADER
+0x010 Limit : Int4B //IMaximumCount
kd> dt _DISPATCHER_HEADER
ntdll!_DISPATCHER_HEADER
+0x000 Type : UChar //信号量类型为5
+0x001 Absolute : UChar
+0x002 Size : UChar
+0x003 Inserted : UChar
+0x004 SignalState : Int4B //lInitialCount
+0x008 WaitListHead : _LIST_ENTRY
释放信号量API:
ReleaseSemaphore
NtReleaseSemaphore
KeReleaseSemaphore
- 设置
SignalState = SignalState + N(参数)
- 通过
WaitListHead
找到所有线程,并从等待链表中摘除
- 设置
互斥体
描述:
- 互斥体(MUTANT) 与 事件(EVENT) 和 信号量(SEMAPHORE) 一样,都可以用来进行线程的同步控制
- 但需要注意的是,这几个对象都是内核对象,这就意味着,通过这些对象可以进行跨进程的线程同步控制,比如:A进程中的X线程和B进程中的Y线程都可以控制等待对象Z
极端情况:
- 若B进程的Y线程还没有来得及调用修改SignalState的函数(如SetEvent),那么等待对象Z将被遗弃,这也就意味着A进程的X线程将永远等待下去
- 当遇到这种情况时,事件和信号量可能无法解决,但互斥体可以很好地解决
例:
当构造了一个临界区一,等待的对象是A,又在临界区内部构造了一个临界区二,等待对象为A、B、C三个,当临界区一执行完自己的功能后,如果等待的对象为事件或者信号量,那么就必须调用相关API将对象设置为有信号,若进入临界区二前未调用相关API,那么临界区二将永远进入等待状态,这种情况称为死锁。
当一个对象需要重复进入临界区时,若A对象为互斥体,就不会出现死锁。
结构体:
nt!_KMUTANT
+0x000 Header : _DISPATCHER_HEADER
+0x010 MutantListEntry : _LIST_ENTRY
+0x018 OwnerThread : Ptr32 _KTHREAD
+0x01c Abandoned : UChar
+0x01d ApcDisable : UChar
MutantListEntry
:拥有互斥体线程 (KTHREAD+0x010 MutantListHead),是个链表头,圈着当前线程所有的互斥体
OwnerThread
:正在拥有互斥体的线程
Abandoned
:是否已经被放弃不用
ApcDisable
:是否禁用内核APC
创建互斥体
函数:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTE SlpMutexAttributes, // 指向安全属性的指针
BOOL bInitialOwner, // 初始化互斥对象的所有者
LPCTSTR lpName // 指向互斥对象名的指针
);
API调用流:
CreateMutex
->NtCreateMutant
(内核函数) ->KeInitializeMutant
(内核函数)
初始化MUTANT结构体:
MUTANT.Header.Type=2;
MUTANT.Header.SignalState=bInitialOwner ? 0 : 1;
MUTANT.OwnerThread=bInitialOwner ? 当前线程 : NULL;
MUTANT.Abandoned=0;
MUTANT.ApcDisable=0;
bInitialOwner==TRUE 将当前互斥体挂入到当前线程的互斥体链表
(KTHREAD+0x010 MutantListHead)
分析 WaitForSingleObject
释放互斥体
API:
BOOL WINAPI ReleaseMutex(HANDLE hMutex);
API执行流:
ReleaseMutex -> NtReleaseMutant ->
KeReleaseMutant
正常调用时:
MUTANT.Header.SignalState++;
如果SignalState=1(即退出最外圈临界区后),说明其他进程可以使用了,将该互斥体从线程链表中移除
解决遗弃问题
描述:
- 当一个进程非正常“死亡时”,系统会调用内核函数
MmUnloadSystemImage
处理后事
内核函数MmUnloadSystemImage
会调用KeReleaseMutant(X, Y, Abandon, Z)
,第三个参数用来判断该互斥体是否被丢弃,正常释放时值为false,有且只有互斥体有这个待遇
if(Abandon == false) //正常调用
{
MUTANT.Header.SignalState++;
}
else
{
MUTANT.Header.SignalState == 1;
MUTANT.OwnerThread == NULL;
}
if(MUTANT.Header.SignalState==1) //意外结束
MUTANT.OwnerThread == NULL;
从当前线程互斥体链表中将当前互斥体移除
ApcDisable
- 用户层:Mutant
对应内核函数:NtCreateMutant
ApcDisable=0 - 内核层:Mutex
对应内核函数:NtCreateMutex
ApcDisable=1
注意:若在三环创建互斥体(Mutant),内核APC仍然可以使用;若通过零环创建互斥体(Mutex),那么当前内核APC是被禁止的