⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄

📁 兼容内核漫谈 适合想将Windows上的程序移植到其它平台上的朋友研究查看
💻
📖 第 1 页 / 共 5 页
字号:
        if(PreviousCount) {
            _SEH_TRY . . . . . ._SEH_END;
        }
    }
    /* Return Status */
    return Status;
}[/code]
    此类函数都是先调用ObReferenceObjectByHandle(),以取得指向目标对象数据结构的指针,然后就对此数据结构执行具体对象类型的具体操作,在这里是KeReleaseSemaphore()。
    常规的V操作只使信号量加1,可以理解为只提供一张通行证,而KeReleaseSemaphore()则对此作了推广,可以使信号量加N,即同时提供好几张通行证,参数ReleaseCount就是这个增量,而PreviousCount则用来返回原来(V操作之前)的信号量数值。这里的常数IO_NO_INCREMENT定义为0,表示不需要提高被唤醒进程的调度优先级。

[code][NtReleaseSemaphore() > KeReleaseSemaphore()]

LONG  STDCALL
KeReleaseSemaphore(PKSEMAPHORE Semaphore,
                   KPRIORITY Increment,
                   LONG Adjustment,
                   BOOLEAN Wait)

{
    ULONG InitialState;
    KIRQL OldIrql;
    PKTHREAD CurrentThread;

    . . . . . .
    /* Lock the Dispatcher Database */
    OldIrql = KeAcquireDispatcherDatabaseLock();

    /* Save the Old State */
    InitialState = Semaphore->Header.SignalState;
    /* Check if the Limit was exceeded */
    if (Semaphore->Limit < (LONG) InitialState + Adjustment ||
                                        InitialState > InitialState + Adjustment) {
        /* Raise an error if it was exceeded */
        KeReleaseDispatcherDatabaseLock(OldIrql);
        ExRaiseStatus(STATUS_SEMAPHORE_LIMIT_EXCEEDED);
    }

    /* Now set the new state */
    Semaphore->Header.SignalState += Adjustment;
    /* Check if we should wake it */
    if (InitialState == 0 && !IsListEmpty(&Semaphore->Header.WaitListHead)) {
        /* Wake the Semaphore */
        KiWaitTest(&Semaphore->Header, Increment);
    }

    /* If the Wait is true, then return with a Wait and don't unlock the Dispatcher Database */
    if (Wait == FALSE) {
        /* Release the Lock */
        KeReleaseDispatcherDatabaseLock(OldIrql);
    } else {
        /* Set a wait */
        CurrentThread = KeGetCurrentThread();
        CurrentThread->WaitNext = TRUE;
        CurrentThread->WaitIrql = OldIrql;
    }
    /* Return the previous state */
    return InitialState;
}[/code]
    参数Adjustment就是信号量数值的增量,另一个参数Increment如为非0则表示要为被唤醒的线程暂时增加一些调度优先级,使其尽快得到运行的机会。还有个参数Wait的作用下面就会讲到。
    程序中KeAcquireDispatcherDatabaseLock()的作用是提升程序的运行级别(称为IRQL,以后在别的漫谈中会讲到这个问题),以禁止线程调度,直至执行与之配对的函数KeReleaseDispatcherDatabaseLock()为止。这样,在这两个函数调用之间就形成了一个“调度禁区”。可是我们从代码中看到,KeReleaseDispatcherDatabaseLock()之是否执行实际上取决于参数Wait。这是为什么呢?我在上一篇漫谈中讲到,在Windows中,当一个线程要在某个或某几个对象上等待某些事态的发生时,有两个系统调用可资调用,一个是NtWaitForSingleObject(),另一个是NtWaitForMultipleObjects()。可是其实还有一个,就是NtSignalAndWaitForSingleObject(),只是这个系统调用有些特殊。正如其函数名所示,这个系统调用一方面是“Signal”一个对象,就是对其执行类似于KeReleaseSemaphore()这样的操作;另一方面自己又立即在另一个对象上等待,类似于执行NtWaitForSingleObject();而且这二者应该是一气呵成的。这样就来了问题,如果在KeReleaseSemaphore()一类的函数中一律调用KeReleaseDispatcherDatabaseLock(),然后在NtWaitForSingleObject()中又调用KeAcquireDispatcherDatabaseLock(),那么在此二者之间就有个间隙,在此间隙中是可以发生线程调度的。再说,从程序效率的角度,那样不必要地(且不说有害)来回折腾,也是不可取的,理应加以优化。像现在这样有条件地执行KeReleaseDispatcherDatabaseLock(),就避免了这个问题。
    对信号量本身的操作倒很简单,就是改变Semaphore->Header.SignalState的数值。同时,如果有线程在睡眠等待(队列非空),并且此前信号量的数值是0,那么既然现在退还了若干张通行证(增加了信号量的数值),就可以放几个正在等待的进程进入临界区了,所以通过KiWaitTest()唤醒等待中的进程。至于KiWaitTest(),读者在上一篇漫谈中已经看过它的代码了。注意这里对于睡眠/唤醒的处理与传统的P/V操作略有些不同。在传统的P操作中,每执行一次P操作、不管能否进入临界区、都使信号量的值递减,所以信号量可以有负值,而且此时其绝对值就是正在睡眠等待的进程的数量。另一方面,当事进程之能否进入临界区也是按递减了以后的信号量数值判定的。而在NtWaitForSingleObject()、KiIsObjectSignaled()、KiSatisfyObjectWait()、以及KiWaitTest()的代码中,则当事进程只有在信号量大于0时才能获准进入临界区,这样的P操作才使信号量的值递减,否则当事进程就被挂入等待队列并进入睡眠,因此信号量不会有负值。所以,NtWaitForSingleObject()是变了形的P操作。
    如前所述,信号量既可以用来实现临界区,也可以使进程(线程)之间形成供应者/消费者的关系和互动。所以,虽然从表面上看信号量操作本身并不携带数据,但是它为高效的进程间通信提供了同步手段。另一方面,进程间同步也蕴含着信息的交换,也属于进程间通信的范畴,所以信号量同时又是一种进程间通信机制。

3. 互斥门(Mutant)
    互斥门(Mutant,又称Mutex,实现于内核中称Mutant,实现于用户空间称Mutex)是“信号量”的一个特例和变种。在信号量机制中,如果把信号量的最大值和初始值都设置成1,就成了互斥门。把信号量的最大值和初始值都设置成1,就相当于一共只有一张通行证,自然就只能有一个线程可以进入临界区;在它退出临界区之前,别的线程想要进入临界区就只好在大门口睡眠等候。所谓“互斥”,就是因此而来。我的朋友胡希明老师曾把这样的临界区比作列车上的厕所(当时他常坐火车出差,想必屡屡为此所苦),二十多年过去了,当年的学生聚在一起还会因此事津津乐道。
    不过,倘若纯粹就是两个参数的事,那就没有必要另搞一套了。事实上互斥门机制有一些特殊性,下面读者就会看到。
    为互斥门的创建和打开提供了NtCreateMutant()和NtOpenMutant()两个系统调用,代码就不用看了。下面是互斥门对象的数据结构:

[code]typedef struct _KMUTANT {
  DISPATCHER_HEADER    Header;
  LIST_ENTRY             MutantListEntry;
  struct _KTHREAD         *RESTRICTED_POINTER OwnerThread;
  BOOLEAN               Abandoned;
  UCHAR                  ApcDisable;
} KMUTANT, *PKMUTANT, KMUTEX, *PKMUTEX;[/code]
    这个数据结构的定义见之于Windows NT的DDK,所以是“正宗”的。可见,这数据结构就与信号量对象的不同。
    跟信号量机制一样,请求(试图)通过互斥门进入临界区的操作就是系统调用NtWaitForSingleObject()或NtWaitForMultipleObjects()。不过,在NtWaitForSingleObject()内部,特别是在判定能否进入临界区时所调用的函数KiIsObjectSignaled()中,其实是按不同的对象类型分别处置的。我们不妨看一下。

[code][NtWaitForSingleObject() > KeWaitForSingleObject() > KiIsObjectSignaled()]

BOOLEAN inline FASTCALL
KiIsObjectSignaled(PDISPATCHER_HEADER Object, PKTHREAD Thread)
{
    /* Mutants are...well...mutants! */
   if (Object->Type == MutantObject) {
        /*
         * Because Cutler hates mutants, they are actually signaled if the Signal State is <= 0
         * Well, only if they are recursivly acquired (i.e if we own it right now).
         * Of course, they are also signaled if their signal state is 1.
         */
        if ((Object->SignalState <= 0 && ((PKMUTANT)Object)->OwnerThread == Thread) ||
            (Object->SignalState == 1)) {
            /* Signaled Mutant */
            return (TRUE);
        } else {
            /* Unsignaled Mutant */
            return (FALSE);
        }
    }
   
    /* Any other object is not a mutated freak, so let's use logic */
   return (!Object->SignalState <= 0);
}[/code]
    可见,互斥门在这里是作为一种特殊情况处理的,使KiIsObjectSignaled()返回TRUE、从而允许当事进程进入临界区的条件之一是SignalState为1。另一个条件表明,只要是互斥门对象当前的“业主(Owner)”,就不受这个限制,即使没有通行证也可以进入。那么谁是互斥门对象当前的业主呢?那就是当前已经在此临界区中的线程,这一点读者看了下面的代码就会清楚。可是既然是已经在临界区中的线程,怎么又会企图通过同一个互斥门进入同一个临界区呢?这意味着一个线程可能递归地多次通过同一个互斥门。这个问题先搁一下,等一下再来探讨。
    在上一篇漫谈中,我们看了KeWaitForSingleObject()的代码,这是NtWaitForSingleObject()的主体,正是这个函数调用了KiIsObjectSignaled()。如果KiIsObjectSignaled()返回TRUE,那就说明当前进程可以领到通行证而进入临界区,此时需要执行KiSatisfyObjectWait(),一方面是进行“账面”上的处理,一方面也还有一些附加的操作需要进行,而这些附加的操作是因具体的对象而异的。我们再重温一下这个函数的代码。

[code][NtWaitForSingleObject() > KeWaitForSingleObject() > KiSatisfyObjectWait()]

