NT内核代码分析

晨曦之光 发布于 2012/04/10 15:01
阅读 433
收藏 0

某种意义上nt内核用irql解决了一切,这正是由于它有一个基本的,核心的支撑。这就是它的抽象中断框架---IRQL。在linux中,我们知道,如 果我要知道能否调用schedule进行调度,那么我必须知道还有什么数据结构处在临界区,这个时候是否适合抢占,等等,当然内核的数据结构已经做得很好,我们做开发的不用考虑那么多,比如用spin_lock啦,用preempt_disable啦,但是当你读到这些横七竖八的加锁,禁中断,一会又开锁,一会又锁页面...我估计有几种人会疯狂的:1.c语言基础不好的人;2.没有耐心的人;3.在读源码方面没有激情的人;.....剩下的就是 linux内核社区的人了,呵呵

再看看windows的代码,虽然都是反汇编得到的,但是就是不看c代码,直接看汇编吗,也是很容易的。

nt系统的总体框架做得异常威猛,它能不与硬件交互就尽量不与硬件交互,把整个系统抽象到了一个很高的级别,比如,它为每一个cpu都抽象出一个结构体, 记录此cpu的一些信息,比如当前线程,当前进程,切换次数...,这个数据结构连接到此cpu上的所有进程控制块。相比之下,linux就简陋多了,只 是每个cpu一个current宏,还有一系列以cpu为索引的每cpu变量,这些结构没有像nt内核里做得那样:高内聚。

nt内核引入irql完全是为了异步性操作,这就是为什么nt一开始就是为异步而存在,在这点上,异步性是设计理念,而irql是实现方案。中断是一个百 分之百的异步性操作,你预测不到它下一次何时到来(时钟中断除外),所以nt内核将所有的操作都像中断的概念抽象,因为中断的不确定---异步性,那么线程上下文就是个问题了,于是ddk文档中屡次提到“任意上下文”。这就是说,只要你把操作(执行绪)抽象成了中断---拥有了passtive以上的中断 优先级,那么你就不要指望会在特定的上下文里面执行你的操作,这就是完全异步的概念。实际上,nt内核可能受到了unix进程的影响,所以线程上下文大多 数情况在被动中断请求级别运行,它们拥有上下文,而且叫做“进程”,“线程”,系统里面的执行绪,它们比较特殊,而且不那么异步,可以说,一切都为线程服 务,这可能是唯一那么一点不对称的地方吧

nt内核的另一大特性就是对象化,几乎所有的模块都纳入对象化管理范畴,比如io管理器,进程管理器之类的,实际上,irql和对象化是有联系的,前者着重于执行,后者着重于管理,中断本身就是一种对象,我们看看一个dpc对象如何触发一个dpc中断:

代码及注释为引用:

BOOLEAN KeInsertQueueDpc (IN PRKDPC Dpc, IN PVOID SystemArgument1,IN PVOID SystemArgument2)

{

  PKSPIN_LOCK Lock;

  KIRQL OldIrql;

  KeRaiseIrql(HIGH_LEVEL, &OldIrql); // 提升当前IRQL到最高

  PKPRCB = KeGetCurrentPrcb(); // 获取当前处理器控制块

  if ((Lock = InterlockedCompareExchangePointer(&Dpc->Lock, ⪻cb->DpcLock, NULL)) == NULL)

  {

    Prcb->DpcCount += 1;

    Prcb->DpcQueueDepth += 1;

    Dpc->SystemArgument1 = SystemArgument1;

    Dpc->SystemArgument2 = SystemArgument2;

    if (Dpc->Importance == HighImportance)

      InsertHeadList(⪻cb->DpcListHead, &Dpc->DpcListEntry);

    else

      InsertTailList(⪻cb->DpcListHead, &Dpc->DpcListEntry);

// 如当前处理器没有DPC对象活动或DPC中断请求,则判断是否发出DPC中断请求

    if (Prcb->DpcRoutineActive == FALSE && Prcb->DpcInterruptRequested == FALSE)

    {

     // 如果DPC对象优先级为中高;

     // 或者DPC队列长度超过阈值MaximumDpcQueueDepth;

     // 或者DPC请求速率小于阈值MinimumDpcRate

       if ((Dpc->Importance != LowImportance) ||

           (Prcb->DpcQueueDepth >= Prcb->MaximumDpcQueueDepth) ||

           (Prcb->DpcRequestRate < Prcb->MinimumDpcRate))

       {

          Prcb->DpcInterruptRequested = TRUE;

          KiRequestSoftwareInterrupt(DISPATCH_LEVEL);

        }

     }

  }

  KeLowerIrql(OldIrql);

  return (Lock == NULL);

}

