多线程锁详解之【WaitForSingleObject】

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

正文:
先看看实现源码吧:


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回调函数,要记住以下三点:

  1. APC回调函数,跟 WaitForSingleObjectEx函数是处于同一线程的。
  2. APC回调函数执行完后,WaitForSingleObjectEx 马上返回,不会检查用户设置的超时时间。
  3. 用户态线程处于 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 才会返回,否则会继续进行沉睡。而判断是否符合返回条件,注释中也说明了,主要是三个条件:

  1. 所等待的对象有信号了
  2. 可以被强制唤醒了
  3. 超时了

其中第一点,所等待的对象有信号,其实就是锁对象把线程对象从等待队列移动到了就绪队列,并且把 SignalState 这个变量设置为大于 0 (可能当成 BOOL 类型来处理,也可能当成 LONG 类型来进行加减法处理,具体要看锁类型)。而一旦确认锁确实获得了信号要被激活,那么就要把信号消耗掉(设置为 FALSE,或者 SignalState 减 1)。

第三点超时了这个概念不难理解,而第二点的可以被强制唤醒,主要就是执行了APC回调函数被唤醒。为什么执行APC之后需要唤醒线程呢?可能是系统考虑到执行了额外的代码,可能进程业务环境发生了改变,提供给开发者一个检查和处理业务的机会吧。另外一个是如果阻塞对象被关闭,也是会被强制唤醒的。

总结:
通篇看下来,KeWaitForSingleObject 之所以会阻塞,是为它把CPU获取指令的地址置换掉了,CPU不再执行当前线程的代码,所以线程看上去陷入了沉睡。而线程被唤醒就是因为线程的指令和寄存器环境又被重新装置到CPU中。

而 KeWaitForSingleObject 函数的醒来,不代表它一定会返回,因为 KeWaitForSingleObject 还要在 for 循环中检测各种条件是否符合才会返回,如果条件不符合 KeWaitForSingleObject 还是会继续陷入沉睡。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值