多线程锁详解之【事件对象】

更多的锁介绍可以先看看这篇文章: 多线程锁详解之【序章】

正文:
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】

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值