Notes_Windows内核原理与实现书评-查字典图书网
查字典图书网
当前位置: 查字典 > 图书网 > 编程 > Windows内核原理与实现 > Notes
伊卡洛斯 Windows内核原理与实现 的书评 发表时间:2011-05-08 22:05:59

Notes

英文名:Understanding the Windows Kernel 作者:潘爱民
第1章 概述
  没有太重要需要记录的东西,就是重新回顾一下操作系统特别是win系列的发展。后面每一章都很长很多,需要做好准备,尤其下一章介绍如何配合wrk学习的一节,需要认真学习


第2章 Windows系统概述
  2.1~2.4
  还是基本的内核原理讲解,包括中断层,系统结构等等,不是很难

  2.5公共管理设施
      Windows对象管理器的基本设计意图:
      1)为执行体的数据结构提供一种统一而又可扩展的定义和控制机制
      2)提供统一的安全访问机制
      3)在无需修改已有系统代码的情况下,加入新的对象类型
      4)提供一组标准的API来对对象执行各种操作
      5)提供一种命名机制,与文件系统的命名机制集成在一起

      对象的引用计数来源:
      1)内核中的指针引用,使用ObReferenceObjectByPointer和ObDereferenceObject
      2)句柄引用,使用ObpIncrementHandleCount和ObpDecrementHandleCount

      一个系统的储巢列表存放在HKLMSYSTEMCurrentControlSetControlhivelist下

      2.6引导过程
      内核加载流程图:
      BIOS
        MBR
          引导扇区(引导分区的第一个扇区)
            引导扇区的后续分区
              ntldr(实模式部分):切换至保护模式
                ntldr(保护模式部分:os loader)
                  1)构造内存描述符数组
                  2)映射页面,设置页目录寄存器,打开内存页面映射机制
                  3)处理boot.ini
                  4)执行ntdetect.com
                  5)加载内核模块ntoskrnl.exe、hal.dll和SYSTEM储巢
                  6)加载引导驱动程序和必要的文件系统驱动程序
                  7)构造LOADER_PARAMETER_BLOCK参数块
                  8)将控制交给内核模块ntoskrnl.exe的入口函数

      内核初始化过程:
      阶段0初始化(入口函数为KiSystemStartup,创建内部状态变量,Idle,System以及一个系统线程,其开始例程为Phase1Intialization)
      阶段1初始化(入口函数为Phase1Intialization,此为内核的完全初始化,可以看到进度条和Windows Logo,最后启动一个用户模式进程Smss)
      用户层初始化过程:
      Smss进程(主要初始化参数在HKLMSYSTEMCurrentControlSetControlSession Manager)启动csrss和winlogon
      Winlogon进程(创建WinSta0以及登陆桌面和默认桌面,启动services,lsass,启动userinit,在退出之前启动explorer)


第3章 Windows进程和线程
  3.1 进程基本概念
  操作系统需要做的事情:
  1)维护一个全局的进程表,记录下当前有哪些进程正在被执行
  2)把时间分成适当的片段,在现在处理器结构中,这可以通过设置时钟中断来完成,因而每次始终中断到来时就会获得控制权
  3)在进程间实施切换,即保留上一个进程的环境信息,恢复下一个进程的执行环境

  3.2 线程基本概念
  Windows使用的是内核级线程,它的调度算法是一个抢占式,支持多处理器的优先级调度算法,它为每个处理器定义了一个链表数组,相同优先级的线程挂在同一个链表中,不同优先级的线程分别属于不同的链表。

  3.3 Windows中进程和线程的数据结构
      内核层的进程和线程对象偏重于基本的功能和机制,执行体层的进程和线程对象更侧重于管理和策略
  
  3.4 Windows的进程和线程管理
  Windows进程句柄表中的有效句柄会被解释成相应的内核对象,有以下四种可能:
  1)-1,代表当前进程
  2)-2,代表当前线程
  3)负值,其绝对值为内核句柄表中的索引。仅限于内核模式的函数可以引用。
  4)不超过266的正值,当前进程的句柄表中的索引

  对象的引用有两种来源:
  1)在内核中直接通过对象地址来引用,这是通过ObReferenceObjectByPointer来记录一次新的引用
  2)通过句柄来引用对象,这是由ObpIncrementHandleCount函数来检查并记录一次句柄引用

  FS寄存器只想一块被称之为处理器控制区(PCR,Processor Control Region)的内存,其数据类型为KPCR。KPCR有一个类型为KPRCB的数据成员PrcbData,这是当前处理器的控制块(Processor Control Block),其中包含了指向当前线程的KTHREAD结构的指针。
  即:FS:[0] KPCR.PrcbData.CurrentThread

  PspCreateProcess与PspCreateThread函数分别负责创建系统中的所有进程与所有进程

  进程和线程的结束,全部调用PspTerminateThreadByPointer来完成

  3.5 Windows中的线程调度
      在Windows中,线程调度的职能是在内核层上完成的,并不涉及执行体层
      进程对象KPROCESS结构的BasePriority域制定了一个进程的基本优先级,由于进程本身不参与调度,所以这个值的意义在意该进程中的每个线程在初始化时都可以直接从该进程对象中获得基本优先级值。
      而线程对象的KTHREAD结构的BasePriority和Priority域则分别定义了一个线程的静态和动态优先级。Priority域记录了一个线程的当前实际优先级。它往往是在BasePriority域的基础上提升一定的优先级增量。但是无论怎么调整,域值范围不会越过1~15
      进程和线程的基本优先级是保持不变的,除非通过调用KeSetPriorityAndQuantumProcess或KeSetBasePriorityThread函数显式改变

      KTHREAD的State域是一个跟线程调度有关的成员,反映了线程的当前调度状态,其枚举类型为C枚举类型KTHREAD_STATE,有以下状态:Initialized,Ready,Running,Standby,Terminated,Waiting,Transition,DeferredReady,GateWait

      KiReadyThread函数是对于线程的状态转移起关键作用,当一个线程从自身逻辑的角度而言,已经满足可可执行条件时,此函数就会被调用。对一个线程来说,KiReadyThread函数被调用是一个转折点,这意味着它进入了备选的通途;而在此之前,此线程是不会被考虑执行的。在WRK中,KiReadyThread函数在以下情况下被调用:
      1)当一个线程被接触等待时,见KiUnwaitThread函数。这通常发生在一个等待对象变成有信号状态的时候
      2)当一个内核队列对象(KQUEUE)中有新的成员插入并且满足必要的条件时,也会导致队列中的等待线程被调用KiReadyThread函数。参见KiInsertQueue函数
      3)当一个线程被附载(attach)到一个新的进程中时,如果目标进程在内存中,则目标进程的就绪列表中的线程被调用KiReadyThread函数。这种情形发生在内存管理任务中。参见KiAttachProcess函数。
      4)在KeSetEventBoostPriority函数中,如同第一种情形一样。在特定情形下,它直接调用KiReadyThread函数,而不是通过KiUnwaitThread函数来就绪一个线程。这是对同步类型的事件对象的一种特殊处理。本质上而言,它与KiUnwaitThread的情形一直。
      5)在处理换入进程链表时,KiInSwapProcess函数会针对每个已换入进程中的就绪线程调用KiReadyThread函数
      6)在处理换出进程链表时,KiOutSwapProcesses函数会检查每一个待换出的进程,如果它的就绪线程链表非空,则不换出该进程,并针对此进程中的就绪线程调用KiReadyThread函数,让它们尽快得到执行
      7)在创建线程函数(PspCreateThread)中,通过KeReadyThread函数来间接地调用KiReadyThread函数,这很容易理解,一旦一个线程创建成功,就需要激活该线程,让它得以执行

      当线程执行到KeDelayExcutionThread、KeWaitForSingleObject或KeWaitForMultipleObjects时肺腑等待条件可立即满足,否则,该线程将会进入等待状态,一直等到等待的条件满足为止。他们都使用KTHREAD结构中的Timer域来设置一个定时器,吧定时器加入到系统的定时器表表(第一个函数)或自己的等待链表(后面两个函数),然后调用KiSwapThread交出控制权。在等待期间,允许APC被执行。

      线程切换的两种情形:
      1)线程自愿放弃执行权(等待对象或延迟执行,门等待,线程终止,不能从队列队向中提取项,负载到非驻留的进程中),通过KiSwapThread切换的新线程
      2)被迫放弃执行权,使用DISPATCH_LEVEL软件中断的处理函数KiDispatchInterrupt,然后产生时限用完或者被抢占(KiDeferredReadThread函数),然后调用SwapContext函数(KiSwapThread,KiSwapContext函数都调用这个函数)切换新线程
      3)KiExitDispatcher,回复当前处理器在进入调度器逻辑之前的IRQL(如果老的IRQL和当前不一样,才可能发生线程切换)