看看这个代码多么清晰:KeRaiseIrql(HIGH_LEVEL, &OldIrql)一句话就可以代替一大堆锁操作,对称位置有一个KeLowerIrql,又将优先级降下。这个函数仅仅将一个dpc请求排入相关cpu的dpc队列,只有在满足一定情况的情况下才触发此“异步”的,软件的中断,真正触发语句就是:KiRequestSoftwareInterrupt,那么这个函数做什么呢?难道仅仅将dpc入队不行吗?根据irql的总原则,任何小于或等于当前irql的执行绪都无法抢占当前执行绪,那么KiRequestSoftwareInterrupt要做的就是拿KiRequestSoftwareInterrupt 要求的irql和当前的比较,如果大于当前的,则立即执行dpc链表的例程,如果小于或等于,那么就设置dpc为pending,意思是说我已经触发了, 只是你没有执行,我没有你重要,那么我先记下来这笔帐,等你忙完了再管我把。

升降irql是nt内核很重要的部分,它实际上控制着一切,当硬件中断来临时,系统做任何事情的可能性都有,进入硬中断前,先作前置工作,调用 HalBeginSysteminterrupt,将irql提高到对应优先级,但是在硬件中断完毕以后,还要end操作将irql下降到原来的优先级, 在下降之前要看看原来的优先级是不是在dispatch级别之上,如果是,就直接归位,如果不是就看看是否需要切换线程,并且看看有没有dpc被触发,重 点类似于下列代码的加粗部分:

代码摘录:

VOID  FASTCALL  KiUnlockDispatcherDatabase ( IN KIRQL OldIrql)

{

    KIRQL CurIrql;

    UCHAR IsApcPending;

    KTHREAD CurrentThread ,NextThread;

    KPCR SelfPcr ;

    if( KeGetPcr()->NextThread)

    {

if(OldIrql >= DISPATCH_LEVEL)

        {

            If(KeGetPcr()->RegisterArea[0x74] != 0)

            {

                 KfLowerIrql(OldIrql );

            }

            else

            {

                HalRequestSoftwareInterrupt(DISPATCH_LEVEL);

                KfLowerIrql(OldIrql );

            }

        }

        else

        {

                SelfPcr = KeGetPcr()->SelfPcr;

                NextThread =  KeGetPcr()->NextThread;

                //从链表中删除CurrentThread

                CurrentThread  = KeGetPcr()->CurrentThread;

                KeGetPcr()->CurrentThread = KeGetPcr()->NextThread;

                KeGetPcr()->NextThread = 0;

                CurrentThread->WaitIrql = OldIrql;

                CurrentThread->IdleSwapBlock = 1;

                KiReadyThread(CurrentThread);

               /*SwapContext 函数:

;   cl - APC interrupt bypass disable (zero enable, nonzero disable)

;   edi - Address of previous thread.

;   esi - Address of next thread.

;   ebx - Address of CR.

; Return value:

;   al - Kernel APC pending.

;   ebx - Address of CR.

;   esi - Address of current thread object.

               */

                _asm

               {

                    mov     ebx,SelfPcr ;

                  mov     esi,NextThread ;

                    mov     edi,CurrentThread;

                    mov     cl,OldIrql

                    SwapContext(); //呵呵,卡巴斯基detour了这个函数。

                    mov IsApcPending,al

                    mov CurrentThread,esi

               }

               CurIrql = CurCurrentThread->WaitIrql ;

               if(IsApcPending)

               {

CurIrql = APC_LEVEL;

                KfLowerIrql(CurIrql );

                KiDeliverApc(0,0,0);

                CurIrql = 0;

                KfLowerIrql(CurIrql );

               }

               else

               {

                  KfLowerIrql(CurIrql );

               }

        }

    }

    else

    {

          KfLowerIrql(OldIrql );

    }

}

但是,我为什么举这段代码的例子呢?实际上KfLowerIrql 也是这么检查的,因为软中断毕竟不像硬件中断那样真的异步触发,它最终还是靠代码来实现触发的,而且软件虚拟中断并不被硬件支持,所以类似于轮询的机制,内核中就必须有一些检查点,检查看有没有更高优先级的虚拟中断,这些检查点包括KfLowerIrql,KiUnlockDispatcherDatabase等等...

这么看来,如果在硬件中断到来之前,当前cpu已经在dispatch_level了,那么硬件中断完毕后就不会执行dpc过程,而是会返回到原来的环境。那么现在可以谈谈nt内核的调度机制了,这里只是说机制,并不提算法,比算法,nt倒是没法和linux和solaris比了,呵呵(还是有所偏好啊),这里只谈结构。nt的调度代码运行在dispatch_level上,这是因为nt是个完全抢占式的内核,而且它所谓的抢占不仅仅是基于线程优先 级,还有一个中断请求级(irql),如果调度代码在被动优先级运行,那么别的线程代码都可能抢占它,要想不被抢占就要加锁禁止抢占等,但是那就到 linux的方法上去了,所以,调度代码必须在一个很高的中断优先级上运行,但是又不能太高,于是选择了dispatch级别,这个名称就是由他而来的, 这样,只要高于或等于这个级别的代码在执行时,完全不可能切换线程,达到了锁定一些东西的目的,另外,缺页处理也在这个级别,道理和此一样,但是高于等于此优先级的代码就不能用分页内存了,这是一个弊端(缺页在dispatch以上不能发生的原因实际是缺页要进行磁盘io,而磁盘io的开始例程在 dispatch级别进行,分发例程在被动级别运行,可能还要睡眠,而进入缺页就到了dispatch级别,睡眠了进程不能切换,系统崩溃,还有即使不睡 眠,那么磁盘io也不能抢占缺页处理),继续说调度,那么,什么时机进行调度呢?

nt内核是可抢占的,任何时候只要新的线程被加入运行队列,那么它的线程优先级就可能被重新安排,如果有优先级比当前线程大的,就切换,这一切都是由dispath_level的中断进行的,也就是说,如果我现在想中断一个线程的执行,我只需要发起一个KiRequestSoftwareInterrupt请求,请求一个调度就可以了。

注意上面代码中的KfLowerIrql调用,这是个很重要的调用,用于降低当前cpu的优先级到参数的位置,在降低之前要派发一个比这个要降到的级别高的中断,如果有的话(没有就算了)。另外HalEnd{System|Software}Interrupt都会派发pending的未决的中断。就像HalEndSystemInterrupt一样,它就会派发dpc过程。下面摘录一句话:HalEndSystemInterrupt 向本地APIC的EOI寄存发送0,表示中断结束,可以接收新中断。并还要判断要降到的IRQL是否小于 DISPATCH_LEVEL,若小于则进一步判断KPCR+0x96(0xffdff096)是否置位,若置位则表示有DPC中断在等待(在IRQL高 于DISPATCH_LEVEL被引发,然后等待直到IRQL降到低于DISPATCH_LEVEL),则将KPCR+0x95和KPCR+0x96清0 后调用KiDispatchInterrupt响应DPC软中断。否则做的工作就是和HalBeginSystemInterrupt一样的过程:把要降 到的IRQL转换成任务优先级设置TRP,并把久的任务优先级转成IRQL返回。
这就是irql的工作过程,微软为了实现完全异步在现实中找了这么好个异步的例子---中断,于是,他们将整个系统的所有行为模拟成中断,并且虚拟出一个中断控制器(类似于硬件中断控制器),里面有完整的屏蔽寄存器以及和硬件中断控制器一样的优先级管理机制等等。


原文链接:http://blog.csdn.net/dog250/article/details/5303593
加载中
返回顶部
顶部