更多的锁介绍可以先看看这篇文章: 多线程锁详解之【序章】
正文:
先看看实现源码吧:
DWORD
WINAPI
WaitForSingleObject(IN HANDLE hHandle,
IN DWORD dwMilliseconds)
{
/* Call the extended API */
return WaitForSingleObjectEx(hHandle, dwMilliseconds, FALSE);
}
可以看到,WaitForSingleObject 只是简单地对 WaitForSingleObjectEx 进行了一个封装,而 WaitForSingleObjectEx 只是增加了一个 bAlertable 参数。这个参数作用是是否检查APC队列。
什么是APC队列?
简而言之,APC队列就是一组回调函数,如果 bAlertable 参数为真,则 WaitForSingleObjectEx 会检查当前线程的APC队列是否存在符合触发条件的APC回调函数,若存在至少一个,则按节点顺序轮流调用,然后 WaitForSingleObjectEx 返回。关于APC回调函数,要记住以下三点:
- APC回调函数,跟 WaitForSingleObjectEx函数是处于同一线程的。
- APC回调函数执行完后,WaitForSingleObjectEx 马上返回,不会检查用户设置的超时时间。
- 用户态线程处于 PASSIVE_LEVEL 中断登记,而APC回调时处于更高的 APC_LEVEL 中断等级,额,这个知识点大家听听就好了,对应用层开发基本没用。
我们还是继续看看 WaitForSingleObjectEx 的实现:
DWORD
WINAPI
WaitForSingleObjectEx(IN HANDLE hHandle,
IN DWORD dwMilliseconds,
IN BOOL bAlertable)
{
PLARGE_INTEGER TimePtr;
LARGE_INTEGER Time;
NTSTATUS Status;
RTL_CALLER_ALLOCATED_ACTIVATION_CONTEXT_STACK_FRAME ActCtx;
/* APCs must execute with the default activation context */
if (bAlertable)
{
/* Setup the frame */
//为APC队列提供一个新的线程上下文环境
RtlZeroMemory(&ActCtx, sizeof(ActCtx));
ActCtx.Size = sizeof(ActCtx);
ActCtx.Format = RTL_CALLER_ALLOCATED_ACTIVATION_CONTEXT_STACK_FRAME_FORMAT_WHISTLER;
RtlActivateActivationContextUnsafeFast(&ActCtx, NULL);
}
/* Get real handle */
//根据标准输入、输出、错误的句柄常量,提取真正的句柄
hHandle = TranslateStdHandle(hHandle);
/* Check for console handle */
//如果是标准输入输出或错误句柄且句柄有效
if ((IsConsoleHandle(hHandle)) && (VerifyConsoleIoHandle(hHandle)))
{
/* Get the real wait handle */
//替换为子系统的输入句柄
hHandle = GetConsoleInputWaitHandle();
}
/* Convert the timeout */
//转换一下时间格式
TimePtr = BaseFormatTimeOut(&Time, dwMilliseconds);
/* Start wait loop */
do
{
/* Do the wait */
//常规性进入Nt 系列函数
Status = NtWaitForSingleObject(hHandle, (BOOLEAN)bAlertable, TimePtr);
if (!NT_SUCCESS(Status))
{
/* The wait failed */
BaseSetLastNTError(Status);
Status = WAIT_FAILED;
}
} while ((Status == STATUS_ALERTED) && (bAlertable));
/* Cleanup the activation context */
//还原线程上下文
if (bAlertable) RtlDeactivateActivationContextUnsafeFast(&ActCtx);
/* Return wait status */
return Status;
}
在看看nt里面怎么实现的
NTSTATUS
NTAPI
NtWaitForSingleObject(IN HANDLE ObjectHandle,
IN BOOLEAN Alertable,
IN PLARGE_INTEGER TimeOut OPTIONAL)
{
PVOID Object, WaitableObject;
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
LARGE_INTEGER SafeTimeOut;
NTSTATUS Status;
/* Check if we came with a timeout from user mode */
if ((TimeOut) && (PreviousMode != KernelMode))
{
/* Enter SEH for proving */
_SEH2_TRY
{
/* Make a copy on the stack */
SafeTimeOut = ProbeForReadLargeInteger(TimeOut);
TimeOut = &SafeTimeOut;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
/* Return the exception code */
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
/* Get the Object */
//将句柄转换成内核对象,引用计数加一
Status = ObReferenceObjectByHandle(ObjectHandle,
SYNCHRONIZE,
NULL,
PreviousMode,
&Object,
NULL);
if (NT_SUCCESS(Status))
{
/* Get the Waitable Object */
//相当于 static_cast<object>
WaitableObject = OBJECT_TO_OBJECT_HEADER(Object)->Type->DefaultObject;
/* Is it an offset for internal objects? */
//判断地址是否为正整数 (内核地址是高2GB,是负数)
if (IsPointerOffset(WaitableObject))
{
/* Turn it into a pointer */
//正整数则代表它是偏移量,进行偏移
WaitableObject = (PVOID)((ULONG_PTR)Object +
(ULONG_PTR)WaitableObject);
}
/* SEH this since it can also raise an exception */
_SEH2_TRY
{
/* Ask the kernel to do the wait */
//进入ke层处理
Status = KeWaitForSingleObject(WaitableObject,
UserRequest,
PreviousMode,
Alertable,
TimeOut);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
/* Get the exception code */
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
/* Dereference the Object */
ObDereferenceObject(Object);//引用计数减一
}
else
{
DPRINT1("Failed to reference the handle with status 0x%x\n", Status);
}
/* Return the status */
return Status;
}
原来nt层只是一个简单的对象检测和转换,那再进ke层看看
NTSTATUS
NTAPI
KeWaitForSingleObject(IN PVOID Object,
IN KWAIT_REASON WaitReason, //线程上次被切出原因
IN KPROCESSOR_MODE WaitMode,
IN BOOLEAN Alertable,
IN PLARGE_INTEGER Timeout OPTIONAL)
{
PKTHREAD Thread = KeGetCurrentThread();
//这个其实是 DISPATCHER_HEADER,对象能够等待,就是利用了DISPATCHER_HEADER这个结构
//线程,进程,事件,互斥等对象,能够进行等待,就是因为他们结构的第一个成员就是 DISPATCHER_HEADER
//把DISPATCHER_HEADER放在对象成员第一位的,叫可直接等待对象
//临界区,文件句柄这些,没有把 DISPATCHER_HEADER 放在第一位,它们叫可间接等待对象
PKMUTANT CurrentObject = (PKMUTANT)Object;//这个就是用应用层句柄转换出来的内核对象
//线程等待块,这是一个队列数据结构,为什么等待块会是一个队列呢?
//因为还有另外一个函数:WaitForMultipleObject ,这个函数可以等待多个对象
//所以这个等待块是为了兼容等待多个对象的情况
PKWAIT_BLOCK WaitBlock = &Thread->WaitBlock[0];
//#define TIMER_WAIT_BLOCK 0x03
PKWAIT_BLOCK TimerBlock = &Thread->WaitBlock[TIMER_WAIT_BLOCK]; //超时队列
PKTIMER Timer = &Thread->Timer; //定时器对象
NTSTATUS WaitStatus;
BOOLEAN Swappable;
LARGE_INTEGER DueTime = {{0}}, NewDueTime, InterruptTime;
PLARGE_INTEGER OriginalDueTime = Timeout;
ULONG Hand = 0;
//这个检查是无效的代码,一旦开启优化它就没有了
//debug状态下检测中断等级
if (Thread->WaitNext)
ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL);
else
ASSERT(KeGetCurrentIrql() < DISPATCH_LEVEL ||
(KeGetCurrentIrql() == DISPATCH_LEVEL &&
Timeout && Timeout->QuadPart == 0));
/* Check if the lock is already held */
//如果还没有使用 KeAcquireDispatcherDatabaseLock 锁定,那就跳转到进行锁定的代码
if (!Thread->WaitNext) goto WaitStart;
/* Otherwise, we already have the lock, so initialize the wait */
Thread->WaitNext = FALSE;
//线程每一次唤醒后,等待队列都可能发生了变化,必须调用这个函数进行重新构造等待块
KxSingleThreadWait(); //这是一个宏,用来构造当前线程的等待块链表
/* Start wait loop */
//循环,每次临时唤醒切回来时的入口
for (;;)
{
/* Disable pre-emption */
Thread->Preempted = FALSE; //因等待而主动放弃的cpu,不是被抢占
/* Check if a kernel APC is pending and we're below APC_LEVEL */
//if进入等待态前的原irql是APC_LEVEL,每次切回来时会自动在KiSwapContextInternal执行掉所有内核APC。
//if进入等待态前的原irql是APC_LEVEL,则用下面的语句手动执行掉所有内核APC。
//总之,确保每次切回来时,总是会先执行掉所有内核APC
if ((Thread->ApcState.KernelApcPending) && !(Thread->SpecialApcDisable) &&
(Thread->WaitIrql < APC_LEVEL))
{
/* Unlock the dispatcher */
//执行所有Pending中的内核APC
KiReleaseDispatcherLock(Thread->WaitIrql);
}
//每轮循环中(即每次切回来时),测试是否条件成熟,可以退出睡眠。可以退出的3种条件为:
//1. 所等待的对象有信号了
//2. 可以被强制唤醒了
//3. 超时了
//上面的三个条件,是决定for循环能够退出的关键,for循环也只能依靠这三个条件结束
else /*SignalState Check Begin*/
{
/* Sanity check */
ASSERT(CurrentObject->Header.Type != QueueObject);
/* Check if it's a mutant */
//先检测所等待的对象是否有信号了,不过互斥对象需要特殊判断。
if (CurrentObject->Header.Type == MutantObject)//包含Mutant结构的信号处理
{
/* Check its signal state or if we own it */
if ((CurrentObject->Header.SignalState > 0) ||
(Thread == CurrentObject->OwnerThread))
{
/* Just unwait this guy and exit */
if (CurrentObject->Header.SignalState != (LONG)MINLONG)
{
/* It has a normal signal state. Unwait and return */
//KiSatisfyMutantWait 负责把目标对象已经收到的信号消耗掉,它主要用于互斥量消耗,
//互斥量收到信号后 SignalState 会加 1, 那么所谓消耗掉,就是把 SignalState 减 1
//因为互斥量是线程可重入,所以它会对 SignalState 进行加减计算,重入时是加 1
KiSatisfyMutantWait(CurrentObject, Thread);//包含Mutant结构的信号消耗
WaitStatus = (NTSTATUS)Thread->WaitStatus;//唤醒原因
goto DontWait;//退出循环,函数准备返回
}
else
{
/* Raise an exception */
//这里就是 if (CurrentObject->Header.SignalState == (LONG)MINLONG) 条件的处理代码
//SignalState 到达 MINLONG 表示正整数溢出,变为负数,意味着这个时候锁计数已经太多了。
KiReleaseDispatcherLock(Thread->WaitIrql); //先执行 执行所有Pending中的内核APC
//重入次数已经太多了,这里只能抛出一个异常
ExRaiseStatus(STATUS_MUTANT_LIMIT_EXCEEDED);
}
}
}
else if (CurrentObject->Header.SignalState > 0)//不包含Mutant结构的信号处理
{
/* Another satisfied object */
//KiSatisfyNonMutantWait 跟 KiSatisfyMutantWait 一样,是负责把目标对象已经收到的信号消耗掉,
//Non-Mutan表明这不是一个基于互斥门所派生的对象,那么它就是基于事件或者信号量了。如果是信号量
//则把 SignalState 减 1, 如果是事件且自动重置信号,则把 SignalState 置 0 。如果是事件且手动
//重置,它是不会有操作的,读者应该通过锁特性,来理解一下为什么要这样做
//另外 KiSatisfyMutantWait 和 KiSatisfyNonMutantWait 其实是一个宏,
KiSatisfyNonMutantWait(CurrentObject);//不包含Mutant结构的信号消耗
WaitStatus = STATUS_WAIT_0;
goto DontWait;
}
/* Make sure we can satisfy the Alertable request */
//若所等等待的对象没有信号,就检查当前状态是否可被强制唤醒
//其实就是检查过是否执行过APC,如果执行过APC,那么没有信号也会被唤醒
WaitStatus = KiCheckAlertability(Thread, Alertable, WaitMode);
if (WaitStatus != STATUS_WAIT_0) break; //执行过APC,返回
/* Enable the Timeout Timer if there was any specified */
if (Timeout)
{
/* Check if the timer expired */
//当TimeOut不为空,表明需要支持超时,先检测线程上的其他定时器是否已经超时
InterruptTime.QuadPart = KeQueryInterruptTime();
if ((ULONGLONG)InterruptTime.QuadPart >= Timer->DueTime.QuadPart)
{
/* It did, so we don't need to wait */
//超时时间到达,设置返回状态后准备退出函数
WaitStatus = STATUS_TIMEOUT;
goto DontWait;
}
/* It didn't, so activate it */
//insert 标记等于 TRUE ? 准确意思不太明白
Timer->Header.Inserted = TRUE;
}
/* Link the Object to this Wait Block */
//到了这里,就说明所有的唤醒条件都不满足,需要进入睡眠状态,
//所谓睡眠,就是将线程对象放到信号对象的等待队列尾部
InsertTailList(&CurrentObject->Header.WaitListHead,
&WaitBlock->WaitListEntry);
/* Handle Kernel Queues */
//将线程从就绪队列中移除,把另外一条可就绪的线程放到队列上
if (Thread->Queue) KiActivateWaiterQueue(Thread->Queue);
/* Setup the wait information */
Thread->State = Waiting;//设置线程进入等待状态,这里只是设置标记
/* Add the thread to the wait list */
//将线程对象放置到系统等待队列中
KiAddThreadToWaitList(Thread, Swappable);
/* Activate thread swap */
ASSERT(Thread->WaitIrql <= DISPATCH_LEVEL);
//标记线程正在进行切换
KiSetThreadSwapBusy(Thread);
/* Check if we have a timer */
if (Timeout)
{
/* Insert it */
//往定时器插入超时对象, 由于已经很底层了,所以这里的超时时间只是一个LONG
//KxInsertTimer里面包含了 KiReleaseDispatcherLockFromDpcLevel
KxInsertTimer(Timer, Hand);
}
else
{
/* Otherwise, unlock the dispatcher */
//释放 LockQueueDispatcherLock锁
KiReleaseDispatcherLockFromDpcLevel();
}
/* Do the actual swap */
//这里进行线程切换,交出控制权,并切换线程上下文
//这里就是导致线程休眠的函数,因为cpu 寄存器的各个内容,都替换成了
//另外一条线程的上下文
WaitStatus = (NTSTATUS)KiSwapThread(Thread, KeGetCurrentPrcb());
/* Check if we were executing an APC */
//如果线程不是因为执行APC而被唤醒的,那么直接返回
if (WaitStatus != STATUS_KERNEL_APC) return WaitStatus;
/* Check if we had a timeout */
if (Timeout)
{
/* Recalculate due times */
//每次被唤醒后,如果还没有超时,那么距离下一次超时的时间就会缩短
//需要重新计算剩余的超时时间
Timeout = KiRecalculateDueTime(OriginalDueTime,
&DueTime,
&NewDueTime);
}
} /*SignalState Check End*/
WaitStart: //首次开始进入等待时,从这儿开始,提升irql(中断级别)
/* Setup a new wait */
//挂靠过程操作过程中禁止线程切换,将运行级别提高到SYNCH_LEVEL级别,避免产生中断
Thread->WaitIrql = KeRaiseIrqlToSynchLevel();
//线程每一次唤醒后,等待队列都可能发生了变化,必须调用这个函数进行重新构造等待块
KxSingleThreadWait();
//获取 LockQueueDispatcherLock 锁
KiAcquireDispatcherLockAtDpcLevel();
}
/* Wait complete */
//等待结束,恢复irql 等级
KiReleaseDispatcherLock(Thread->WaitIrql);
return WaitStatus;
DontWait:
/* Release dispatcher lock but maintain high IRQL */
//释放 LockQueueDispatcherLock锁
KiReleaseDispatcherLockFromDpcLevel();
/* Adjust the Quantum and return the wait status */
KiAdjustQuantumThread(Thread);//调整时间片
return WaitStatus;
}
KeWaitForSingleObject的代码,可不是一次就能看懂的,大家肯定要多看几次,我们先来了解一下必须知道的常识,再进行逻辑讲解。
首先是KMUTANT (互斥门)这个变量,上面的 KMUTANT 赋值,就是为了拿到 DISPATCHER_HEADER ,大家可能疑惑,如果是KEVENT对象的话,这个结构体的成员,是不是就过多了?是的,对于KEVENT对象,KMUTANT 结构体的成员是过多了,但是大家从 KeWaitForSingleObject 的代码中也可以看到,里面在使用 KMUTANT 对象的时候,有做类型等条件判断的,KMUTANT 的成员被访问时,会保证这个 HANDLE 的内核对象是存在这个成员的。
typedef struct _KMUTANT {
DISPATCHER_HEADER Header;
LIST_ENTRY MutantListEntry;
struct _KTHREAD *RESTRICTED_POINTER OwnerThread;
BOOLEAN Abandoned;
UCHAR ApcDisable;
} KMUTANT, *PKMUTANT, *RESTRICTED_POINTER PRKMUTANT, KMUTEX, *PKMUTEX, *RESTRICTED_POINTER PRKMUTEX;
另外大家应该也想到了,一个对象是否可等待,主要是取决于它数据结构的第一个成员,是否就是 DISPATCHER_HEADER 。而我们已经知道 KEVENT 等同于 DISPATCHER_HEADER ,所以大家想封装一个系统还没有提供的锁(比如XP没提供条件变量和读写锁),大家可以通过DISPATCHER_HEADER /KEVENT,或者应用层的CreateEvent 来实现它们(当然你用信号量等实现也可以)。
代码注释中,有两次插入阻塞队列的操作。关于阻塞队列,其实是基于对象的。就是说每个对象至少有一个阻塞队列。比如KEvent 对象里面有阻塞队列,KThread 对象里面也有阻塞队列。阻塞时,既要把 KThread 放到 KEvent 的阻塞队列中,又要把KEvent 放到 KThread 的阻塞队列中。等等,WaitForSingleObject 只有一个对象,为什么 KThread 要提供一个队列来存放?WaitForSingleObject 确实只有一个对象,但是 KThread 要兼容 WaitForMultipleObject 啊。
而线程的阻塞与唤醒,会涉及线程上下文切换概念,所谓线程上下文切换,简单来说,就是把CPU各个寄存器的值拷贝一份到当前线程对象里面,然后把下一条要执行的线程对象的寄存器备份,拷贝到CPU寄存器中,并获取指令的地址指向下一条线程对象的代码区。大家想一想,计算机运算结果虽然千变万化,但它的本质就是按照指令操作寄存器里面的值啊,现在获取指令的地址改了,寄存器的内容也全改了。CPU确实还是哪个CPU,但是CPU的内部已经全部重新装修了一遍,好比给了它一个新的家,它将执行新的线程!
还有 KiAcquireDispatcherLockAtDpcLevel 和 KiReleaseDispatcherLockFromDpcLevel 这两个函数,其实它们内部就是一个自旋锁,不了解自旋锁的同学可以简单地把它当成临界区,反正是用来保证数据同步操作的就对了。
然后我们再来说说流程逻辑。
KeWaitForSingleObject 里面支持超时等待,然而大家留意带有【Timeout】变量判断的代码,里面并没有把 Timeout 赋值到 Timer 里面,这就有点奇怪了。对Timer 赋予超时变量的,只有 【KxInsertTimer(Timer, Hand)】这句代码,其中 Hand 没有额外赋值的地方,也就是说 Hand 一直等于 0。所以目前我只能推断,KeWaitForSingleObject 是通过不断地被中断唤醒,然后计算唤醒后的时间差来判断超时的。这种方法真的好矬啊,不知道为什么要这样做,不过代码是 ReactOS 的,可能跟真正的XP有差别把,又或者是我没理解透(正常操作是会把需要超时的时间赋予定时器,当定时或其他事件到达,KeWaitForSingleObject 才会在for循环里面被唤醒的,这样性能开销更低)。而计算时间差的代码是 KiRecalculateDueTime 函数,判断时间是否超时的地方是 InterruptTime.QuadPart >= Timer->DueTime.QuadPart 。
既然超时的问题我们没办法确定,那我们先忽略超时相关的代码,看看线程如何被唤醒的吧。首先线程等待时,是处于KiSwapThread 函数中,它放弃了当前的CPU执行权限并置换了另外一条线程的上下文(如果有其他活跃线程),因为上下文被切换,当前线程把自己所有的信息都从CPU挪出来了,所以线程当然不会继续执行咯。而在KiSwapThread 函数之前,线程对象也已经把自己置于各种阻塞队列之中,部署好了自己的阻塞环境。那阻塞的KiSwapThread 函数,是如何被唤醒的呢?
唤醒的过程理解起来也很简单,就是一些类似SetEvent 之类的函数,把线程从阻塞队列移动到就绪队列,系统充当一个消费者的角色,逐个提取就绪的线程,当轮到阻塞的线程时,其上下文环境被替换到CPU中(这个时候线程所指向的代码区位置,肯定就是 KiSwapThread 函数阻塞的位置),如此线程便被唤醒了。
当然了,线程被唤醒,也是需要通过一系列的检查,确认符合真正的唤醒条件,KeWaitForSingleObject 才会返回,否则会继续进行沉睡。而判断是否符合返回条件,注释中也说明了,主要是三个条件:
- 所等待的对象有信号了
- 可以被强制唤醒了
- 超时了
其中第一点,所等待的对象有信号,其实就是锁对象把线程对象从等待队列移动到了就绪队列,并且把 SignalState 这个变量设置为大于 0 (可能当成 BOOL 类型来处理,也可能当成 LONG 类型来进行加减法处理,具体要看锁类型)。而一旦确认锁确实获得了信号要被激活,那么就要把信号消耗掉(设置为 FALSE,或者 SignalState 减 1)。
第三点超时了这个概念不难理解,而第二点的可以被强制唤醒,主要就是执行了APC回调函数被唤醒。为什么执行APC之后需要唤醒线程呢?可能是系统考虑到执行了额外的代码,可能进程业务环境发生了改变,提供给开发者一个检查和处理业务的机会吧。另外一个是如果阻塞对象被关闭,也是会被强制唤醒的。
总结:
通篇看下来,KeWaitForSingleObject 之所以会阻塞,是为它把CPU获取指令的地址置换掉了,CPU不再执行当前线程的代码,所以线程看上去陷入了沉睡。而线程被唤醒就是因为线程的指令和寄存器环境又被重新装置到CPU中。
而 KeWaitForSingleObject 函数的醒来,不代表它一定会返回,因为 KeWaitForSingleObject 还要在 for 循环中检测各种条件是否符合才会返回,如果条件不符合 KeWaitForSingleObject 还是会继续陷入沉睡。