VOID  FASTCALL
KiSatisfyObjectWait(PDISPATCHER_HEADER Object, PKTHREAD Thread)
{
    /* Special case for Mutants */
    if (Object->Type == MutantObject) {
        /* Decrease the Signal State */
        Object->SignalState--;
        /* Check if it's now non-signaled */
        if (Object->SignalState == 0) {
            /* Set the Owner Thread */
            ((PKMUTANT)Object)->OwnerThread = Thread;
            /* Disable APCs if needed */
            Thread->KernelApcDisable -= ((PKMUTANT)Object)->ApcDisable;
            /* Check if it's abandoned */
            if (((PKMUTANT)Object)->Abandoned) {
                /* Unabandon it */
                ((PKMUTANT)Object)->Abandoned = FALSE;
                /* Return Status */
                Thread->WaitStatus = STATUS_ABANDONED;
            }
            /* Insert it into the Mutant List */
            InsertHeadList(&Thread->MutantListHead,
                                    &((PKMUTANT)Object)->MutantListEntry);
        }
    } else if ((Object->Type & TIMER_OR_EVENT_TYPE) == EventSynchronizationObject) {
        /* These guys (Syncronization Timers and Events) just get un-signaled */
        Object->SignalState = 0;
    } else if (Object->Type == SemaphoreObject) {
        /* These ones can have multiple signalings, so we only decrease it */
        Object->SignalState--;
    }
}[/code]
    我们只看对于互斥门对象的处理。首先是递减SignalState,这就是所谓“账面”上的处理,也是P操作的一部分。由于前面已经通过KiIsObjectSignaled()进行过试探,如果当时的SignalState数值为1,或者说如果当时的临界区是空的,那么现在的SignalState数值必定变成了0。所以,下面if语句中的代码是在一个线程首次进入一个互斥门时执行的。这里说的“首次进入”并不是指退出以后又进去那样的反复进出中的首次,而是指嵌套多次进入中的首次。当一个线程首次顺利进入互斥门时,它就成了这个互斥门当前的业主,直至退出;所以把互斥门数据结构中的OwnerThread字段设置成指向当前线程的KTHREAD数据结构。此外,如果Abandoned字段显示这个互斥门行将被丢弃,则暂时将其改成继续使用(因为又有线程进来了),但是把这情况记录在当前线程的KTHREAD数据结构中。互斥门数据结构中的ApcDisable字段表明通过互斥门进入临界区的线程是否需要关闭APC请求,现在当前进程通过了互斥门,所以要把这信息记录在它的数据结构中。注意这里是从Thread->KernelApcDisable的数值中减去互斥门的ApcDisable的值,结果为非0(负数)表示关闭APC请求,而ApcDisable的值则非1即0。举例言之,假定Thread->KernelApcDisable原来是0,而ApcDisable为1,则相减以后的结果为-1,表示关闭APC请求。最后,当前进程既已成为这个互斥门的主人,二者之间就有了连系,所以通过队列把它们结合起来,这是因为一个线程有可能同时存在于几个临界区中。
    应该说这里别的都还好理解,成为问题的是为什么要允许嵌套进入互斥门。据“Programming the Microsoft Windows Driver Model”书中说,互斥门的特点之一就是允许嵌套进入,而优点之一则是可以防止死锁。书中并没有明确讲这二者之间是否存在因果关系,所以我们只能分析和猜测。首先,如过互斥门不允许嵌套进入(在前面的代码中取消允许当前业主进入的条件),而已经通过互斥门进入临界区的线程又对同一个互斥门进行P操作,那么肯定是会引起死锁的。这个线程会因为在P操作中不能通过互斥门而进入睡眠,能唤醒其睡眠的是已经在这个临界区中的线程(如果它执行V操作的话),可是这正是已经在睡眠等待的那个线程本身,所以就永远不会被唤醒。反之,有了前面KiIsObjectSignaled()中那样的安排,即允许互斥门当前的业主递归通过,那确实就可以避免由此而导致的死锁。
    可是,为什么要企图递归通过同一个互斥门呢?既然已经通过这个具体的互斥门进入了临界区,为什么还要再一次试图进入同一个互斥门呢?应该说,在精心设计和实现的软件中是不应该有这种情况出现的。可是,考虑到应用软件的可重用(reuse),有时候也许会有这种情况。例如,一个线程在临界区内可能调用某个软件模块所提供的操作,而这个软件模块可能需要通过NtWaitForMultipleObjects()进入由多个互斥门保护的复合临界区,可是其中之一就是已经进入的那个互斥门。在这种情况下,对于已经进入的那个互斥门而言,就构成了递归进入。当然,我们可以通过修改那个软件模块来避免此种递归,但这可能又不是很现实。在这样的条件下,允许递归进入不失为一个简单的解决方案。
    还要说明,允许递归通过互斥门固然可以防止此种特定形式的死锁,却并不是对所有的死锁都有效。真要防止死锁,还是得遵守有关的准则,精心设计,精心实现。
    读者也许会问:这里所引的代码出自ReactOS,所反映的是ReactOS的作者们对Windows互斥门的理解,但是他们的理解是否正确呢?确实,Windows的代码是不公开的,所以也无从对比。可是,虽然Windows的代码不公开,它的一些数据结构的定义却是公开的,这里面就包括KMUTANT,所以前面特地说明了这是来自Windows DDK(其实ReactOS的许多数据结构都可以在DDK中找到)。既然我们知道互斥门允许递归进入,又看到KMUTANT中确有OwnerThread这个指针,那么我们就有理由相信ReactOS的这些代码离“真相”不会太远。当然,我们还可以、也应该、设计出一些实验来加以对比、验证。

    再看从临界区退出并交还“通行证”的操作、即V操作,这就是系统调用NtReleaseMutant()。

[code]NTSTATUS  STDCALL
NtReleaseMutant(IN HANDLE MutantHandle, IN PLONG PreviousCount  OPTIONAL)
{
    PKMUTANT Mutant;
    KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
    NTSTATUS Status = STATUS_SUCCESS;
  
    . . . . . .
    if(PreviousMode == UserMode && PreviousCount) {
        _SEH_TRY . . . . . . _SEH_END;
        if(!NT_SUCCESS(Status)) return Status;
    }
    /* Open the Object */
    Status = ObReferenceObjectByHandle(MutantHandle, MUTANT_QUERY_STATE,
                       ExMutantObjectType, PreviousMode, (PVOID*)&Mutant, NULL);

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -