更多的锁介绍可以先看看这篇文章: 多线程锁详解之【序章】
正文:
Event,又名事件锁或者事件对象,是一个内核锁,可用于跨进程等待。
先看看事件锁的头文件定义:
HANDLE WINAPI CreateEvent(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes, //安全描述符
_In_ BOOL bManualReset, //是否自动重置
_In_ BOOL bInitialState, //初始状态
_In_opt_ LPCTSTR lpName //内核对象名称
);
对内核有了解的同学都知道,一般应用层的内核函数就,经过一系列参数检查和转换后,下层都会调用对应的【Nt!XXX】函数,那我们再来看看NtCreateEvent源码
NTSTATUS NTAPI NtCreateEvent(
[out] PHANDLE EventHandle, //接收的对象指针
[in] ACCESS_MASK DesiredAccess, //访问权限
[in, optional] POBJECT_ATTRIBUTES ObjectAttributes, //安全属性,包含了对象名称
[in] EVENT_TYPE EventType, //是否自动重置
[in] BOOLEAN InitialState //初始状态,是否有信号
);
NTSTATUS NTAPI NtCreateEvent(OUT PHANDLE EventHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN EVENT_TYPE EventType,
IN BOOLEAN InitialState)
{
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
PKEVENT Event;
HANDLE hEvent;
NTSTATUS Status;
PAGED_CODE();
DPRINT("NtCreateEvent(0x%p, 0x%x, 0x%p)\n",
EventHandle, DesiredAccess, ObjectAttributes);
/* Check if we were called from user-mode */
if (PreviousMode != KernelMode)
{
/* Enter SEH Block */
_SEH2_TRY
{
/* Check handle pointer */
ProbeForWriteHandle(EventHandle);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
/* Return the exception code */
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
/* Create the Object */
Status = ObCreateObject(PreviousMode,
ExEventObjectType,
ObjectAttributes,
PreviousMode,
NULL,
sizeof(KEVENT),
0,
0,
(PVOID*)&Event);
/* Check for Success */
if (NT_SUCCESS(Status))
{
/* Initalize the Event */
KeInitializeEvent(Event,
EventType,
InitialState);
/* Insert it */
Status = ObInsertObject((PVOID)Event,
NULL,
DesiredAccess,
0,
NULL,
&hEvent);
/* Check for success */
if (NT_SUCCESS(Status))
{
/* Enter SEH for return */
_SEH2_TRY
{
/* Return the handle to the caller */
*EventHandle = hEvent;
}
_SEH2_EXCEPT(ExSystemExceptionFilter())
{
/* Get the exception code */
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
}
}
/* Return Status */
return Status;
}
由于nt 函数可能是用户层与内核层交互,所以上面的try系列动作,是用来验证地址权限保证安全的,在不考虑安全性和出错的情况下,我们可以简化为下面这样。
NTSTATUS NTAPI NtCreateEvent(OUT PHANDLE EventHandle)
{
PKEVENT Event = ObCreateObject(); //创建对象,把它当成 malloc 函数即可。
//有一些很细心的同学可能会观察到, 类似对象名称等一些参数,在KEVENT结构体里面是没有地方存放的
//其实ObCreateObject 相当于 malloc(sizeof(KEVENT) + 额外属性尺寸), ObCreateObject 把对象名称
//等一些参数,追加到KEVENT 后面了,而且故意没有声明这些对象的成员变量,相当于把一些额外的属性
//隐藏了起来。
KeInitializeEvent(Event); //根据参数初始化对象
HANDLE hEvent = ObInsertObject(Event); //将内核对象指针绑定到句柄表
*EventHandle = hEvent; //将句柄赋值到句柄指针中
return; //返回
}
由于我们不是学习驱动,所以上面函数的用法同学们可以自己上网查查,反正它核心的动作就是创建了一个 KEVENT 结构体,然后初始化后返回,让我们来看看 KEVENT 的定义 :
typedef struct _KEVENT {
DISPATCHER_HEADER Header;
} KEVENT, *PKEVENT, *RESTRICTED_POINTER PRKEVENT;
噢,原来 KEVENT 只是简单包含了 DISPATCHER_HEADER 结构体,那我们再来看看 DISPATCHER_HEADER :
typedef struct _DISPATCHER_HEADER {
_ANONYMOUS_UNION union {
_ANONYMOUS_STRUCT struct {
UCHAR Type;
_ANONYMOUS_UNION union {
_ANONYMOUS_UNION union {
UCHAR TimerControlFlags;
_ANONYMOUS_STRUCT struct {
UCHAR Absolute:1;
UCHAR Coalescable:1;
UCHAR KeepShifting:1;
UCHAR EncodedTolerableDelay:5;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;
UCHAR Abandoned;
#if (NTDDI_VERSION < NTDDI_WIN7)
UCHAR NpxIrql;
#endif
BOOLEAN Signalling;
} DUMMYUNIONNAME;
_ANONYMOUS_UNION union {
_ANONYMOUS_UNION union {
UCHAR ThreadControlFlags;
_ANONYMOUS_STRUCT struct {
UCHAR CpuThrottled:1;
UCHAR CycleProfiling:1;
UCHAR CounterProfiling:1;
UCHAR Reserved:5;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;
UCHAR Size;
UCHAR Hand;
} DUMMYUNIONNAME2;
_ANONYMOUS_UNION union {
#if (NTDDI_VERSION >= NTDDI_WIN7)
_ANONYMOUS_UNION union {
UCHAR TimerMiscFlags;
_ANONYMOUS_STRUCT struct {
#if !defined(_X86_)
UCHAR Index:TIMER_EXPIRED_INDEX_BITS;
#else
UCHAR Index:1;
UCHAR Processor:TIMER_PROCESSOR_INDEX_BITS;
#endif
UCHAR Inserted:1;
volatile UCHAR Expired:1;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;
#else
/* Pre Win7 compatibility fix to latest WDK */
UCHAR Inserted;
#endif
_ANONYMOUS_UNION union {
BOOLEAN DebugActive;
_ANONYMOUS_STRUCT struct {
BOOLEAN ActiveDR7:1;
BOOLEAN Instrumented:1;
BOOLEAN Reserved2:4;
BOOLEAN UmsScheduled:1;
BOOLEAN UmsPrimary:1;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME; /* should probably be DUMMYUNIONNAME2, but this is what WDK says */
BOOLEAN DpcActive;
} DUMMYUNIONNAME3;
} DUMMYSTRUCTNAME;
volatile LONG Lock;
} DUMMYUNIONNAME;
LONG SignalState; // event 是否有状态(当成BOOL处理)
LIST_ENTRY WaitListHead; //锁的特性是排队等待,所以它的本质是一个队列数据结构
} WaitDISPATCHER_HEADER, *PDISPATCHER_HEADER;
再看看 LIST_ENTRY 结构,其实没什么特别的,就一个双向链表定义,它是到处通用的,你自己不想定义双向链表时也可以引用它:
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
或许 DISPATCHER_HEADER 结构体看的你头都疼了,但是不要慌,再看看 KeInitializeEvent 的实现,你会明白自己主要关注那几个成员变量:
VOID
NTAPI
KeInitializeEvent(OUT PKEVENT Event, // event对象
IN EVENT_TYPE Type, // 是否自动重置
IN BOOLEAN State) // 初始状态是否有信号
{
/* Initialize the Dispatcher Header */
Event->Header.Type = Type;
//Event->Header.Signalling = FALSE; // fails in kmtest
Event->Header.Size = sizeof(KEVENT) // sizeof(ULONG);
Event->Header.SignalState = State;
InitializeListHead(&(Event->Header.WaitListHead)); //
}
FORCEINLINE
VOID
InitializeListHead(
_Out_ PLIST_ENTRY ListHead)
{
ListHead->Flink = ListHead->Blink = ListHead;
}
如果同学们能够举一反三,估计已经猜出ResetEvent, SetEvent 两个函数是怎么写的了。
我们来自己先写一个模拟代码:
void ResetEvent(PKEVENT Event
{
Event->Header.SignalState = 0; //设置为无状态
}
void SetEvent(PKEVENT Event
{
Event->Header.SignalState = 1; //设置为有状态
if (Event->Header.Type == 自动重置)
{
//如果有线程等待,只唤醒一条线程
}
else
{
//如果有线程等待,唤醒所有等待线程
}
}
我们设计的 SetEvent 函数有两个疑问点,一是怎么知道有多少个线程在等待?
之前已经说了,线程等待是阻塞排队动作,所以等待的线程会按进入等待的时间顺序插入到 Event->Header.WaitListHead 数据结构的尾部。这里有一个问题要提醒一下,虽然等待队列的插入是有顺序的,但是线程在等待过程中可能被警醒(不是正常的唤醒),等待队列的节点也可能随之发生变化,所以线程的唤醒顺序,你必须认为跟插入顺序是没关系的。
另外一个问题是在自动重置状态中,什么时候被重置回无信号?
但是是肯定在WaitForSigleObject, 你想想,假设并没有线程等待(没有线程调用(WaitForSigleObject), 如果此时SetEvent会把状态还原为无信号的话,那么这次SetEvent算是什么都没做了。
接下来我们再看看ResetEvent的代码:
NTSTATUS
NTAPI
NtResetEvent(IN HANDLE EventHandle,
OUT PLONG PreviousState OPTIONAL)
{
PKEVENT Event;
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
NTSTATUS Status;
PAGED_CODE();
DPRINT("NtResetEvent(EventHandle 0%x PreviousState 0%x)\n",
EventHandle, PreviousState);
/* Check if we were called from user-mode */
if ((PreviousState) && (PreviousMode != KernelMode))
{
/* Entry SEH Block */
_SEH2_TRY
{
/* Make sure the state pointer is valid */
ProbeForWriteLong(PreviousState);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
/* Return the exception code */
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
/* Open the Object */
Status = ObReferenceObjectByHandle(EventHandle,
EVENT_MODIFY_STATE,
ExEventObjectType,
PreviousMode,
(PVOID*)&Event,
NULL);
/* Check for success */
if(NT_SUCCESS(Status))
{
/* Reset the Event */
LONG Prev = KeResetEvent(Event);
ObDereferenceObject(Event);
/* Check if caller wants the old state back */
if(PreviousState)
{
/* Entry SEH Block for return */
_SEH2_TRY
{
/* Return previous state */
*PreviousState = Prev;
}
_SEH2_EXCEPT(ExSystemExceptionFilter())
{
/* Get the exception code */
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
}
}
/* Return Status */
return Status;
}
代码虽然多,其实也就三个步骤:
void ResetEvent(HANDLE EventHandle)
{
根据句柄找到对象,并把对象引用计数加1
PKEVENT Event = ObReferenceObjectByHandle(EventHandle);
KeResetEvent(Event); //这才是真正的操作
ObDereferenceObject(Event);//操作完了,引用计数减1
}
再看看 KeResetEvent
LONG
NTAPI
KeResetEvent(IN PKEVENT Event)
{
KIRQL OldIrql;
LONG PreviousState;
ASSERT_EVENT(Event);
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
/* Lock the Dispatcher Database */
//挂入本线程当前拥有的互斥对象链表
OldIrql = KiAcquireDispatcherLock();
/* Save the Previous State */
PreviousState = Event->Header.SignalState;
/* Set it to zero */
Event->Header.SignalState = 0;
/* Release Dispatcher Database and return previous state */
KiReleaseDispatcherLock(OldIrql);
return PreviousState;
}
哈哈,这代码跟我们自己模拟的 ResetEvent 差不多,就是多了 KiAcquireDispatcherLock 和 KiReleaseDispatcherLock 两个操作,大家可以简单认为它是一个互斥锁,能保证数据独占访问就行了。
然后再看看 SetEvent :
NTSTATUS
NTAPI
NtSetEvent(IN HANDLE EventHandle,
OUT PLONG PreviousState OPTIONAL)
{
PKEVENT Event;
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
NTSTATUS Status;
PAGED_CODE();
DPRINT("NtSetEvent(EventHandle 0%x PreviousState 0%x)\n",
EventHandle, PreviousState);
/* Check if we were called from user-mode */
if ((PreviousState) && (PreviousMode != KernelMode))
{
/* Entry SEH Block */
_SEH2_TRY
{
/* Make sure the state pointer is valid */
ProbeForWriteLong(PreviousState);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
/* Return the exception code */
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
/* Open the Object */
Status = ObReferenceObjectByHandle(EventHandle,
EVENT_MODIFY_STATE,
ExEventObjectType,
PreviousMode,
(PVOID*)&Event,
NULL);
if (NT_SUCCESS(Status))
{
/* Set the Event */
LONG Prev = KeSetEvent(Event, EVENT_INCREMENT, FALSE);
ObDereferenceObject(Event);
/* Check if caller wants the old state back */
if (PreviousState)
{
/* Entry SEH Block for return */
_SEH2_TRY
{
/* Return previous state */
*PreviousState = Prev;
}
_SEH2_EXCEPT(ExSystemExceptionFilter())
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
}
}
/* Return Status */
return Status;
}
有了前面的知识铺垫,我们直接跳到 KeSetEvent 吧
(关于KeSetEvent的函数定义,可以看看 微软文档 的说明,里面有详细的参数介绍)
LONG
NTAPI
KeSetEvent(IN PKEVENT Event,
IN KPRIORITY Increment,
IN BOOLEAN Wait)
{
KIRQL OldIrql;
LONG PreviousState;
PKTHREAD Thread;
ASSERT_EVENT(Event);
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
/*
* Check if this is an signaled notification event without an upcoming wait.
* In this case, we can immediately return TRUE, without locking.
*/
//if(Event->Header.Type == 手动重置信号 &&
// Event->Header.SignalState == 有信号 &&
// 内部不需要调用KiReleaseDispatcherLock函数)
//对于SetEvent 的下层调用,KeSetEvent的Wait参数总为FALSE
if ((Event->Header.Type == EventNotificationObject) &&
(Event->Header.SignalState == 1) &&
!(Wait))
{
/* Return the signal state (TRUE/Signalled) */
return TRUE;
}
/* Lock the Dispathcer Database */
//挂入本线程当前拥有的互斥对象链表
OldIrql = KiAcquireDispatcherLock();
/* Save the Previous State */
PreviousState = Event->Header.SignalState;
/* Set the Event to Signaled */
//此处跟我们的模拟代码没什么区别
Event->Header.SignalState = 1;
/* Check if the event just became signaled now, and it has waiters */
if (!(PreviousState) && !(IsListEmpty(&Event->Header.WaitListHead)))
{
/* Check the type of event */
if (Event->Header.Type == EventNotificationObject) //手工重置信号
{
/* Unwait the thread */
//唤醒所有正在等待的线程
KxUnwaitThread(&Event->Header, Increment);
}
else
{
/* Otherwise unwait the thread and unsignal the event */
//自动重置信号,只唤醒一条等待的线程
KxUnwaitThreadForEvent(Event, Increment);
}
}
/* Check what wait state was requested */
if (!Wait)
{
/* Wait not requested, release Dispatcher Database and return */
KiReleaseDispatcherLock(OldIrql);
}
else
{
/* Return Locked and with a Wait */
Thread = KeGetCurrentThread();
Thread->WaitNext = TRUE;
Thread->WaitIrql = OldIrql;
}
/* Return the previous State */
return PreviousState;
}
之前我们也说过,调用 WaitForSigleObject 陷入等待的线程,会按顺序插入到 Event->Header.WaitListHead 队列中,这个就是阻塞队列,这个数据结构里面的节点,保存的都是 PKTHREAD (线程对象)的指针。
而所谓的唤醒,其实是把线程对象从阻塞队列移动到就绪队列中,本质上就是一个数据管理结构迁移的过程。这里要提醒大家,线程对象只是被放到了就绪队列中,处于准备执行的状态,事实上它还没真正放到CPU上面执行的。只有系统将线程对象从就绪队列取出,放到CPU上跑代码,线程才是真正地在执行。所以线程的唤醒状态(即就绪状态)跟执行状态(即运行状态)其实不是同一个概念来的,而且线程从被唤醒到被执行,具有一个“延时性” —— 它在就绪队列中等待被系统调度。
另外一个是关于信号的重置问题,之前说过,信号是在 WaitForSigleObject 进行重置的。要讲清楚事件锁的激活和等待,就不能不说 WaitForSigleObject 这个函数,但由于篇幅太长,这个函数我将放到独立的章节讲解。
关于 WaitForSingleObject 函数的介绍在这里: 多线程锁详解之【WaitForSingleObject】