探究Windows内核你知多少

Windows内核


      如上所述,现代操作系统的一个明显特征就是用户空间和系统空间的划分,从UNIX时代以来,人们一直把存在于系统空间的代码和数据的集合称为“内核(Kernel)”,因此内核是有明确边界的。空间的不同,或者说CPU运行模式(系统态和用户态)的不同,是不会被混淆的本质区别。可是,在Windows的术语中却不同,微软并不把系统空间的所有代码和数据的集合称为内核,而是把这里面的一部分,即比较低层、与硬件靠得最近因而最为核心的一部分称为“内核”,即Kernel。实际上,这也反映了当初微软在决策上的举棋不定,因为微软称为Kernel的那一部分大致上相当于一个微内核。有些资料甚至据此而认定Windows内核为“微内核”,殊不知现今的Windows内核恐怕是最宏的“宏内核”,因为连图形界面和视窗机制的实现也在内核里面了。微软的文献把同在系统空间但在所谓“Kernel”以上的部分称为Executive。国内有些资料把Executive译为“执行体”,其实是不妥当的。试想,有“执行体”,莫非还有“不执行体”吗?实际上,在英语中,特别是在企业管理的语境中,Executive是“管理层”、“企业高管”的意思,所以微软其实是把内核分成了两大层,其中的低层或者说核心部分称为“内核”,而高层则称为“管理层”。管理层中有些什么呢?读者在后面将看到,里面有“对象管理”、“内存管理”、“进程/线程管理”、“I/O管理”、“安全管理”、“进程(线程)间通信”等模块。但是,这么一来,所谓“内核”的边界就变得不很清晰了。
      相比之下,还是像UNIX/Linux那样,以系统空间与用户空间的划分为界,把存在于系统空间的所有成分的集合统称为内核比较清晰,也更为科学(因为有明确的判定方法)。所以,在本书中,只要没有特别加以说明,“内核”就是“系统空间”的同义词,而不是特指微软所称的那个内核;而微软所称的内核,则在本书中称为内核中的“核心层”。内核中从高到低在逻辑上分成若干层次,这一点任何操作系统的内核都是如此。事实上,研究操作系统的几种观点(视野)之一就是分层模拟、分层提供服务的观点。即便是微内核的内部,也还是分出层次,其中最底层的就是“硬件抽象层(Hardware Abstraction Layer)”HAL。
    不过操作系统的概念有狭义和广义之分。狭义的操作系统就是指内核,而广义的操作系统则并不只是一个内核,也包括一些用户空间的软件。例如,一些工具性的软件、实现人机界面的软件(例如Linux上的Shell,Windows上的“资源管理器”),就一般都认为属于操作系统的范畴。Windows操作系统原先的设计目标是支持三种“子系统”。其中用来实现视窗子系统的服务程序,即服务进程Csrss所执行的软件,当然也应该属于操作系统。此外,应用软件在运行时需要用到一些函数库,特别是“动态连接库”即DLL,其中靠近内核的“系统DLL”,也应属于操作系统。这样,包括应用软件在内的整个Windows系统的结构。
    图中画出了系统的结构层次,愈往上愈接近应用软件,愈往下愈接近硬件。而包括内核在内的所有中间层次的作用,则是帮助应用软件更好、更安全、更方便、更有效地利用包括CPU在内的硬件资源。
    位于最高层的是应用软件,就是一般文件扩展名为.exe的可执行程序。除.exe可执行程序之外,应用软件的开发者和提供者可以(但并非必须)选择把其中的一部分基础性的功能放在若干“动态连接库”DLL里面,这就是文件扩展名为.dll的可执行程序。DLL也是分层次的,相对高层的DLL依赖于相对低层的DLL,调用由低层DLL所提供的服务。
    在Windows系统中,应用软件是看不见操作系统内核的,应用软件通过一个“应用程序(设计)界面”Win32 API获得Windows操作系统的支持。微软通过一系列的DLL来实现其API,其中低层、比较靠近内核的几个DLL称为“系统DLL”,意思是这些DLL已经属于操作系统的范畴。其中最靠近内核、最基本的DLL是ntdll.dll,此外还有kernel32.dll、user32.dll。这样,所谓Windows操作系统,至少应该是内核加上系统DLL还有子系统服务进程的总和。当然,其中最重要、最复杂的是内核。
    图中的水平粗线表示用户空间和系统空间的分界。在Windows操作系统中,整个4GB的虚存地址空间被对半分成两块,从地址0x80000000开始向上是“系统空间”,就是内核所在的地方,下面则是“用户空间”,是应用程序所在的地方。后面将讲到,系统空间是全局的,而用户空间只属于具体的进程,每个(用户)进程都有自己的用户空间。
    除表示用户空间和系统空间的分界外,图中的水平粗线还有另一层意思,就是表示Windows的“应用程序二进制界面”,即ABI。ABI一方面是系统调用的界面,定义了所有系统调用函数界面的集合,并规定了系统调用如何进行;另一方面也规定了可执行程序文件的结构和格式。Windows的系统调用界面是不公开的,现在人们所知道的有关信息基本上来自有关的研究和实验,特别是来自逆向工程的研究。
    CPU必须进入“系统态”才能执行存放在系统空间的程序,访问存放在系统空间的数据。而对用户空间的数据则不论处于“系统态”或“用户态”都能访问。但是CPU怎样才能进入“系统态”,即进入内核呢?只有三种途径。第一种是“系统调用”,第二种是“中断”,第三种是“异常”。所以图中内核的最上层是“系统服务”,即系统调用的界面。图中还画上了中断和异常的入口,这是因为通过中断或异常进入内核时的一些系统开销性质的操作与系统调用相似。但是,从逻辑的角度讲,中断和异常的入口应该是在内核的底部,因为中断和异常都来自(包括CPU在内的)硬件。
    到了内核中,系统调用界面的下面就是Executive,即内核的管理层,管理层的下面又具体分为对象管理、内存管理、进程管理、安全管理、I/O管理等模块。在微软的术语中,这些管理模块称为Manager,即“管理者”或“主管”,例如对象管理模块就称为“Object Manager”,意为“高管(Executive)”领导下的部门主管。在特定的语境下,也有把这些模块称为“子系统”的,读者需要注意分辨,不要跟“Windows子系统”、“POSIX子系统”和“OS/2子系统”相混淆。值得注意的是,这些模块虽然在管理层,但是它们所管理的目标和操作并不局限在管理层内部,就好像具体的管理部门虽然在公司总部,但是所管理的具体操作却可能在基层的营业所或办事处。例如内存管理,其上层在管理层中,其底层的操作却可能在HAL层中。所以,层次是横向的概念,而模块是纵向的概念。
    再往下就是微软所称的“内核(Kernel)”了。所以微软所称的“内核”其实是内核中较为核心的、比较接近底层的一层。这一层中包含了跟设备驱动底层中断处理、异常处理等有关的功能。这下面就是“硬件抽象层”HAL了。
    当CPU运行于内核中时,尽管都是在内核中,有些操作只允许在特定的层次上进行,或者只允许针对特定的层次进行。例如在中断处理的内部就不允许线程切换,线程切换只有在完成了中断处理以后才能进行。所以,Windows内核规定只有在从所谓的“内核”层(而不是整个内核)退出来时才可以进行线程切换。