第4章 Windows内存管理
  4.1 Windows内存管理概述
      内存地址类型:
      1)物理地址,即内存存储器索引
      2)虚拟地址(线性地址)
      3)逻辑地址,包含两个部分:段(segment)和偏移(offset)。段部分指定了在整个地址空间中的一个基地址以及段空间的大小,以及一些其他属性。偏移部分指定了一个逻辑地址相对于段基址的偏移量,次偏移量不能超过段边界

      内存管理方式主要有以下两种:
      1)页式内存管理,如何把虚拟地址转译为物理地址
      2)段式内存管理,如何把“段+偏移”形式的逻辑地址转移为物理地址
      转译的机制是由芯片硬件提供的,但是转移中用到的数据结构则可能是由操作系统来管理的

           页式内存管理:
           在Intel x86处理器中采用分级页表的方式来管理物理地址和虚拟地址的映射关系,每个虚拟地址的32位信息中,由页目录索引(10位)、页表索引(10位)、页内偏移(12位)组成。在解析一个虚拟地址时,首先根据最高10位在页目录中定位到一个页目录项(PDT,Page Directory Entry),它指向一个包含1024位项的页表。然后根据接下来的10位,在页表中定位到一个页表项(PTE,Page Table Entry),此页表指定了目标页面的物理地址。最后在此物理地址的基础上加上页内偏移,即得到最终的物理地址。
           在这样的二级页表结构中,CR3寄存器包含了页目录的物理地址,页目录的大小是4096个字节,每个目录项为4个字节;同样,每个页表的大小也是4096个字节,其中每个页表项为4个字节。目录项和页表项均指向一个32位的地址,但只有前20位真正指向一个物理地址,后12位用于各种标志信息,如是否被访问过,是否允许缓存等。
           其过程可以概括为如下步骤:
           1)通过CR3寄存器定位到页目录的起始地址,正因为如此,CR3寄存器又被称为页目录基地址寄存器(PDBR)
           2)取虚拟地址的高10位作为索引选取页目录的一个表项,也就是PDE
           3)根据PDE中页表基地址(取PED的高20位,低12位设为0)定位到页表
           4)取虚拟地址的12位到21位(共10位)作为选取页表的一个表项,也就是PTE
           5)取出PTE中的内存页基地址(取PTE的高20位,低12位设为0)
           6)取虚拟地址的低12位作为页内偏移与上一步的内存页地址想家便得到物理地址
            *这里要特别注意,在步骤2后,一定要查看PDE第7位的数值,如果为0说明是页大小是4KB,可以继续往下转译。但是如果是1的话说明页大小是4MB,这时需要将PDE的高12位加上虚拟地址的低24相加得到物理地址

           段式内存管理:
           处理器在解析一个“段+偏移”的逻辑地址时,首先根据段寄存器中的表指示位确定应该使用GDT(若表指示位为0)还是LDT(表指示位为1),然后从GDTR或LDTR中得到描述符表的地址,再加上段索引部分乘以8,即得到段描述符的地址,然后根据段描述符的格式,拼出32位段基地址,最后加上CPU指令中的偏移值,得到最终的逻辑地址。

  4.2 Windows系统内存管理(太难了,回头看吧...)

  4.3 进程内存管理
  当PspCreateProcess创建一个进程时,如果指定的父进程不为NULL,则需要创建一个新的地址空间。京城空间的创建过程是由MmCreateProcessAddressSpace函数完成的,而且,地址空间被创建起来以后,PspCreateProcess还调用MmInitializeProcessAddressSpace函数来初始化该地址空间。
  Windows在进程切换时,只需直接切换页目录页面(CR3寄存器),而武训对页目录中的PDE做任何调整或一致性维护,岂可实现从一个进程的地址空间转移到另一个进程的地址空间,并且系统空间除了超空间和会话空间部分以外,在新老进程中保持一致。

  对于进程地址空间,用户程序比寻经过“reserve”和“commit”两个阶段才能使用一段地址范围。
  Windows的进程地址空间是通过VAD(虚拟地址描述符,Virtual Address Descriptor)来管理。EPROCESS对象中的VadRoot指向此树的根。在设计上,虚拟地址空间VAD树的真正根节点是VadRoot.BalancedRoot.RightChild,类型为MMVAD
  NtAllocateVirtualMemory函数首先根据进程的句柄找到进程的EPROCESS对象,然后构造一个VAD对象,利用参数中的信息来填充VAD对象中的有关域,再把VAD对象插入到VAD树中,并调用MiInsertVadChange函数记录内存的使用开销,最后更新进程中的相关域。如果是commit的情形,则还需要计算各种管理开销,以及更新PDE与PTE属性
  NtFreeVirtualMemory函数首先找到进程EPROCESS对象,然后根据参数中给出的地址,找到相应的VAD节点。如果要释放的地址范围超出了VAD节点的范围,则返回失败。也就是说NtFreeVirtualMemory函数仅用于一个VAD节点地址范围内的内存释放。然后NtFreeVirtualMemory函数检查VAD节点中的信息以确保此释放操作时合适的,并且删除此VAD节点或者将VAD节点的地址范围缩小,甚至有可能还要切分出一个新的VAD节点来。最后,它调用MiDeleteVirtualAddresses清楚这段地址范围的PTE以及页面,并且调用MiReturnPageTablePageCommitment函数毁坏表页面以及回复VAD位图中相应的位

  MmCreateSection函数逻辑大致如下:如果FileHandle或者FileObject参数非空,则利用文件对象中的信息来填充一个CONTROL_AREA对象,包括其中SEGMENT对象。如果文件对象还没有SEGMENT对象,则调用MiCreateImageFileMap或MiCreateDataFileMap创建一个新的SEGMENT对象。如果参数中没有指定文件句柄或文件对象,则调用MiCreatePageingFileMap函数创建一个SEGMENT对象。有了SEGMENT对象以后,MmCreateSection调用对象管理器的ObCreateObject函数创建一个SECTION对象,并填充其中的信息,最后返回该SECTION对象。

  4.4 内存页面交换
      当Intel x86处理器在执行一个执行流的过程中需要翻译一个虚拟地址引用时,如果该地址的页表项(PTE)中的有效位为0,则处理器会引发一个异常,也称为页面错误(page fault)。Windows内核的陷阱处理器(trap handler)会把这一异常交给内存管理器的页面错误处理例程(page fault handler),由它来解决页面无效的问题。
      一般来说,只要这个页面错误是合理的(比如,页面已被唤出到外存),则页面错误处理例程将试图分配一个物理页面,并设置好PTE,从而消除虚拟地址的引用错误,使得该地址引用能被正确地翻译成恰当的物理页面。由于异常是硬件触发的,而页面错误处理例程是操作系统提供的组件,所以这一页面分配并重映射的过程对于原始的指令流是完全透明的,最多只是时间上稍有停顿。

      当PTE最低位为0时(这种PTE称为无效PTE),处理器引发错误异常,。该异常的陷阱处理器将把控制权交给页面错误处理例程,这时PTE的其他含义由操作系统来解释。
      当进程第一次引用一个已被映射的内存去对象的视图中的页面时,内存管理器利用原型PTE中的信息来填充页表中的实际PTE。段对象中的原型PTE与段对象一起创建,或者在映射内存去对象时根据需要而创建。原型PTE只是在内存去的段对象中记录了页面的状态,它们并不参与处理器的地址转译。我们可以把原型PTE理解为一个页面在其生命周期中全程有效的状态信息,而系统为处理器地址转译而准备的PTE只不过是此生命周期中一部分时间有效的状态信息。当内存去对象中的一个页面从有效变成无效时,它的硬件PTE将直接指向原型PTE。

      页面错误处理异常的陷阱处理器是_KiTrap0E,它首先构造一个陷阱帧,然后把控制权交给内存管理器的MmAccessFault,如果MmAccessFault函数能解决此页面错误,则陷阱处理器正常返回;若不能解决此页面错误(如访问违例(access violation,例如写一个只读页面)),则_KiTrao0E函数判断此页面错误是否发生在一些特殊的点上,并进行相应的处理,否则交给CommonDispatchException或CommonDispatchException2Args函数,由Windows统一的异常分发设施来处理。

      可能发生页错误的情形:
      1)无效PTE,又分为以下四个情形:
        1、页面位于页面文件或映射文件中。解决办法是分配一个物理页面并从磁盘上读入内容。这种页面错误也称为“硬(hard)”页面错误,因为页面数据确实要从磁盘上读回来
        2、访问一个尚在内存中但正在转移过程中的页面。将该页面转移到进程工作集或系统工作集。这种页面错误也称为“软(soft)”页面错误,因为它们只是由于管理的原因而产生的,数据本身仍然在内存中
        3、访问一个尚未提交的页面。这是访问违例错误
        4、访问一个要求0的页面。申请一个填满零的页面,并加入到当前进程的工作集
      2)在用户模式下访问一个只有在内核模式下才允许访问的页面。返回访问违例错误
      3)写一个只读页面。返回访问违例错误
      4)写一个守护页面。返回守护页面违例错误
      5)执行代码位于“不可执行”的页面中。返回访问违例错误
      6)写一个标记为“写时复制”的页面。复制一份属于当前进程私有的页面拷贝,并替换原来的页面
      从上面的列表可以看出,页面错误实际上分为两类:地址转译不成功;PTE中的访问控制位检查没通过,包括模式位和读写位的检查

      MmAccessFault函数需要判断所有这些情形,并在可能的情况下解决页面错误,然后反悔处理的结果,结果为以下四种情形之一:
      1)页面错误解决成功:STATUS_SUCCESS,或者其他一些只是某些特性的返回值,包括:
        STATUS_PAGE_FAULT_PAGING_FILE、STATUS_PAGE_FAULT_TRANSITION
        STATUS_PAGE_FAULT_DEMAND_ZERO、STATUS_PAGE_FAULT_COPY_ON_WRITE
      2)访问违例:STATUS_ACCESS_VIOLATION
      3)守护页面违例:STATUS_PAGE_FAULT_GUARD_PAGE
      4)页面换入失败:STATUS_IN_PAGE_ERROR

      MmAccessFault函数首先处理发生在中断请求级别APC_LEVEL智商的页面错误,之后处理虚拟地址位于系统地址空间的页面错误。在经过一系列的检查以后,真正的页面错误处理由MiDispatchFault函数来完成(也有一些情形调用了MiCopyOnWrite或MiResolveDemandZeroFault函数来解决页面错误)

  4.5 进程内存管理
      PFN数据库位于系统地址空间的一段区域,用于管理系统的物理内存。PFN数据库是一个阵列,或者说是一个数组,每一项都描述了一个物理页面的状态,长度为24字节。要查看一个物理页面的状态,只需直接以页帧编号为索引,因此,要查看一个物理页面的状态,只需直接以页帧编号为索引,就可以获取到该页面的PFN项(利用ML_PFN_ELEMENT实现)
      每个进程和系统都有一个工作集,而页面可能属于某个工作集,也可能不属于任何一个工作集。

      一个物理页面的可能状态如下:
      1)活动状态(active),也称为有效状态(valid):正在被某个进程使用,或者被用于系统空间(非换页内存池,或者在系统工作集)。对应有一个有效的PTE指向该页面
      2)备用状态(standby):本来属于某个进程或系统工作集,但现在已经从工作集中移除。这种页面对于原来的工作集仍然是有效的。原来工作集中的PTE仍然指向该页面,但是已被标记成正在转移的无效PTE。这种页面出于被回收状态,既可以被系统回收以作他用,也可以被原来的工作集回收而继续留用
      3)已修改状态(modified):类似备用状态,但是页面包含的内容已经被修改过。原来工作集中的PTE仍然指向物理页面,但已被标记成正在转移的无效PTE。如果系统要把这种页面回收作他用,则必须将其中的内容写到磁盘上
      4)已修改但不写出(modified no-write):类似于上一种状态,但区别在于,内存管理器不会将它的内容写在磁盘上
      5)转移状态(translateed):说明一个页面正处于I/O操作进行中,
      6)空闲状态(free):页面是空闲的,不属于任何一个工作集,它们包含了不确定的数据,内存管理器在重新使用这些页面以前,应根据需要(尤其是出于安全考虑)清楚脏数据
      7)零化状态(zeroed):和空闲状态类似,但是其中的内容已经被全部清零
      8)坏状态(badpage):页面产生硬件错误,系统不再使用这样的页面

      MMPFN的u3.e1.PageLocation域(共三位)保存的是MMLIST的某一个枚举值,说明这个页面处于什么状态,在哪个链表中。相应地,系统定义了6个链表(活动页面和转移状态的页面没有链表),分别有6个全局变量代表这些链表的链表头。全剧数组MmPageLocationList将这些链表按照MMLISTS枚举值的顺序组织在一起。因此在Windows内核代码中,根据PFN可以很方便地找到MMPFN项,再根据PageLocation域的值就可以知道该页面当前的状态,以及位于哪个链表中。

      内存管理器根据系统内存的数量以及各个进程对于内存的需求,动态地调度这些物理页面的使用,页面错误有以下三种:
      1)由一个正在转移状态的无效PTE引起
      2)由一个位于页面文件中的PTE引起
      3)需要一个零页面来满足

      修改但不写出页面的一个典型用途是,NTFS文件系统利用这种页面来映射文件系统元数据,所以,这些元数据不会被自动刷新到磁盘上。只有当记录元数据修改情况的日志信息被写到磁盘上以后,这些元数据才被显式地写到磁盘上。这样,NTFS才可以保证对元数据的任何修改总是会被记录下来,从而一旦发生意外事故,元数据可以恢复。但是这样这些页面就会占据系统物理内存,所以一定要定期显式地通过MmEnableModifiedWriteOfSection函数将它们移除。

      坏页面并不参与系统的页面调度,并且理论上坏页面插入后就不再移除

      当内核或设备驱动程序需要多个物理内存页面时,Windows内存管理器提供了MDL(Memory Descriptor List,内存描述符链表)的数据结构来描述这些页面。MDL描述的内存是一组物理页面,其PFN紧跟在MDL对象之后(通过 Pages = (PPFN_NUMBER)(Mdl+1) 可以访问到包含这些页面的PFN数组)。在考虑数据传输性能,或者为了某些硬件特性(比如DMA),需要在物理内存地址上工作,这时MDL是很好的途径

      当系统的内存消耗很多时,零化链表、空闲链表和备用链表上的页面数量就会减少,少到一定程度时,系统中将有两个线程开始工作,把已修改的页面写入到外存中,其中一个负责往页面文件中写页面,另一个负责往映射文件中写页面。一旦页面的内容被写到外存,这些页面就将被转移到备用链表中,供系统或其他进程使用。这两个进程被称为修改页面写出器(modified page writer)。

      内存管理器在初始化时,还创建了另外一个称为进程/栈交换器(process/stack swapper)的线程。该线程的主函数是KeSwapProcessOrStack,其优先级为23,它的主要任务是唤出内核栈、唤出进程,换入进程和换入内核栈。该线程每8s(若物理内存小于19MB则是4s)被唤醒一次,然后判断是否有符合条件的内核栈或进程需要唤出或换入。

      应用程序可以通过系统名字空间中的名称“KernelObjectsLowMemoryCondition”和“KernelObjectsHighMemoryCondition”来监听高内存和低内存通知事件,这两个系统变量分别是MiLowMemEvent和MiHighMemoryEvent,从而在内存变低或变充裕时获得通知。

  4.6 工作集管理
      工作集是指一个进程当前正在使用的物理页面的集合。在Windows中,除了进程工作集以外,另有两个工作集概念:系统工作集指System进程(由全局变量PsInitialSystemProcess表示)的工作集,其中包含系统模块的映像区(比如ntoskrnl.exe和设备驱动程序)、换页内存池和系统缓存(cache);会话工作集指一个会话所属的代码和数据区,包括Windows子系统用到的与会话有关的数据结构、会话换页内存池、会话映射视图,以及会话空间中的设备驱动程序。

  每个进程都有一个专门的页面用来存放它的工作集链表,这是在创建进程地址空间时已经映射好的一个页面,地址位于全局变量MmWorkingSetList中,所以在任何一个进程中,通过变量MmWorkingSetList就可以访问到它的工作集链表。MmWorkingSetList的类型为MMWSL。Windows为每个进程定义了工作集的最小值和最大值。此值只是页面调度时的一个指导,当进程使用很多页面并且系统也确实有足够的可用页面时,进程工作集可以超过所设定的最大值。但不能超过系统设定的全局最大值(变量MmMaximumWorkingSetSize)。

  内存管理器包含一个工作集管理器,它会定期检查各个进程的工作集,必要时修建占用内存较多的进程的工作集,以便更加有效地使用物理内存。

  工作集管理器的主函数是MmWorkingSetManager,它运行在内存管理器的平衡集管理器线程(KeBalanceSetManager函数)中。

  平衡集管理器(balance set manager)是在系统初始化时创建的,它是一个系统线程,其优先级为16,最低的实时优先级,因此比普通的非实时线程的优先级高。平衡集管理器的实际用途是维持系统内存资源的平衡,当多个进程发生资源竞争时,平衡集管理器负责从系统全局来调度可用的物理内存以及调整每个进程的工作集。
  平衡集是指所有具备资源分配资格的进程的集合,当然也包括System进程。当系统内存紧缺时,它一方面把拥有较多的内存的进程从它们的工作集中换出一些页面,另一方面把不满足运行条件的进程排除在平衡集以外。这些被排除在平衡集以外的进程,只有当其中的线程满足运行条件并且系统有了足够空闲内存时,才会被重新加入到平衡集中。


第5章 Windows并发和同步
  并发是指多个控制流同事在执行,既可能是真正的并行执行(例如在多处理器和多核的硬件环境中),也可能是分时的方式并发执行;
  同步是保证在并发执行的环境中各个控制流可以有序地执行,包括对于资源的共享或互斥访问,以及代码功能的逻辑顺序。

  5.1 Windows进程和线程的同步基础
       多个进程和线程并发执行的复杂性,复杂性在与,一旦它们相互之间有依赖关系,则它们的交互过程或计算步骤的顺序将可能存在不确定性,从而各个进程或线程的正确性难以保证和分析。

  多个线程在用全局变量进行通信时,如果对全局变量的操作不是原子的,则可能存在潜在的副作用。

  进程或线程的并发性有以下一些可能的来源:
  1)多处理器环境,2)多核环境,3)超线程环境,4)处理器外部中断,5)内部中断,6)线程放弃执行权

  Windows中的同步原语:
  1)互斥和互斥体(mutex):代表对一个共享资源的独占式访问,任何时候只有一方可以访问(有信号时是可用的,无信号时不可用)
  2)信号量(semaphore)计数器为N时,代表可以用N个线程引用该数据
  3)锁(lock):有Lock和Unlock两种,在使用的时候要避免锁竞争
  4)临界区(critical section):从程序代码的角度来控制线程的并发性,要在程序的并发性和复杂性之间进行平衡
  5)自旋锁(spinlock)和忙等待:忙等待是一种软件技术,指重复地检查一个条件;等待自旋锁的线程既不用把执行权让出来,也不用其他线程告诉它所的状态,仅适用于多处理系统。一般用于保护一些关键的内核数据结构
  6)消息(Message):Send和Receive

  互斥访问是各种同步原语的基础,几乎每一种同步原语都依赖于在一个原子操作中完成对一个内存单元的读(或测试)和写的两步骤操作。另一方面,这些同步原语并不是独立的,甚至可以相互实现。

  经典的同步问题:
  1)死锁(deadlock):如果一组线程中的每个线程都在等待只能由其他线程才能满足的条件,那么这组线程是思索的,所有的线程都将会继续等待,无法前进
    死锁的4个必要条件:
    1、对资源的访问是互斥的。每个资源要么已被某个线程所占有,要么还可以得到
    2、线程已占有了一个或多个资源,同时还在等待其他的资源
    3、资源一旦分配分配给一个线程,则只能被这个线程释放,其他线程无法抢占资源
    4、一组正在等待的线程形成了环,环中的线程都在等待下一个线程占有的资源
   解决方案:比较复杂,理想的方法就是打破上面例举的四个条件,从而使死锁无法形成。比如Dijkstra的银行家算法
  2)饥饿(starvation):一个进程或线程满足执行的条件,但一直得不到执行,甚至永远得不到执行(“饿死”)
  3)优先级反转(priority inversion):在线程调度时,一个低优先级的线程占有一个共享资源,从而导致高优先级的线程虽然其他运行条件都满足,但因为得不到该组员而无法运行。在抢占式调度算法的Windows系统中,当低优先级的线程占有了资源时,它可能被中等优先级的其他线程抢占,从而导致高优先级的线程在更长的时间内无法运行,实际效果就是相当于把高优先级的线程降到低优先级了。
   解决方案:1、优先级继承(priority inheritance) 2、优先级限高(priority celling)
  4)设学家进餐问题(dining philosophyers problem):这是一个很好用于说明思索、饥饿和活锁的例子,它蕴含了在计算机系统中以呼哧方式访问共享资源时的一些共性问题,因此它也被用于验证各种同步原语在解决实际问题时的能力和缺陷。
   解决方案:1、引入仲裁者(或侍者) 2、资源偏序 3、Chandy/Misra方案(能适应较大规模的问题,并且也有很好的并发性)

  Chandy/Misra方案步骤:
  1)每根筷子都被赋予一个状态,脏或干净。初始时,所有的筷子都被赋予编号较低的哲学家,其状态为脏
  2)当哲学家想要吃饭时,如果他尚未占有一根筷子,则向领域发送一个请求信息
  3)如果哲学家已经占有了筷子,并且受到了一个消息,那么,若筷子是干净的,则他继续占有筷子;如果筷子是脏的,则放弃筷子。如果他要把筷子交出去,则先将筷子置成干净的,再交出去(礼貌期间,洗干净再交出去)
  4)当哲学家吃过饭以后,两根筷子都变成脏的。如果已经有其他哲学家法国请求,则置成干净的,再交给对方。

  5.2 Windows中断与异常
  中断的产生与当前正在执行的指令流并无实质性关联,所以,中断与当前正在执行的线程和进程无关;而异常则是当前指令流中的某些特殊指令产生的结果。
  中断是异步的,而异常是同步的

  当一个中断或异常发生时,处理器利用中断向量号作为索引,定位到IDT中的描述符,如果此描述符指定了一个中断门或陷阱门,则处理器调用该描述符中的制定的中断或异常例程。如果处理例程是在一个更高特权级的代码中执行的,则需要进行栈切换。切换到新栈后,需要把原来被中断的;例程的栈信息压到新栈中。处理器在把控制权交给中断或异常处理例程以前,将EFLAGES、CS和EIP寄存器的状态压到栈中。如果这是一个异常并且有错误码需要保存的话,则把错误码也压到栈中。中断或异常处理例程在完成;了它自己的事情是,恢复EFLAGS寄存器;如果在进入中断或向量处理例程前有栈切换的话,还需要前换回原来被中断的例程执行环境。

  Intel x86处理器将0~31之间的中断向量保留,在小雨32的中断向量中,只有2好是不可屏蔽中断,其他都定义为异常或保留。这里的异常有三种类型,处理器分别有三种做法:
  1)错误(fault):处理器期待异常条件可被修正,它会返回到原来出错的指令处,如Page Fault
  2)陷阱(trap):处理器期待原来的指令流仍然是连续可执行的,这只是一个陷阱而已,所以它会返回到发生异常的指令后面那条指令,如DebugBreakPoint
  3)中止(abort):处理器不期待程序或系统能够从失败中恢复,这通常用于报告严重的错误,如硬件错误或系统数据结构中的不一致性

  软件中通过INT n可以产生中断,且不会被EFLAGS的IF屏蔽掉,但是在产生异常时,即使是Intel x86规定的小于32并且有错误码的异常,处理器也不会把错误码压到栈中。因此如果异常处理例程试图从栈中弹出一个错误码,则会导致后续EIP错位,从文跳转到错误的地方。

  Windows在系统层充分运用处理器的中断能力。本地APIC的初始化工作是在Windows体系结构的HAL模块中完成的,而IDT的许多初始化工作是在Windows内核中完成的。在Windows系统中,内核对中断和异常分别定义了布胡同的系统结构,以便允许内核代码或用户代码扩展中断或异常的处理过程。
  用于处理中断的机制是一种被称为中断对象的内核对象,它允许设备驱动程序为它设备注册一个ISR(中断服务例程)。内核在接收到一个中断时,可以将该中断分发到那些已经为该中断向量注册了中断对象的设备驱动程序中。
  与此同时Windows内核也提供了一套灵活的、可扩展的机制来分发异常,允许内核调试期或进程调试器、内核或应用程序本身,以及环境子系统有机会处理异常。

  当一个中断发生时,其处理过程如下:
  1)中断对象(KINTERRUPT)的DispatchCode代码块首先获得控制,它把该代码块所在的中断对象放到EDI寄存器中,然后跳转到该中断向量对应的中断分发函数
  2)在中断分发函数中,它提升IRQL,获取自旋锁,然后调用中断对象的服务例程,即KINTERRUPT的ServiceRoutine成员,待返回以后,释放自旋锁,恢复IRQL。依据不同的中断分发函数以及中断对象的模式,可能还会处理中断对象链表上的其他中断对象
  3)最后,中断分发函数通过Kei386EoiHelper辅助函数返回。Kei386EoiHelper中的iretd指令结束整个中断处理
  中断对象是一种强大而方便的系统机制,它允许设备驱动程序通过一种可移植的途径茶语它们的中断服务例程,从而支持硬件设备的中断处理。

  DPC(Deferred Procedure Call)运行在DISPATCH_LEVEL上,低于任何一个硬件中断IRQL,所以它优先于任何线程的执行,也屏蔽了线程调度。他是硬件中断和线程调度间的一个IRQL,它可以打断当前线程的执行,凌驾于线程调度器之上,但又不屏蔽任何硬件中断。它的主要用途是供设备驱动程序在相对低IRQL状态下完成I/O任务,以及供内核本身来实现定时器、时限结束处理以及电源失败恢复等任务。
  DPC分为普通的(normal)和线程的(threaded),前者可以在任何一个线程环境中运行;而后者只能在一个专门的DPC线程中运行。DPC为系统全局,每个处理器都有DPC链表。

  Windows最底层的定时器机制是通过时钟中断(APIC来控制的)加上DPC对象来实现的

  APC(Asynchronous Produce Call)专门在APC_LEVEL这个IRQL上运行。每个APC都是在特定的线程环境中运行,从而也一定在特定的进程环境中运行。APC是针对线程的,每个线程都有自己特有的APC链表。头一个线程的APC也是被排队执行的。由于APC_LEVEL高于PASSIVE_LEVEL,所以它优先于普通的线程代码。当一个线程获得控制时,它的APC过程会立刻被执行。APC通常勇士实现各种异步通知事件,如I/O Complete Notification
  APC也分为普通和特殊的。KAPC没有标志做区分,从实现上可以这样区分:特殊APC是指NormalRoutine成员为NULL的APC对象,而普通APC是指NormalRoutine成员不为NULL,但ApcMode成员为Kernel的APC对象。特殊APC和普通APC共用一个APC链表,分别位于链表前部和后部。特殊APC可以抢占普通APC的执行。
  引入APC环境的概念是为了应对在线程被负载到其他进程以后,以及再回来后的情形。


  异常分发:对于程序的指令流而言,异常就是一个同步过程。异常既可以由处理器硬件产生,也可以由指令流软件产生。Windows的异常处理例程是一些名为——KiTrapXX的函数(XX是十六进制的两位向量号)
  IDT中的异常处理例程通过jmp指令跳转到异常分发的入口处,根据不同的参数,跳转点有所不同,但共同的异常分发入口是汇编过程CommonDispatchException
  发生异常时根据处理器模式分别处理:
  对于内核模式异常,如果这是第一次机会处理该异常,即FirstChance参数为True,那么交给内核调试器处理该异常,若内核调试器处理了该异常,则返回,发生异常的指令流继续执行;若不存在内核调试器或内核调试器没处理该异常,则调用RtlDispathException函数,试图将该异常分发到一个基于调用帧的异常处理器(call frame based handler)。只要能找到一个异常处理器能处理此异常,就返回
  对于用户模式异常,首先判断是否是第一次机会处理该异常。如果这是第一次机会,若发生异常的进程(即当前进程)的调试端口为空并且有内核调试期,则交给内核调试期;若调试端口不为空,则发送一个消息至调试端口,然后等待应答。如果调试器处理了异常,则返回,继续执行。否则把异常信息填充到用户栈中,并且设置好用户模式的EIP为KeUserExceptionDispatcher函数指针(指向ntdll.dll中的KiUserExceptionDispatcher函数,在进程管理子系统初始化时设置),准备好该函数所需要的参数,然后,KiDispatchException函数返回
  因此,当从内核模式的异常处理例程返回后,ntdll.dll的KiUserExceptionDispatcher获得控制,它会试图将该异常分发给一个基于调用帧的异常处理器。如果存在一个基于调用帧的异常处理器处理了该异常,则程序继续执行。如果该异常仍然未被处理,则通过NtRaiseException函数再次进入到内核中处理该异常。这一次KiDispatchException函数被调用时,FirstChance参数为False
  如果是第二次机会处理一个异常,并且当前进程由一个调试端口,则发送一个消息至调试端口,然后等待应答。如果进程的调试器处理了该异常,则返回,并继续执行。否则,如果当前进程由一个异常端口,则发送一个消息至异常端口,然后等待应答。如果连接异常端口的环境子系统处理了该异常,则返回,并继续执行。如果该异常仍未被处理,则进程被终止

  5.3 不依赖于线程调度的同步机制
  主要办法有:提升IRQL、互锁操作、无锁的单链表和自旋锁

  提升IRQL实现数据同步:当内核代码要访问一个共享资源时,它只需将ORQL提升到任何有可能访问该资源的中断源的最高IRQL以上,就可以屏蔽掉所有可能的访问冲突(在单核处理器下),如当内核访问有关线程调度信息时。在多处理器系统上,还需要使用其他的互斥访问机制来配合。

  互锁操作:InterlockedXXX

  无锁的单链表实现:对于频繁使用的共享资源有重要的意义,它使得多个处理器以极其高效的方式共享一个链表。Windows的内存管理器和I/O管理器是无锁单链表的主要客户。

  自旋锁:不适合在单处理器上工作,在DISPATCH_LEVEL或更高的ORQL上使用,使用时需要注意以下几点:
  1)任何一段代码,其抓住自旋锁的时间不能过长,否则可能会严重影响多处理器的性能
  2)若内核代码持有了一个自旋锁,则它既不能引发页面错误,也不能调度线程,否则会导致系统崩溃(蓝屏)
  3)用户程序不能使用自旋锁,因为它们运行在PASSIVE_LEVEL上

  5.4 基于线程调度的同步机制
  Windows的线程等待机制,核心是通过KWAIT_BLOCK对象将线程对象与分发器对象关联起来。在这一等待机制中,分发器对象由DISPATCH_HEADER来描述,其中Type域决定了分发器对象的类型;DISPATCHER_HEADER只是分发器对象的头部,它的对象体(body)紧跟在头部苟免。

  Windows支持的8种分发器对象:
  1)事件(KEVENT)(同步/通知)
  2)突变体(KMUTANT)
  3)信号量(KSEMAPHONE)
  4)进程(KPROCESS)
  5)线程(KTHREAD)
  6)队列(KQUEUE)
  7)门(KGATE)
  8)定时器(KTIMER)(同步/通知)

  门等待是是Windows提供的一套更加轻量级的等待机制,是Windows标准等待机制的一个简化,它避免了一个线程从进入等待函数到离开等待函数过程中的许多步骤。唤醒一个门等待的线程几乎是以最快捷的方式来进行的,它直接调用KiDeferredReadyThread函数将该线程分发到某个处理器上准备执行。

  快速互斥体(fast mutex)建立在事件对象(Intel x86平台)或门对象(非Intel x86平台)的基础上,因此实现比较简捷,其数据结构为FAST_MUTEX。由于当不存在竞争时,它不必等待事件对象或门对象,因此快速互斥体提供更好的性能
  执行体资源(executive resource)建立在内核的事件对象和信号量对象基础上,它利用事件对象来实现互斥控制,利用信号量对象来实现共享访问控制。其数据结构为ERESOURCE。执行体资源对象位于非换页内存池,它本质上实现了一个读写锁。
  推锁(push lock)是一个很小的数据结构,在32/64位上为一个32/64位证书,并且可以从换页内存区分配。数据结构为_EX_PUSH_LOCK。推锁是Windows内部使用的读写锁,它利用InterlockedCompareExchangePointer函数来更新自己的状态,从而避免多个处理器同时操作推锁时产生状态不一致的情形。另外它利用门对象和门等待带来解决锁竞争的问题。相比较执行体资源使用自旋锁来保护对象的状态,使用事件和信号量对象来实现互斥和共享语义,推锁有更好的性能和绳索能力。在执行体进程对象和线程对象中的进程所和线程锁,以及进程的工作集锁,还有对象管理器等都使用推锁来保护其数据结构


第6章 Windows I/O系统
  6.1 I/O概述
  对于计算机的处理器来说,I/O硬件设备只是一些符合某些接口规范的控制器而已,处理器并不直接与设备打交道,而是向设备控制器发号施令,或者接收设备控制器的通知或命令。处理器与控制器之间通过总线进行通信,而控制器与设备之间的通信则往往通过专门的接口进行。
  按照传统的分类方法,I/O设备可以分为块设备和字符设备。块设备的寻址和读写操作都是以块为单位进行的。另一类是字符设备,它们接收或发送的数据是字节流。而一些外部设备如时钟发生器、电源控制器等不是上述分类的任何一种。

  设备控制器中往往有一些状态寄存器或者命令寄存器,因而处理器可以通过操纵这些寄存器,从而控制相应的设备,比如告诉控制器发送数据、接收数据、打开或关闭某些特定的功能。所以,操作系统的任务是根据设备的工作模式,读或写这些寄存器,从而达到控制外部设备的目的。
  设备控制器中的寄存器除了通过I/O端口来访问以外,也可以被映射到系统内存空间(这种方案被称为内存映射I/O)。

  处理器通过in/out指令或者直接读写内存的方式与各种控制器打交道,而设备在执行任务时,一般通过2种方式把任务的状态反馈给处理器:
  1)设备控制器提供一个状态寄存器,因而处理器可以定期或者用忙等待(busy-wait)的方式检查此寄存器(除非要等待的时间预计不是很长,否则忙等待的做法应该避免)
  2)设备用中断的方式来通知处理器。当处理器接收到中断信号时,它可以根据中断的设置或者寄存器的状态,来做相应的处理

  直接内存访问(DMA,Direct Memory Access),这是现代计算机通常采用的方案,DMA需要硬件支持,它也是一种控制器。既可以共享一个DMA控制器,也可以让一个设备,比如磁盘,有单独的DMA控制器(取决于硬件设计)。DMA通过系统总线来传输数据,但是在它传输数据时不占用处理器的指令周期,所以,使用DMA可以将处理器解放出来。当数据传输完成时,DMA控制器以中断的方式通知处理器。

  简单小结一下:
  在现代计算机体系结构中,处理器通过I/O端口或者内存映射I/O的方式与硬件设备的控制器打交道,而非直接操纵硬件设备。Intel x86处理器提供了in/out指令来访问I/O端口空间,并且也支持内存映射I/O。为了在内存与设备之间传输大量数据,现代计算机往往采用DMA结构,以便将处理器从数据传输任务中解脱出来。当DMA数据传输完成时,DMA控制器以中断的方式通知处理器。

  I/O软件的模型设计是设备无关的,必须有足够的通用性,能够把I/O模型的硬件特性涵盖到模型之中。另外,操作系统必须提供有效的管理手段,从而让设备的软件组件融入到系统的I/O处理框架中,这样,这些软件组件可以专注于针对特定设备的功能需求。而不必日过多地考虑与系统打交道或者与系统中其他模块的写作。
  设备的软件组件通常称为设备驱动程序(device driver),在设计的时候,需要注意一下的几个事项:
  1)与设备的通信是同步或异步方式
  2)缓冲区管理
  3)共享和独占设备
  4)设备的状态管理
  设备驱动程序与操作系统之间应该是互补关系。设备驱动程序为操作系统提供了必要的访问硬件设备的能力,同时也依赖于操作系统提供的软件功能,比如内存管理、错误处理、同步互斥机制等。在现代操作系统中,诸如即插即用、电源管理等特性越来越重要,操作系统的职责是提供一个符合即插即用和电源管理工业标准的软件框架。

  Windows I/O的系统结构由五个部分组成:I/O管理器、即插即用管理器、电源管理器、WMI例程、以及设备驱动程序。其中I/O管理器是整个I/O系统的核心,它定义了一个非常通用的框架。允许各种功能的设备驱动程序(设备相关或者设备无关)容纳于其中。
  Windows设备驱动程序可以直接访问硬件,或者通过硬件抽象层(HAL)访问硬件。
  Windows提供分层驱动程序模型,不仅提供了对硬件设备的灵活控制,而且也允许多个驱动程序协同完成I/O任务。
  Windows I/O系统非常依赖注册表,注册表是由Windows内核中的配置管理器来实现的。

  6.2 I/O管理器
  在I/O系统中,有3种基本内核对象:
  1)驱动程序对象,代表了一个设备驱动程序被加载到系统中以后的内部表示
  2)设备对象,代表了系统中的一个设备,它既可以是物理设备,也可以是逻辑设备
  3)文件对象,代表了一个设备对象被打开后的实例
  I/O管理器负责管理和协调这三种对象,并建立起它们之间的关系

  文件对象是I/O操作的基本抽象,它将应用程序对设备的操作抽象成了对文件数据的读或写操作。文件对象作为设备对象的已打开实例,不仅可以代表设备(比如磁盘)中的文件,还可以代表任何其他设备,包括逻辑设备商的操作实例。
  如同对象管理器中的其他对象一样,进程对文件对象的访问要经过系统的安全饮用监视器(SRM,Security Reference Monitor)的检查。文件设备不承担设备对象的数据存储和状态变迁,真正的文件数据位于设备对象而非文件对象中。多个文件对象可以指向同一个设备对象,因而它们共享同样的设备对象(所以对文件对象的操作有必要进行同步)。

  6.3 即插即用管理器
  即插即用管理器负责在内核中对硬件设备的即插即用提供支持,其职责是:
  1)自动检测设备的插入和移除,既可能在系统引导时,也可能在系统运行过程中
  2)动态地分配硬件资源,包括中断向量、I/O端口、I/O寄存器以及与总线有关的资源,以避免设备之间产生资源冲突
  3)指示I/O管理器为设备加载正确的驱动程序,必要时通过一个用户模式的应用程序允许用户制定或搜索驱动程序
  4)向内核及应用程序提供有关设备插入或移除通知

  6.4 电源管理器
  电源管理器目前采用的工业标准是ACPI(Advanced Configuration and Power Interface)
  在Windows中,电源管理是由I/O系统的电源管理器和设备驱动程序配合完成的,电源管理器根据以下一些条件来做出电源状态转移的决定:
  1)系统的活动情况
  2)系统的电池使用情况
  3)用户的软操作,如停机、休眠或睡眠动作
  4)用户的硬操作,例如按下电源开关,合上或打开笔记本屏幕

  6.5 设备驱动程序
  在Windows I/O系统中,设备驱动程序不仅为操作系统提供了支持各种I/O设备的能力,也是Windows内核本身扩展的基础。Windows可以动态地加载或卸载设备驱动程序,通过这些驱动程序来调整或扩展内核的功能。Windows I/O系统规定了设备驱动程序应遵循的结构,这组接口是通用的,可是用于所有的内核模式驱动程序。设备驱动程序依据其用途不同,可以分为以下三类:
  1)即插即用驱动程序,也称为WDM驱动程序。它们通常是为了驱动硬件设备而由硬件厂商提供,与Windows 的I/O管理器、即插即用管理器和点于啊管理器一起工作
  2)内核扩展驱动程序,也称为非即插即用驱动程序。它们扩展内核的功能,或者提供了访问内核模式代码和数据的一种途径。
  3)文件系统驱动程序。它们接收针对文件的I/O请求,再进一步将这些请求变成真正对于存储设备或网络设备的I/O请求,从而满足客户的原始请求。

  WDM在I/O模型中增加了对即插即用、电源管理和Windows管理规范(WMI)的支持。可以进一步划分成以下三类:
  1)总线驱动程序。管理一个总线设备,负责监测总线上负载的所有设备,并通知即插即用管理器关于这些设备的情况。总线驱动程序也负责总线的电源管理。(总线是指可供其他设备负载的设备,其中既有像PCI和SCSI这样的额物理总线设备,也有像HAL这样的虚拟总线设备。
  2)功能驱动程序。管理具体的设备,在一个设备的设备栈中,功能驱动程序创建的设备对象(即FDO)相当于操作系统控制该设备的逻辑接口。功能驱动程序是实际管理该设备的功能模块。
  3)过滤驱动程序。在设备栈中,过滤驱动程序位于功能驱动程序之上或之下,它的用途是:监视一个设备的I/O请求以及这些请求的处理情况,或者,增加或改变一个设备或另一个驱动程序的行为。

  6.6 I/O处理
  IRP是Windows系统中勇于表达一个I/O请求的核心数据结构,当内核模式代码要发起一个I/O请求时,它会构造一个IRP,用于在处理该I/O请求的过程中代表该请求。
  IRP对象从一个I/O请求被发起时开始存在,一直到该I/O请求被完成或者取消为止,在此过程中,会有多方操纵此IRP对象,包括I/O管理器、即插即用管理器、电源管理器以及一个或多个驱动程序等。Windows I/O系统本质上支持异步I/O请求,所以IRP对象必须携带足够多的环境信息,以便能够描述一个I/O请求的所有状态。
  IRP对象的设计特点:
  1)所有的I/O请求都被抽象成针对文件对象的操作,OriginalFileObject成员记录了该文件对象,代表一个I/O请求的目标。
  2)栈单元。由于可能有多个程序依次参与到一个I/O请求的处理过程中,所以,一个IRP对象除了以上列出的IRP数据结构,其后还有一个称为栈单元(Stack Location)的数组。每个参与处理的驱动程序都可以有它自己的栈单元。
  3)支持取消。线程可以指定一个取消例程,以便当I/O请求被取消时可以执行相关的动作。
  4)I/O请求的完成。这是一个显式的通知,当一个驱动程序的分发例程认为它已经完成了该I/O请求时,它会显式地告诉I/O管理器,此I/O请求已完成。参与处理的驱动程序可以为I/O请求注册一个完成例程,从而当它的下级驱动程序完成了该I/O请求时,此完成例程会被调用。

  一个I/O请求的基本处理流程:Windows的系统服务例程接收到来自应用程序的I/O请求,然后,它们或者通过快速I/O通道处理此请求,或者通过I/O管理器的IoCallDriver函数将它交给驱动程序进行处理。

  I/O完成端口(I/O Completion Port)机制:能够让I/O的完成处理交由一个专门的线程池来完成,而线程池的线程数量是一个可配置的参数。这种做法将I/O请求的发起动作与完成处理分离到不同的线程中,通过调节I/O完成端口的并行度(即线程池的线程数)使得系统处理客户请求的吞吐量最大化。
  


