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

📄

📁 兼容内核漫谈 适合想将Windows上的程序移植到其它平台上的朋友研究查看
💻
📖 第 1 页 / 共 5 页
字号:
        KeReleaseDispatcherDatabaseLock(OldIrql);
    } else {
        /* Return Locked and with a Wait */
        KTHREAD *Thread = KeGetCurrentThread();
        Thread->WaitNext = TRUE;
        Thread->WaitIrql = OldIrql;
    }
    /* Return the previous State */
    return PreviousState;
}[/code]
    参数Increment和Wait所起的作用与前面KeReleaseSemaphore()中的相同。所谓“设置事件”其实就是一种特殊的、变通的V操作。具体的操作分两种情况:
    1. 如果IsListEmpty()为真,即等待队列是空的、没有线程在睡眠等待,就把Event->Header.SignalState设置成1。这样,如果此后有线程对此事件对象执行P操作、即NtWaitForSingleObject(),就因此而不必睡眠等待。这对于通知型和同步型的事件对象都是一样。
    2. 已经有线程在这个对象上睡眠等待,那就要从中唤醒一个或所有线程。这时候的处理取决于事件对象的类型以及等待的方式:
    l 对于通知型的事件对象,或者等待者的等待方式是WaitAll,而且此前SignalState为0,就将SignalState置1,并通过KiWaitTest()唤醒这个线程,以及等待队列中所有符合条件的线程。可是,要是SignalState本来就已经是1,则没有任何影响。
    l 否则,对于同步型的事件对象,并且等待者的等待方式是WaitAny,就通过KiAbortWaitThread()唤醒等待队列中的第一个线程。此时并不改变SignalState的值。因为既然唤醒了一个线程,就已经把这筹码消耗掉了。
    这里KiWaitTest()和KiAbortWaitThread()的区别在于:KiWaitTest()是在一个while循环中对等待队列中的所有进程执行KiAbortWaitThread(),条件是SignalState大于0。对于信号量,由于每唤醒一个线程就使SignalState减1,这循环很快就停止了,一般是只唤醒一个线程。但是如前所述,通知型事件对象在唤醒一个线程的时候不改变SignalState的值。于是,这个while循环就会唤醒等待队列中的所有进程。不过这里也有例外,如果其中的某个线程是在多个“可等待对象”上等待,而且等待方式是WaitAll,那就还要看是否别的条件也满足了,不然就只好把它跳过,这也是KiWaitTest()的代码中按排好了的。

    前面讲过,一旦将通知型事件对象的SignalState设置成1,它就一直保持为1,P操作不会改变它的值。即使再对其执行一次KeSetEvent(),也不会改变它的值,因为本来就已经是1了。为了使其变成0,以便再次使用这个事件对象,就需要对其执行另一个系统调用NtResetEvent()。同样,NtResetEvent()的主体是KeResetEvent()。

[code][NtSetEvent() > KeResetEvent()]

LONG  STDCALL
KeResetEvent(PKEVENT Event)
{
    KIRQL OldIrql;
    LONG PreviousState;
   
    DPRINT("KeResetEvent(Event %x)\n",Event);
   
    /* Lock the Dispatcher Database */
    OldIrql = KeAcquireDispatcherDatabaseLock();
   
    /* Save the Previous State */
    PreviousState = Event->Header.SignalState;
   
    /* Set it to zero */
    Event->Header.SignalState = 0;
   
    /* Release Dispatcher Database and return previous state */   
    KeReleaseDispatcherDatabaseLock(OldIrql);
    return PreviousState;
}[/code]
    解释就没有必要了。注意调用NtResetEvent()的不必就是NtSetEvent()的调用者,而可以是别的线程或内核模块。不过有些内核模块只能调用KeResetEvent(),而不是NtResetEvent()。
    Windows还有个系统调用NtPulseEvent(),这是把NtSetEvent()和NtResetEvent()组合在了一起,相当于先NtSetEvent()、然后马上就NtResetEvent()。这样,其效果就是唤醒已经在通知型事件对象上等待的所有线程,但是下不为例。而对于同步型事件对象则大致等同于NtSetEvent()。
    可见,虽然“事件”实质上是“信号量”的一种特例和变种,但是在使用上却有着明显的差别。信号量的“正宗”的用途是构筑临界区。在这种应用中,一个线程得以通过P操作进入临界区的原因可能是有另一个线程执行了V操作,但是既然进了临界区就总有从临界区退出而执行V操作的时候。这样,一个线程在P操作以后总是有个V操作。从总体上看,每个线程的P操作和V操作是平衡的、即数量相等的。但是“事件”则不同,“事件”并不是用来构筑临界区、而纯粹是用于线程间同步的。在这里,等待事件发生的一方总是执行P操作,而发出事件通知的一方则总是执行V操作。在前面对于“信号量”的比喻中把P操作比作领取通行证,把V操作比作交还通行证。相比之下,对于“事件”则相当于领取的通行证从来不交还,而另有供应者在不时地提供新的通行证。而且,特别有意义的是,发出事件通知的一方还不必非得是一个线程,也可以是内核中的某些子系统,例如设备驱动,所以也可以用于线程与内核之间的同步,特别是广泛地应用于设备驱动。当然,发出事件通知的一方更不必局限于某一个特定的线程,而是任何一个线程都可以。
    为了帮助读者加深对事件机制的理解,下面是一个内核线程DebugLogThreadMain的代码:

[code]VOID STDCALL
DebugLogThreadMain(PVOID Context)
{
  KIRQL oldIrql;
  IO_STATUS_BLOCK Iosb;
  static CHAR Buffer[256];
  ULONG WLen;

  for (;;)
    {
      LARGE_INTEGER TimeOut;
      TimeOut.QuadPart = -5000000; /* Half a second. */
      KeWaitForSingleObject(&DebugLogEvent, 0, KernelMode, FALSE, &TimeOut);
      KeAcquireSpinLock(&DebugLogLock, &oldIrql);
      while (DebugLogCount > 0)
      {
        if (DebugLogStart > DebugLogEnd)
        {
            WLen = min(256, DEBUGLOG_SIZE - DebugLogStart);
            memcpy(Buffer, &DebugLog[DebugLogStart], WLen);
            Buffer[WLen + 1] = '\n';
            DebugLogStart =  (DebugLogStart + WLen) % DEBUGLOG_SIZE;
            DebugLogCount = DebugLogCount - WLen;
            KeReleaseSpinLock(&DebugLogLock, oldIrql);
            NtWriteFile(DebugLogFile, NULL, NULL, NULL, &Iosb, Buffer, WLen + 1,
                        NULL, NULL);
        }
        else
        {
            WLen = min(255, DebugLogEnd - DebugLogStart);
            memcpy(Buffer, &DebugLog[DebugLogStart], WLen);
            DebugLogStart =
                   (DebugLogStart + WLen) % DEBUGLOG_SIZE;
            DebugLogCount = DebugLogCount - WLen;
            KeReleaseSpinLock(&DebugLogLock, oldIrql);
            NtWriteFile(DebugLogFile, NULL, NULL, NULL, &Iosb, Buffer, WLen,
                        NULL, NULL);
          }
        KeAcquireSpinLock(&DebugLogLock, &oldIrql);
      }
      KeResetEvent(&DebugLogEvent);
      KeReleaseSpinLock(&DebugLogLock, oldIrql);
    }
}[/code]

    这个内核线程是为内核调试日志(Log)服务的。内核中有个环形缓冲区DebugLog[],以及用作该数组下标的变量DebugLogStart和DebugLogEnd,还有表示环形缓冲区中数据长度的变量DebugLogCount。不管是哪一个线程,只要是进入了内核,如果需要在日志中写上一笔,就可以把字符串拷贝到这个环形缓冲区中,然后要求这个内核线程把内容写到一个日志文件中。为此当然需要同步,这是通过一个(同步型)事件对象DebugLogEvent达成的。由于是在内核中,所以这里对事件对象的操作都直接调用其内核版本,例如KeResetEvent()、而不是NtResetEvent()。此外,对于环形缓冲区的使用当然还需要互锁,这是通过“空转锁”DebugLogLock实现的,不过那不是我们此刻所关心的。
    每当需要生成一项日志时,可以调用DebugLogWrite():

[code]VOID
DebugLogWrite(PCH String)
{
  KIRQL oldIrql;

   . . . . . .
   KeAcquireSpinLock(&DebugLogLock, &oldIrql);

   if (DebugLogCount == DEBUGLOG_SIZE)
   {
      DebugLogOverflow++;
      KeReleaseSpinLock(&DebugLogLock, oldIrql);
      if (oldIrql < DISPATCH_LEVEL)
      {
        KeSetEvent(&DebugLogEvent, IO_NO_INCREMENT, FALSE);
      }
       return;
   }

   while ((*String) != 0)
   {
      DebugLog[DebugLogEnd] = *String;
      String++;
      DebugLogCount++;

      if (DebugLogCount == DEBUGLOG_SIZE)
      {   
        DebugLogOverflow++;
        KeReleaseSpinLock(&DebugLogLock, oldIrql);
        if (oldIrql < DISPATCH_LEVEL)
        {
           KeSetEvent(&DebugLogEvent, IO_NO_INCREMENT, FALSE);
        }
        return;
      }
      DebugLogEnd = (DebugLogEnd + 1) % DEBUGLOG_SIZE;
   }

    KeReleaseSpinLock(&DebugLogLock, oldIrql);

    if (oldIrql < DISPATCH_LEVEL)
    {
      KeSetEvent(&DebugLogEvent, IO_NO_INCREMENT, FALSE);
    }
}[/code]

    对于这段代码,以及对于DebugLogWrite()和DebugLogThreadMain()之间怎样互动,这里就不作解释了。只是要指出:DebugLogWrite()的每次执行可能都在不同线程的上下文里、代表着不同的线程,因为任何线程都可以调用DebugLogWrite()。另外,想必读者已经注意到,KeResetEvent()是由DebugLogThreadMain()自己调用、而不是由别的线程调用的。

    介绍完事件对象,还应该提一下,Windows还有一种特殊的“事件对(EventPair)”对象。与此有关的系统调用有这么一些:
[code]   NtCreateEventPair()
    NtOpenEventPair()
    NtWaitHighEventPair()
    NtWaitLowEventPair()
    NtSetHighWaitLowEventPair()
    NtSetLowWaitHighEventPair()
    NtSetHighEventPair()
    NtSetLowEventPair()[/code]

    顾名思义,“事件对”就是把两个事件对象紧密地组合在一起。事实上也正是如此,一个事件对由“高”、“低”两个事件对象组合构成,其设计意图是用于“点对点”的双向进程间通信。实际上这是为提高Windows进程与服务进程Csrss之间的通信效率而设置的(Csrss是Windows子系统的管理/服务进程)。早期的csrss承担着许多操作,Windows进程与Csrss之间的通信非常频繁,所以其效率至关重要。这种进程间通信的典型情景就是一方唤醒另一方、自身却又进入睡眠,反过来等待被对方唤醒,就像打乒乓球一样,为此就专门设计了NtSetHighWaitLowEventPair()和NtSetLowWaitHighEventPair()两个系统调用。不仅如此,为了尽可能地提高效率(在这种情况下的优化甚至是以CPU的时钟周期数计算的),还专门单独分配了两个中断向量0x2B和0x2C,而不跟别的系统调用合用0x2E。不过,后来Csrss的许多操作被移到了内核中,不再需要那么频繁的进程间通信了,因而在效率上的容忍度也宽松了一些,所以现在又回到了0x2E,而不再使用0x2B和0x2C这两个中断向量。

5. 命名管道(Named Pipe)和信箱(Mail Slot)
    前面提到,如果从字面上理解,那么进程间通信也可以通过磁盘文件而实现。但是,把信息写入某个磁盘文件,再由另一个进程从磁盘文件读出,在速度上是很慢的。固然,由于文件缓冲区(Cache)的存在,对磁盘文件的写和读未必都经过磁盘,但是那并没有保证。再说,普通的文件操作也没有提供进程间同步的手段。所以通过普通的磁盘文件实现进程间通信是不太现实的。但是这也提示我们,如果能实现一种特殊文件,使得对文件的读写只在缓冲区中进行(而不写入磁盘),并且实现进程间的同步,那倒是个不坏的主意。命名管道就是这样一种特殊文件。实际上,命名管道还不仅是这样的特殊文件,它还是一种网络通信的机制,只是当通信的双方存在于同一台机器上时,才落入本文所说的进程间通信的范畴。
    既然命名管道是一种特殊文件,它的创建、打开、读写等等操作就基本上都可以利用文件系统中的有关资源加以实现。当然,这毕竟是一种特殊文件,对于使用者来说,最大的特殊之处在于这是一个“先进先出”的字节流,不能对其执行lseek()一类的操作。
    先看命名管道的创建,Windows的Win32 API上提供了一对库函数CreateNamedPipeA()和CreateNamedPipeW(),前者用于ASCII码字符串,后者用于“宽字符”即Unicode的字符串,实际上前者只是把8位字符转换成Unicode以后再调用后者。对CreateNamedPipeW()的调用大致如下:

[code]  Handle = CreateNamedPipeW(L"[A]\\\\.\\pipe\\MyCont

⌨️ 快捷键说明

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