在线程调度/切换的问题上,有两种不同的方式:一种称为“剥夺式(Preemptive)”,或称“抢占式”,意思是只要有优先级更高的线程就绪,哪怕CPU已经在为别的优先级较低的线程所用而且无意主动让路,也要立即把它夺过来;另一种是“非剥夺式(Non-Preemptive)”,或称“不抢占式”,如果CPU已经在执行别的线程,就先忍一忍,等待适当的机会。什么机会呢?就是等正在系统空间运行的线程返回用户空间的时候。可是,优先级较高的线程之所以变成就绪,其触发的条件也必为系统调用、中断或异常其中之一,总之是在CPU运行于系统空间的时候变成就绪的。所以,所谓“剥夺式”和“非剥夺式”,其实不是剥夺不剥夺的问题(那取决于调度策略),而是什么时候剥夺的问题。2.6版以前的Linux主要面对分时应用,所以是不剥夺的,其特征就是要到CPU从内核退回用户空间时才进行调度和切换。现在的Linux则更多地面向桌面应用,所以也是剥夺式的了。但是,即使是剥夺式的调度和切换,也不表示在内核中的任何一个角落都可以切换线程,例如在中断服务程序中就不允许,所以这里也有个时机的问题。如上所述,Windows内核规定只有在从所谓的“内核”退出来时才可以进行线程切换。那么Windows的线程调度/切换是剥夺式的还是不剥夺式的呢?微软一直声称是剥夺式的(后面读者将看到确实是),但是又说只有在从“内核”退出来时才可以进行线程切换,这就引起了质疑和困惑,因为当时很多人都把从“内核”退出来理解成从整个内核退出来而返回用户空间的时候。可见,一方面不公开技术细节,另一方面对术语的使用又与人不同,这就很容易引起困惑。
    与Linux相比,Windows的内核还有个明显的不同,就是其内核的相当一部分页面是可倒换的。后面读者将看到,内存管理即虚存技术的重要内容之一是物理页面的倒换,就是可以将已经有映射但是暂时不受到访问的页面倒换到外存中去,到实际受到访问时再倒换进来,使外存成为内存的扩充。但是,在Linux中,属于内核的页面无论是用于代码还是数据(用于文件内容缓存的除外),是不受倒换的。这一方面是为了简化内核的设计和实现,一方面也是因为觉得价值不大,因为内核毕竟是全局的,所有进程都公用同一个系统空间,即使要倒换也油水不大。但是,Windows的内核却不同,其相当一部分页面是可倒换的。究其原因,另一方面可能是来自VMS的影响,另一方面也可以从Windows内核的体积得到一些解释。如前所述,微软把图形操作/视窗服务的实现也搬到了内核中,这就使内核的体积增加了很多。再说每个线程的系统空间堆栈也因此而增大了许多,当线程数量很大时也确实不可忽视。所以,在这样的情况下,使内核的部分页面成为可倒换的确实有其合理的一面。不过当然不是所有页面都可倒换,有些页面注定是不可倒换的。例如,与中断和异常有关的代码和数据所在的页面,以及与页面倒换有关的代码和数据所在的页面,就显然是不能被倒换出去的,否则就无法把这些页面倒换回来了。这样,我们就大致可以推断,Windows内核中“内核”层及其以下应该是不允许倒换的。
    与此相关,Windows为CPU的运行状态定义了许多“IRQ级别”,即IRQL。在任一时间中,CPU总是运行于其中的某一个级别,这个级别就表明了什么事情可以做、什么事情不可以做。下面是这些级别的定义:
