内容:43—51页
在Intelx86处理器上,段描述符有一个2位长度的特权级:0表示最高特权级,3表示最低特权级。Windows只使用0和3两种特权级。特权级0表示CPU处于内核模式,3表示用户模式。处理器有许多指令只能够在特权级0的模式下使用,例如I/O指令,操纵内部寄存器(如GDT、IDT、MSR)的指令等。在Windows中,当处理器位于用户模式下,处理器只能访问当前进程的地址空间。而在内核模式下,处理器不仅可以访问当前进程的地址空间,还可以访问系统地址空间。
一个指令流(线程)在执行时,出现以下情况会发生模式切换:
1、用户模式代码触发了异常;
2、用户模式代码在执行时发生了中断。
3、执行特殊的模式切换指令。
Windows系统中,0~2GB是进程地址空间,2GB~4GB是系统地址空间。为了有效管理2GB的系统地址空间,Windows在初始化时将这2GB划分为一些固定的区域,各个区域有专门的用途。Windows内核使用了一组全局变量来记录每个区域的边界,系统地址初始化实际上是对这些全局变量的初始化,并相应地初始化每个区域。Windows的引导选项和系统配置(位于注册表)可能会影响到某些区域的位置和大小。
系统空间中主要区域包括:内核模块镜像,PFN数据库、换页内存池、非换页内存池、会话空间、系统缓存区、系统PTE区域、系统视图以及页表等。
Windows使用Intelx86的二级或多级页表机制来访问虚拟内存。翻译过程涉及查询页目录与页表。如果页表项指示的一个页面未在物理内存中,则触发页面错误(page fault)异常。虚拟内存管理器通过页面错误异常将已被换出到磁盘上的数据或代码重新带入物理内存,以供访问。Windows利用页表机制,实现了灵活的页面交换算法,可以支持一个或多个页面交换文件,同时也实现了内存页面的写时复制(copy-on-write)特性。
在系统空间中,不同的区域使用不完全相同的内存页面管理算法:
1、非换页内存池:初始化时已经被映射到物理页面上,做法是按照不同的粒度,将空闲页面链接起来。申请和释放页面的操作实际上是针对空闲链表来进行的。
2、换页内存池:Windows使用位图来管理页面的分配。
3、系统PTE区域:这部分内存区域存放的并非PTE,而只是表示这部分地址范围是以PTE的形式来管理的,把PTE当成资源来管理。
Windows执行体在这些系统内存区域管理的基础上,提供了一组更小粒度(8B的倍数)的内存池管理,包括执行体换页内存池和执行体非换页内存池。这些内存池通过空闲链表记录下每个已申请页面中的空闲内存块;当释放内存时,自动与相邻的空闲内存块合并以构成更大的空闲内存块。内核的其他组件通过使用执行体暴露的API函数(例如ExAllocatePoolWithTag和ExFreePoolWithTag)来使用这些内存池。
进程地址空间是随进程一起被创建的,进程地址空间按照其虚拟地址是否被分配或者保留来进行管理,用户模式代码通过Windows API函数VirtualAlloc和VirtualFree来申请获释放地址范围,而内核中的虚拟内存管理器通过一棵平衡二叉搜索树来管理进程地址空间被使用的情况。树中的每个节点VAD(虚拟地址描述符,Virtual Address Descriptor),描述了一段连续的地址范围。
在VAD中,有一种重要的节点类型为内存区对象(section object),它是Windows平台上两个或多个进程之间共享内存的一种常见方式。内存区对象可以被映射到系统的页面文件、可执行文件或者其他数据文件中,也可能被映射到物理内存中。内存区对象代表了一种物理存储资源。
内存管理器的另外一个重要功能是管理有效的物理内存。在Windows的系统地址空间中,保留了一个被称为PFN(Page frame number database,页帧编号数据库)的区域。每个物理页面都对应PFN数据库中的一项,此PFN项描述了该页面的状态。Windows支持8种状态:活动、备用、已修改、已修改但不写出、转移、空闲、零化、坏状态。活动是指此正在被某个进程或系统空间所使用,有一个对应的PTE指向该页面。坏是指该物理页面已检测到硬件错误。
工作集管理器要解决的是当系统中的进程需要使用大量内存时,内存管理器如何将有限的物理页面分配给那些需要使用内存的进程。这里的工作集是指一个进程当前正在使用的物理页面的集合。每个进程都有一个工作集链表,其中每一项不仅记录了物理页面的编号,还记录了其他属性,包括了它的年龄。工作集管理器可以根据一些策略来选择要修剪的进程。针对被选中的进程,选择哪些页面被换出到磁盘中,从而将物理页面腾出来。工作集管理器运行在一个成为平衡集管理器的线程中,每隔1s被触发一次,当可用内存太低时,也会被触发。平衡集管理器也会定期触发进程/栈交换器(process/stack swapper)。进程/栈交换器是另一个单独的线程,一旦被唤醒,就会将满足特定条件的进程和栈换入内存或换出内存。
Windows优先级分为0-31,0表示系统优先级,为最低优先级,仅用于零页面线程;1-15为动态优先级,在某些情形下线程的动态优先级可以在此范围内进行微调;16-31为实时优先级,用于处理一些实时处理任务。
作业(Job)是一个执行体支持的内核对象,它使得一个或多个进程被当做一个整体来加以管理和控制。
纤程(fiber)是一种用户线程,它对于内核是不可见的,由kernel32.dll实现
中断是处理器与外部设备打交道的重要途径,异常是处理器的正常指令流在执行过程中产生的一些特殊事件,需要紧急处理才能继续原来的指令流。它们都会打断一个正常的指令流,但区别在于中断的发生与当前指令流并无实质联系,而异常则是当前指令流执行的直接结果。中断是异步的,异常是同步的。
Intelx86采用同一套陷阱机制来处理中断和异常,它利用IDT,将每个中断或异常同一个处理该中断或异常的服务例程联系起来。Windows在此硬件机制的基础上,提供了一种更为灵活的软件机制,允许驱动设备程序为特定的中断向量添加它的中断服务例程(ISR,Interrupt Service Routine)。一个中断向量允许连接多个中断对象(interrupt Object),这里中断对象是一种封装了中断服务服务例程的内核对象。因此,中断发生时,这些中断对象中的中断服务例程都有机会处理该中断。同时,多个硬件设备也可以共享同样的硬件中断向量。
中断控制器(如APIC)允许设定每一个硬件中断的优先级,但Windows中没有使用中断控制器的优先级,而是规定了一套软件中断优先级,称为中断请求级别(IRQL,Interrupt Request Level)。在Intelx86系统中,Windows使用0~31表示IRQL,数值越大,优先级越高。处理器在运行时总有一个当前IRQL。如果发生中断,中断源的IRQL等于或低于当前级别,则该中断被屏蔽,直到处理器的IRQL降下来为止。IRQL=0表示普通线程,称为PASSIVE_LEVEL或被动级别,它的优先级最低,可被其他任何级别的中断打断。IRQL=1表示异步过程调用(APC,Asynchronous Procedure Call),称为APC_LEVEL。在一个线程中插入一个APC对象可以打断该线程的执行;IRQL=2表示处理器正在做以下两个事情之一:正在进行线程调度;正在处理一个硬件中断的后半部分(不那么紧急的部分),这被称为延迟过程调用(DPC)。因此,IRQL=2也被称为DISPATCH/DPC级别,也即DISPATCH_LEVEL。3~26是设备IRQL,27~31是一些特殊的硬件中断,包括时钟中断,处理器间中断等,它们都是硬件中断。
DPC是一个重要的概念。它往往用来执行一些相对于当前高优先级的任务来说不那么紧急的事情,硬件中断服务例程可以把一些相遇不紧急的事情放倒一个DPC对象中处理,从而缩短处理器停留在高IRQL的时间。在Windows内核中,DPC一个典型用法是定时器(Timer)的实现,在时钟中断服务例程,它负责处理中断时间、系统时间,以及当前线程的时间信息等,并判断系统的定时器数组中是否有定时器到期,若有则发出DISPATCH_LEVEL的软中断请求。定时器到期是在DPC交付(deliver)过程中处理的,定时器被当做一种特殊的DPC对象而交付执行。
APC是线程相关的例程,只能够在特定的线程环境中被执行,因此也一定在特定的地址空间中被执行。当一个线程获得执行权时,它的APC例程会被立刻执行。这一特性使得APC非常适合于实现各种异步通知事件。例如,I/O的完成通知可以用APC来实现。
在Intelx86处理器中,异常也是通过IDT分发的,在IDT表中,0~0x1f之间的中断向量是Intel保留的,除了2号中断向量保留给NMI(不可屏蔽中断)意外,其它已经定义的中断向量都是针对各种条件所引发的异常的。Windows为所有的异常都提供了异常处理器,有些异常直接由系统内核处理,例如页面错误(0x0e异常),由虚拟内存管理器接管,有些异常则需要交给当前线程或Windows子系统的代码来处理。
在内核模式下,异常分发器首先将异常交给内核调试器来处理,如果没有内核调试器或者内核调试器没有处理该异常,则尝试分发到一个基于帧的异常处理器,它将异常处理器与“栈帧”关联了起来,因此当发生异常时,异常分发器根据当前栈中的栈帧来查找与之关联的异常处理器。如果未能找到这样的异常处理器,则异常分发器则会将该异常再次交给内核调试器,如果这次异常扔未被处理,则被认为是一个严重错误,系统崩溃。
在用户模式下,异常分发器首先判断进程的调试端口是否有效,若有效,则发送消息至调试端口,然后等待应答,否则将异常交给内核调试器。如果仍然未得到处理,则将控制转到用户模式下,由用户模式的异常分发器(ntdll中的KeUserExceptionDispatcher函数)寻找一个基于帧的异常处理器。如果未找到,则控制再次回到内核模式下。这一次,内核模式的异常处理器首先尝试调试端口,若异常未被处理,则再尝试当前进程的异常端口。连接进程异常端口的是Windows子系统。如果Windows子系统在异常处理的最后时刻仍有机会处理它所属进程的异常,如果它也不能处理此异常,则该进程被终止。
现代操作系统中,由于多处理器、多核或者中断等各种并发性因素的存在,同样的代码有肯呢过被开发执行,而数据也可能被并发访问。在这些情况下,对于可能被并发访问的数据进行必要的同步。根据执行环境中的IRQL大于APC_LEVEL或者等于PASSIVE_LEVEL,可以讲同步机制分为“不依赖与线程的同步机制”和“基于线程调度的同步机制”两大类。
当IRQL大于APC_LEVEL时,Windows提供的典型的同步机制如下:
另一类是PASSIVE_LEVEL上的线程之间的同步。Windows定义了统一的机制来支持各种线程同步原语:分发器对象(dispatcher object),其数据结构头部为DISPATCH_HEADER。Windows Server 2003实现了以下分发器对象: