Windows事件等待学习笔记(四)—— 事件&信号量&互斥体

要点回顾

  1. 线程在进入临界区之前会调用WaitForSingleObject或者WaitForMultipleObjects
  2. 此时如果有信号,线程会从函数中退出并进入临界区;如果没有信号那么线程将自己挂入等待链表,然后将自己挂入等待网,最后切换线程
  3. 其它线程在适当的时候,调用方法修改被等待对象的SingleState,设置为有信号(不同的等待对象,会调用不同的函数),并将等待该对象的其它线程从等待链表中摘掉,这样,当前线程便会在WaitForSingleObject或者WaitForMultipleObjects恢复执行(在哪切换就在哪开始执行),如果符合唤醒条件,此时会修改SignalState的值,并将自己从等待网上摘下来,此时的线程才是真正的唤醒
  4. 被等待对象不同,主要差异在以下两点:
    1. 不同的被等待对象,修改SingnalState所调用的函数不同
    2. 当前线程一旦被临时唤醒后,会从原来进入等待状态的地方继续执行,不同的等待对象,判断是否符合激活条件修改SignalState的具体操作不同
      在这里插入图片描述

事件

创建事件对象APICreateEvent(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);						//新增一行,设置信号量
第六步:观察结果

在这里插入图片描述

总结
  1. CreateEvent函数的第三个参数决定了事件对象一开始是否有信号
  2. 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得到了执行
在这里插入图片描述

解释说明
  1. CreateEvent第二个参数为TRUE,系统将事件对象的Type设置成0,此时对象为通知类型类型
  2. CreateEvent第二个参数为FALSE,系统将事件对象的Type设置成1,此时对象为事件同步对象
  3. 当SetEvent函数将信号值(SignalState)设置为1时,如果对象Type0,唤醒所有等待该状态的线程;如果对象Type1,从链表头找到第一个并唤醒

分析WaitForSingleObject

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
总结

  1. 当事件对象的Type为0时,WaitForSingleObject函数在处理时并不修改对象的信号量,原来是多少还是多少,因此所有线程都得以执行
  2. 当事件对象的Type为1时,WaitForSingleObject函数在处理时对象的信号量清零,因此只有一个线程能够得到执行

信号量

描述

  1. 事件中,当一个线程进入临界区时,其它所有事件都无法进入临界区
  2. 信号量允许多个线程进入临界区

优点:举个例子,在生产者与消费者的问题中,若生产者只有三份,那么开五个消费者线程是没有意义的,信号量的存在正是为了解决这种问题

创建信号量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

  1. ReleaseSemaphore
  2. NtReleaseSemaphore
  3. KeReleaseSemaphore
    1. 设置SignalState = SignalState + N(参数)
    2. 通过WaitListHead找到所有线程,并从等待链表中摘除

互斥体

描述

  1. 互斥体(MUTANT)事件(EVENT)信号量(SEMAPHORE) 一样,都可以用来进行线程的同步控制
  2. 但需要注意的是,这几个对象都是内核对象,这就意味着,通过这些对象可以进行跨进程的线程同步控制,比如:A进程中的X线程B进程中的Y线程都可以控制等待对象Z

极端情况

  1. 若B进程的Y线程还没有来得及调用修改SignalState的函数(如SetEvent),那么等待对象Z将被遗弃,这也就意味着A进程的X线程将永远等待下去
  2. 当遇到这种情况时,事件和信号量可能无法解决,但互斥体可以很好地解决


当构造了一个临界区一,等待的对象是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(即退出最外圈临界区后),说明其他进程可以使用了,将该互斥体从线程链表中移除

解决遗弃问题

描述

  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

  1. 用户层Mutant
    对应内核函数:NtCreateMutant
    ApcDisable=0
  2. 内核层Mutex
    对应内核函数:NtCreateMutex
    ApcDisable=1

注意:若在三环创建互斥体(Mutant),内核APC仍然可以使用;若通过零环创建互斥体(Mutex),那么当前内核APC是被禁止的

在这里插入图片描述

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值