第7章 Windows 存储管理
  7.1 存储管理概述
  在Intel x86存储体系结构如下:
  1)寄存器:这是处理器内部定义的存储体,它们除了存储功能,往往还兼有其他的能力,比如参与运算、地址解析、指示处理器的状态,等等。
  2)L1缓存:这是靠近处理器的缓存,它存放的是处理器最近访问过的内存数据。缓存通常是由SRAM(静态随机访问存储器)来实现,而系统的主内存则是由DRAM(动态随机访问处理器)来实现的。
  3)L2缓存:如同L1缓存一样,它也是由SRAM来实现的,只不过更加靠近系统的主内存而不是处理器。与L1缓存不同的是,L2缓存是用物理地址来寻址的。
  4)主内存:或简称内存。在物理上,内存是由DRAM来实现的,它比L1和L2缓存慢,但容量要大太多。一个系统中正在使用的数据,包括系统本身的数据,以及各个应用程序用到的数据,都存放在内存中。
  5)本地一级存储设备和远程存储设备:在现代计算机系统中,像硬盘这样的本地永久存储设备仍然是不可或缺的,操作系统所需要的各个模块和数据都存放在硬盘上,应用程序所需要的可执行文件和数据通常也存放在硬盘上。本地一级存储设备还包括像光盘、USB这样的可移动存储介质。
  6)本地二级存储和远程存储:比如用磁带设备来备份一个系统中的数据,或者通过索引库来访问一个包含大量光碟的影像资料库。二级存储也可能是远程的,比如将数据导入到一个分布式存储系统中,或者连接一个网络文件系统。
  在以上存储体系中,下层存储体包含了上层存储体的内容。系统或应用程序在访问数据时,若能够在上层存储体中获得,则无须再往下读取数据,否则需要逐层向下寻找数据。
  L1、L2缓存位于处理器内部,它们的工作方式对于处理器中的指令是透明的,所以,操作系统并不需要显式地管理L1和L2缓存的内容,处理器会自动维护这两极缓存。本地一级、二级存储体和远程存储体都是通过设备来支持的,所以,这些存储体的管理需要由设备驱动程序来完成,操作系统往往只是提供一个框架。

  7.2 Windows 缓存管理
  Windows的缓存管理器介于内存管理器与文件系统驱动程序之间,它包含一组以“Cc”打头的内核模式函数、全局变量以及一些系统线程。缓存管理器使用了系统地址空间中预留的一段地址范围,并且控制哪些文件的哪些部分可以使用这些地址和相应的物理内存,以达到在大量文件请求和系统有限内存资源之间的一种平衡。
  缓存管理器有预读的能力,文件对象的私有缓存表记录了最近两次读访问的历史信息,缓存管理器利用这两次读访问的信息来预测下一次读请求,并提前完成这一读操作。另一方面,由于缓存管理器通过页面错误来读取文件数据,因此,它也指示页面错误处理例程,一次读入足够的页面数量。
  缓存管理器的写操作通常是在后台以异步方式完成的。这意味着被写到文件中的数据首先存放在视图页面中。除非缓存管理器显式地指示要写到磁盘上(譬如等待方式的CcCopyWrite调用),否则,这些数据将被统一按盘,等积累到一定程度以后才被刷新到存储设备中,这样做的目的是减少硬件I/O操作的总量。同时缓存管理器也支持直写(write-through)方式。
  除了文件系统,任何访问文件数据流的内核组件都可以是缓存管理器的客户。除了文件系统驱动程序以外,像网络重定向器(network redirector)和网络文件服务器内核组件,以及一些需要使用文件系统接口的驱动程序和内核组件(比如配置管理器),也可以使用缓存管理器提供的接口函数。

  7.3 Windows 中卷的管理
  在Windows中,卷有两种:简单卷(simple volume)和多分区卷(multipartition volume)。简单卷对应于一个分区,它管理该分区中的全部扇区或部分连续扇区;而多分区卷管理多个分区,将这些分区中的扇区统一起来管理。因此,卷是扇区的逻辑集合,这些扇区可能位于一个磁盘上,也可能来自多个磁盘。
  在Windows系统上管理一个基本磁盘所涉及的设备驱动程序:从I/O管理器接收到应用程序的I/O请求开始,经过文件系统设备栈(fltMgr.sys、ntfs.sys)、卷管理设备栈(volsnap.sys、ftdisk.sys)、磁盘管理(partmgr.sys、disk.sys、acpi.sys、atapi.sys)设备栈到达磁盘的控制器。
  给卷分配一个驱动器字母并不等于该卷的数据有了文件格式,或者说卷有了对应的文件系统。卷上的数据是以扇区来管理的,而文件系统则为扇区中的数据定义了结构信息。因此,为了能以文件和目录的方式来访问一个卷上的数据,该卷必须被关联上一个文件系统,此过程被称为卷识别(volume recognition)。

  7.4 Windows 文件系统
  Windows文件系统为存储设备提供了流方式的数据管理,允许应用程序共享卷的存储空间,同时又可以独享不同的数据流。
  在文件系统中,通常有两种类型的对象:目录(directory)和文件(file)。按照通常的定义,文件代表了一个数据流,应用程序可以从文件中读写数据。目录是文件系统中用于将多个文件有效地组织起来的容器对象,其本身并不包含用户数据流;相反地,作为一个容器对象,目录可以容纳文件,甚至目录(子目录)。
  对于在系统运行过程中被引用的文件或目录,文件系统驱动程序必须使用恰当的内存数据结构来描述它们,分别称为FCB(File Control Block)和DCB(Directoey Control Block)。文件系统驱动程序也可以使用统一的数据结构FCB,既代表磁盘文件,也代表磁盘目录,这完全取决于驱动程序自身。每一个磁盘文件或目录必须至多只有一个FCB或DCB存在,每一个FCB或DCB都确定地代表了层次结构中的一个文件或目录。
  在Windows文件系统驱动程序中,另一个数据结构是CCB(Context Control Block),它代表了一个应用程序已打开一个文件。CCB与FCB的不同之处是,FCB针对每个文件而唯一存在,而CCB针对每一次打开文件而存在。
  在文件系统驱动程序接收到IRP_MJ_CREATE请求时,如果文件对象名称中指定的文件商务对应的FCB,那么,驱动程序会创建一个FCB;如果该文件已经有一个FCB了,那么,驱动程序将不会重复地为一个文件创建FCB,它会在FCB中用一个引用针数来控制FCB的生命周期。因此,驱动程序在接收到创建请求时,必须有办法检测到目标文件是否已存在FCB。另外,驱动程序对CCB的处理方式与此不同,只要能成功地打开指定的文件,则驱动程序总是要构造CCB对象。

  Windows内核本身提供了一个成为RAW的文件系统,代表了无格式的文件系统,也就是说,它不假定磁盘分区的扇区具有任何特定的格式。同时Windows提供了一套针对文件系统的支持库,称为文件系统运行库(File System Runtime Libaray,简称FsRtl)



第8章 Windows 系统服务
  8.1 Windows 系统服务原理
  早期Intel使用中断门来实现模式切换,中断号为0x2e;或者使用系统异常来完成从用户模式切换到内核模式;到了PII处理器开始,Intel引入了sysenter/sysexit实现了快速切换

  sysenter指令的内部逻辑是:
  1)将IA32_SYSENTER_CS和IA32_SYSENTER_EIP分别装载到CS和EIP寄存器中;将IA32_SYSENTER_CS+8和IA32_SYSENTER_ESP分别装载到SS和EIP寄存器中
  2)切换到Ring0
  3)清除EFLAGS中的VM标志(虚拟8086模式)
  4)执行目标例程
  sysenter指令对于跳转目标的代码段和栈段的要求:
  1)代码段和栈段在GDT中必须是相邻的,IA32_SYSENTER_CS指向代码段的GDT表项,紧随其后的表项应该是栈段描述符
  2)代码段描述符指定的是一个基地址为0,段范围可达4GB的Ring0的段,具有执行和读访问权限
  3)栈段描述符制定的也是一个基地址为0、段范围可达4GB的Ring0的段,但具有读写访问和向上扩展的权限

  sysexit指令的内部逻辑是:
  1)将IA32_SYSENTER_CS+16和EDX分别装载到CS和EIP寄存器中;将IA32_SYSENTER_CS+24和ECX分别装载到SS和ESP寄存器中
  2)切换到Ring3
  3)执行EIP中指定的用户模式代码

  操作系统判断处理器是否支持sysenter/sysexit:当通过EAX=1来执行处理器的CPUID指令时,处理器的特征信息被存放在ECX和EDX寄存器中,其中EDX包含了一个SEP位(SysEnter/SysExit Present),该位指明了当前处理器是否支持sysenter/sysexit指令

  在Windows的虚拟地址空间管理中,系统空间有一个特殊的页面会被映射到每个进程空间中,也就是说,该页面是系统地址空间和进程地址空间共享的。它在进程地址空间中的地址是0xffdf0000,即宏定义KI_USER_SHARED_DATA;在进程地址空间中的地址是0x7ffe0000,即宏定义MM_SHARED_USER_DATA_VA。此共享页面记录了当前系统的一些关键信息。

  8.2 LPC 服务
  LPC(Local/Lightweight Procedure Call,本地过程调用)是一种直接由内核支持的进程间通信机制,主要用于操作系统各个组件之间进行通信,或者用户模式程序与系统组件之间通信。LPC涉及两方通信,其基本工作方式是消息传递。一个进程创建一个LPC端口对象,然后等待其他进程连接过来;当其他进程成功地连接到LPC端口以后,两者便可以开始通信。
  LPC允许两个进程进行双向通信,它在Windows中的主要应用如下:
  1)Windows应用程序与系统进程,包括WIndows环境子系统之间的通信,这通常发上在一些Windows API函数的内部
  2)用户模式程序与内核模式组件之间的通信,比如LSASS(Local Security Authority Subsystem)进程与SRM(Security Reference Monitor)之间的通信就是通过LPC来完成的
  3)当RPC的两端在同一系统中时,RPC通信转化为LPC通信

  通过LPC进行通信的两个进程本质上是C/S模型,其工作方式与基于连接的Socket编程模型相仿:
  1)S进程创建一个LPC端口对象(LPCP_PORT_OBJECT)
  2)在该端口上监听连接请求
  3)C进程根据已知的端口名称,请求连接到此端口对象上
  4)S进程接收到连接请求后,创建一个通信端口对象,用以代表与C进程之间的LPC连接;同时C进程也会创建一个通信端口对象,代表它与S进程之间的LPC连接
  通信端口对象没有名称,它们是私有对象,所以,除了服务器进程和客户进程通过通信端口句柄来访问它们意外,其他进程无法访问。LPC连接端口对象属于服务器进程,它们有公开的名称,通常被加入到系统的对象管理器目录中,所以,其他进程可以通过名称访问它们,从而建立起单独的LPC连接。LPC允许一对多通信模型,LPC连接端口对象是一个中心的连接点,它只接受连接请求,不接受数据请求。而通信端口可以进行任意的数据请求和服务。

  当服务器进程和客户进程间了LPC连接以后,它们可以给对方发送消息。每个端口对象都有一个消息队列,用来保存发送给该端口对象的消息,使用信号量来同步发送和接受操作。
  LPC允许使用2种方法来传输数据:
  1)当最大消息长度不超过256字节时,要传输的数据直接跟在消息头后面。发送进程将数据拷贝到系统地址空间中,然后接受进程将数据拷贝到它的进程地址空间中。
  2)若最大消息长度超过256字节,那么,客户在连接到端口对象上时,可以指定一个内存区对象,并且在客户进程中映射一个视图,以用于向服务器发送最大数据;服务器进程在接受连接时,也映射一个视图。而且,服务器也可以指定一个内存区对象,用于向客户发送大数据。因此,两个进程通过共享内存区来传输数据。

  8.3 命名管道(Named Pipe)服务
  Windows 命名管道服务是一种可用于在不同系统之间进行网络通信的机制,它提供了双向通信的能力,与Windows 的文件对象管理紧密地集成在一起。它使用了Windows安全机制,所以,命名管道的服务端可以控制哪些可恶有权与它建立连接。利用命名管道服务,不同机器上的进程可以相互进行通信,命名管道依赖于低层的网络接口,包括DNS服务、TCP/IP协议等。
  命名管道的名称格式为“\<Server>Pipe<PipeNames>”
  如同LPC一样,命名管道的通信也是以连接方式进行的,服务器创建一个命名管道对象,然后在此对象上等待连接请求。一旦客户链接过来,则两者都可以通过命名管道读或写数据。与LPC不同的是,命名管道本质上使用文件接口,它的实现使用了文件系统驱动程序的形式,而且它支持远程通信。

  8.4 邮件槽(Mailslot)服务
  邮件槽是Windows提供的另一种跨进程通信手段,与命名管道不同的是,它提供的是不可靠的单向数据传输服务,并且支持广播。如同命名管道服务一样,邮件槽服务也与Windows的文件对象管理与安全检查机制紧密地集成在一起,服务器只能创建本地的邮件槽。由于邮件槽实现上的原因,客户端发送的消息只有当小于425字节时,才可以被广播给多个服务器。如果消息长度大于425字节,那么,邮件槽将使用可靠的通信机制,这就要求建立起客户/服务器之间的端到端连接,因而在这种情形下邮件槽不支持广播通信。