#define PASSIVE_LEVEL                          0
#define LOW_LEVEL                             0
#define APC_LEVEL                              1
#define DISPATCH_LEVEL                        2
#define PROFILE_LEVEL                         27
#define CLOCK1_LEVEL                          28
#define CLOCK2_LEVEL                          28
#define IPI_LEVEL                               29
#define POWER_LEVEL                           30
#define HIGH_LEVEL                             31
其基本的意图是,如果CPU从而一个线程已经处于某个级别,其操作就不能受同级或更低级别的操作所干扰。
    这里的PASSIVE_LEVEL是级别最低的,但是却对应着系统结构中较高的层次。当CPU运行于用户空间,或者虽然进入了内核但还只是运行于管理层的时候,其运行级别就是PASSIVE_LEVEL。比其略高的是APC_LEVEL,那是在(内核中)为APC函数(见本书“进程与线程”一章)的执行进行准备时的运行级别,APC请求相当于对用户空间程序的(软件)中断。注意IRQL在x86系统结构中并没有硬件的支持(CPU中并没有这么一个寄存器)而只是一个变量。与CPU只能通过特殊的指令或中断/异常才能进入系统态不同,IRQL是CPU可以自由设置的,每当CPU进入更底层、更核心的层次时就提高IRQL,反之则降低IRQL。不过,表明IRQL的变量在内核中,运行于用户空间时是无法改变IRQL的。
再高一级是DISPATCH_LEVEL,这大致相当于CPU运行于Windows内核中的核心层,即“内核”层。线程的切换只能发生于CPU行将从DISPATCH_LEVEL级别下降的时候。
    IRQL级别3及以上用于硬件中断。显然,设计者的意图是采用中断优先级,即优先级较高的中断源可以中断优先级较低的中断服务。但是x86的系统结构并不支持中断优先级,所以这实际上是来自VMS的遗迹,因为VAX和PDP的系统结构都是支持中断优先级的。
    回到页面换出的问题上,只要CPU的IRQL级别不高于APC_LEVEL的层次,其代码都是允许倒换的,但是从DISPATCH_LEVEL开始就不允许了。显然,如果在这一点上搞错了,后果是很严重的。所以在管理层的代码中几乎每个函数的开头都要放上一个宏操作PAGED_CODE(),说明代码作者的意图是让这个函数所占的页面可以被倒换出去。这个宏操作的定义如下:
#ifdef DBG
#define PAGED_CODE() { /
  if (KeGetCurrentIrql() > APC_LEVEL) { /
    KdPrint( ("NTDDK: Pageable code called at IRQL > APC_LEVEL (%d)/n",
                                                  KeGetCurrentIrql() )); /
    ASSERT(FALSE); /
  } /
}
#else
#define PAGED_CODE()
#endif
    在Debug模式下,这个宏操作检查CPU当前的运行级别,如果发现高于APC_LEVEL就说明这个函数有可能在DISPATCH_LEVEL或更高的级别上受到调用,因而是不应该被倒换出去的,所以就发出警告。至于在正式运行的版本中,则这个宏操作定义为空。
当然,光是在程序中引用宏操作PAGED_CODE()不会使一个函数所在的页面可倒换,真正使其可倒换的是编译指示“#pragma alloc_text()”。例如NtQueryObject()中的第一行就是PAGED_CODE(),与此相应,这个函数所在的源文件中就有这么一行:
#pragma alloc_text(PAGE, NtQueryObject)
    正是这一行编译指示让编译工具将为此函数生成的可执行代码放在可被倒换的区间。
    在本书所引的代码中,许多函数的开头都有对PAGED_CODE()的引用,但是为了压缩篇幅而将其省略了。此外,原来的代码中为增加可读性而插有一些空行,为压缩篇幅也把它们删去了。还有,在一些if语句中,也是为增加可读性,许多人主张哪怕只有一行代码也要加上前后花括号。笔者很赞同这些主张,但是那样一来就又得多占两行,所以书中有些地方把前后花括号删去了,这只是为压缩篇幅,而不表示笔者认为不应该使用这些花括号。
    书中所引代码中凡是黑体字的部分都是笔者觉得需要提醒读者注意的,但是为不同目的而阅读同一段代码时的侧重面可能不同,所以这只是就一般的阅读、就代码的“主旋律”而言,并不表示别的代码就不重要。

你可能感兴趣的:(探究Windows内核你知多少)