第9章 Windows 系统高级话题
  9.1 网络
  Windows平台上典型的网络API:
  1)Winsock
  2)WinInet
  3)Name Pipe/Mailslot
  4)NetBIOS
  5)RPC

  Windows在API驱动程序(如Winsock的驱动程序afd.sys)与协议驱动程序(如TCP/IP的驱动程序tcpip.sys)之间规定了一个接口标准,称为传输驱动程序接口(TDI,Transport Driver Interface,tdi.sys),从而使网络API驱动程序可以按照TDI接口来调用更底层的协议驱动程序。本质上,TDI是将各种网络请求转变成一种规范的IRP描述。

  在Windows的网络栈中,网络协议与网络适配器是分离的,协议驱动程序并不针对特定的网络适配器而设计,然而,当协议驱动程序真正运行时,它必须通过一个网络适配器才能发送和接受数据。协议驱动程序通过统一开发的接口与网络适配器驱动程序进行通信,这就是NDIS(Network Driver Interface Specification,网络驱动程序接口规范)。
  NDIS库并没有使用Windows I/O管理器的接口规范,而是自己定义了一套开发接口。所以,NDIS驱动程序不是真正的Windows驱动程序,它们并不接收和处理IRP。详单地,TDI传输器调用NDIS库的函数来构造一个NDIS包,再通过NDIS库的发送函数将NDIS包传送给NDIS驱动程序。
  在NDIS体系结构中,有一种特殊的驱动程序起着协议过滤器的作用,这便是NDIS中间驱动程序(NDIS Intermediate Driver)。它位于TDI传输器(即协议驱动程序)和NDIS驱动程序之间:对于NDIS驱动程序来说,它们就好像是一个协议驱动程序;而对协议驱动程序来说,它们就好像是一个NDIS驱动程序。因此,NDIS中间驱动程序必须同时暴露着两种驱动程序所要求的编程接口,以便能够与上层的协议驱动程序和下层的NDIS驱动程序进行通信。

  9.2 Windows 子系统
  Windows子系统是Windows操作系统不可分割的一部分,它在Windows内核的基础上,为应用程序提供了一个图形用户界面(GUI)环境
  WIndows子系统包含以下组件:
  1)内核模块win32k.sys,虽然它的名称像是一个驱动程序,但实际上,win32k.sys并不遵从I/O管理器定义的程序模型,而仅仅是Windows内核的扩展而已。win32k.sys包含两大功能组成部分:窗口管理器与GDI
  2)图形设备驱动程序:显示驱动程序、打印驱动程序以及食品小端口驱动程序
  3)Windows环境子系统进程(csrss.exe),它支持控制团窗口、创建和深处进程线程、支持16位虚拟DOS机进程以及其他一些函数(GetTempFile,DefinedDosDevice,ExitWindowsEx以及少量自然语言支持函数)
  4)子系统DLL,例如user32.dll、advapi32.dll、gdi32.dll和kernel32.dll,它们实现了已经文档化的Windows API函数,可能需要调用内核模块ntoskrnl.exe和win32k.sys中未文档化的系统服务,甚至与环境子系统进程通信

  9.3 内核日志
  Windows内核中的WMI驱动程序(其名称为WMIxWDM)实现了一个内核日志记录器。内核日志记录器中的事件触发点散布在内核各处。
  以下一些典型的性能问题可以通过ETW内核日志来诊断:
  1)对用户动作的响应迟缓
  2)应用程序中的长时间延迟
  3)搞CPU使用率
  4)开关状态转移很慢
  5)电池寿命比预期的短

  9.4 NT6的重要变化
  MinWin:这是一个工程项目,其目标是将Windows(不仅仅内核,但内核是极为重要的部分)的模块梳理出来,以求理解和驾驭模块之间的依赖性,从而可以构建出一个最小的操作系统核,并在此基础上根据需要添加新的功能和模块。
  MinWin项目本质上是代码重构,它试图将Windows系统中的模块一一种更加清晰和自然的方式组合在一起,因而当Windows继续发展时,Microsoft的开发人员可以更加清楚地知道哪些模块依赖于其他那些模块,以及它们对于特定用途的Windows系统是否必要。

  NT6的主要变化在以下几个方面:
  1)进程和线程管理:线程时限计算更加精确、多媒体类别的调度服务、用户模式调度(允许应用程序调度它的工作线程)
  2)内存管理:动态内核地址空间、内存优先级、SuperFetch
  3)I/O处理:I/O完成端口、全面支持I/O取消、I/O优先处理、基于文件的符号链接

展开全文


推荐文章

猜你喜欢

附近的人在看

推荐阅读

拓展阅读