目录
概述
x86
1978年6月8日,Intel发布了新款16位微处理器“8086”,也同时开创了一个新时代:x86架构诞生了。x86指的是特定微处理器执行的一些计算机语言指令集,定义了芯片的基本使用规则,一如今天的x64、IA64等。
操作系统
操作系统作为硬件平台上最重要的软件,对下负责管理平台硬件,对上向应用程序提供标准接口。操作系统中最重要的部分称为操作系统内核,运行在CPU最高的特权级上,可以访问系统的一切资源,称操作系统内核运行的状态为内核态。应用程序通常运行在CPU最低的特权级上,只能访问部分资源,此种状态称为用户态。
x86内存架构
内存架构往往是硬件架构中最为复杂的部分。不夸张地说,理解了内存架构,就理解了现代计算机体系架构的大部分内容。
地址空间
如果把内存比作一个大数组,地址就是这个数组的索引。与之类似,地址空间则是一个更大的数组,它是所有可用资源的集合,同样,地址是这个数组的索引。
地址空间可以划分成物理地址空间和线性地址空间两种类型。
物理地址空间
硬件平台可以粗略地划分成三个部分:CPU、内存和其他硬件设备。其中,CPU是平台的主导者,从CPU的角度来看,内存和其他硬件设备都是可以使用的资源。这些资源组合在一起,分布在CPU的物理地址空间内,CPU使用物理地址索引这些资源。物理地址空间的大小,由CPU实现的物理地址位数决定,物理地址位数和CPU处理数据的能力(CPU位数)没有必然联系。
假设一个平台,CPU的物理地址空间为4GB,有512MB内存,其他硬件设备的I/O寄存器被映射到512MB的I/O地址内,则该平台的物理地址空间可能是这样划分的。
从上图可以看出,512MB内存和I/O地址只占用物理地址空间的一部分,还有大部分处于空闲。有一个4GB大小的数组,其中1GB的元素具有有效值(512MB内存、512MB I/O地址),其他元素不存在。
线性地址空间
一个平台只有一个物理地址空间,但是每个程序都认为自己独享整个平台的硬件资源,为了让多个程序能够相互隔离和使用物理地址空间的资源,线性地址空间的概念被引入了。
和物理地址空间一样,线性地址空间的大小取决于CPU实现的线性地址位数,例如实现了32位线性地址的CPU具有4GB大小的线性地址空间。
线性地址空间会被映射到物理地址空间某一部分或整个物理地址空间。CPU负责将线性地址转换成物理地址,使程序能够正确访问到该线性地址空间所映射到的物理地址空间。一个平台上可以有多个线性地址空间,在现代操作系统中,每个进程通常都拥有自己的私有线性地址空间。
一个典型的线性地址空间构造如下。
地址
地址是访问地址空间的索引。根据访问的地址空间不同,索引可以分为线性地址和物理地址,但由于x86特殊的分段机制,还有一种额外的地址——逻辑地址。
逻辑地址
该地址即程序直接使用的地址。例如下面的程序。
1 | int a = 1; |
这里,指针变量p中存储的即是一个逻辑地址。逻辑地址由一个16位的段选择符和一个32位的偏移量(32位平台)构成。在这例子中,指针变量p实际存储的是逻辑地址的偏移部分,该偏移对应的段选择符位于段寄存器中,并没有在程序中反映出来。
线性地址
又称虚拟地址,32位无符号整数。线性地址是逻辑地址转换后的结果,用于索引线性地址空间。当CPU使用分页机制时,线性地址必须转换成物理地址才能访问平台内存或硬件设备;当分页机制未启用时,线性地址等于物理地址。
物理地址
该地址索引物理地址空间,是CPU提交到总线用于访问平台内存和硬件设备的最终地址。
它和上面两个地址有如下关系。
某些书籍中还有总线地址的叫法,这是因为给设备寄存器分配的物理地址和寄存器在设备上的地址是不同的(通常设备的寄存器都认为自己是从地址0开始的),两者之间存在一个映射关系,由设备的电子线路负责转换并对CPU透明。由于CPU用于访问设备的物理地址是设备寄存器展现给总线的地址,所以在x86下有时也称物理地址为总线地址。
内存管理机制
分段机制
分段是一种朴素的内存管理机制,它将内存划分成起始地址(Base)和长度(Limit)描述的块,这些内存块就被称为“段”。
分段机制由4个基本部分构成:逻辑地址、段选择寄存器、段描述符和段描述符表。其核心思想是:使用段描述符描述段的基地址、长度以及各种属性(读写属性、访问权限等)。当程序使用逻辑地址访问内存的某个部分时,CPU通过逻辑地址中的段选择符,索引段描述符表以得到该内存对应的段描述符,并检测程序的访问是否合法,如合法,根据段描述符中的基地址将逻辑地址转换为线性地址。分段机制的流程如下。
段选择符(Segment Selector)
段选择符是逻辑地址的一个组成部分,共16位,用于索引段描述表以获得该段对应的段描述符。
段选择符作为逻辑地址的一部分,对程序是可见的。但通常段描述符的修改和分配由连接器和加载器完成,而不是应用程序本身。
为了使CPU能够快速地获得段选择符,x86架构提供了6个段寄存器(segment register)用于存放在当前程序的各个段的段选择符。
- CS(code-segment,代码段):存放代码段的段选择符。
- DS(data-segment,数据段):存放数据段的段选择符。
- SS(stack-segment,栈段):存放栈的段选择符。
- ES、FS、GS:供程序自由使用,可以存放额外3个数据段的段选择符。
通常程序只使用CS、DS、SS三个段寄存器。
段描述符(Segment Descriptor)
每个段描述符的长度是8字节,含有3个主要字段:段基地址、段限长和段属性。
- L: 64-bit code segment(IA-32e mode only)
- AVL: Available for use by system software
- BASE: Segment base address
- D/B: Default operation size(0 = 16-bit segment; 1 = 32-bit segment)
- DPL: Descriptor privilege level
- G: Granularity
- LIMIT: Segment limit
- P: Segment present
- S: Descriptor type(0 = system, 1 = code or data)
- TYPE: Segment type
- E: Expansion Direction
- W: Writable
- A: Accessed
- C: Conforming
- R: Readable
- A: Accessed
Limit
段限长字段Limit(Segment limit field):用于指定段的长度。处理器会把段描述符中两个段限长字段组合成一个20位的值,并根据颗粒粒度标志G来指定段限长Limit的值的实际含义。
- G = 0,则Limit范围是1B~1MB,单位是1B。
- G = 1,则Limit范围是4KB~4GB,单位是4KB。
根据段类型Type的段扩展方向标志E,处理器会以两种不同的方式使用Limit:
- 对于向上扩展的段(expand-up segment,上扩段),逻辑地址中的偏移范围可以是从0到段限长值Limit。大于段限长Limit的偏移值将产生一般保护性异常(general-protection exception,SS段寄存器除外)或产生栈错误异常(stack-fault exception)。
- 对于下扩段,减小Limit的值会在该段地址空间底部分配新的内存,而不是在顶部分配。
IA-32架构的栈总是向下扩展的,因此这种实现凡是很适合堆栈。
Base
基础地址字段Base(Base address field):该字段定义在4GB线性地址空间中一个段字节0所处的位置。处理器会把3个分立的基地址字段组合成32位的值。段基地址应该对齐16字节边界。16字节对齐不是必须的,但对齐在16字节边界上可以使得程序能发挥最大化性能。
S
描述符类型标示S(Descriptor type field):用于指明一个段描述符是系统段描述符(S=0)还是代码或数据段描述符(S=1)。
Type
段类型字段Type(Type field):指定段或门(Gate)的类型,说明短的访问类型以及段的扩展方向。该字段的解释依赖于描述符类型标志S。
DPL
描述符特权级字段DPL(Descriptor privilege level):用于指明描述符的特权级。特权级范围从0到3。0级特权级最高,3级最低。DPL用于控制对段的访问。
P
段存在标志P(Segemnt present):用于支出一个段在内存中(P=1)还是不在内存中(P=0)。
当一个段描述符的P为0时,那当把指向这个段描述符的选择符加载进寄存器将导致产生一个段不存在异常(segment-not-present execption)。内存管理软件可以使用这个标志来控制在某一给定时间实际需要把那个段加载进内存中。这个功能为虚拟存储提供了除分页机制以外的控制。
当P标志为0时,操作系统可以自由使用格式中标注为可用(Available)的字段位置来保存自己的数据。
D/B
D/B标志(默认操作大小/默认栈指针大小和/或上界限,Default operation size/default stack pointer size and/or upper bound):根据段描述符描述的是一个可执行代码段、下扩数据段还是一个堆栈段,这标志具有不同的功能。
对于32位代码和数据段,这个标志应该总是设置为1;对于16位代买和数据段。这个标志设置为0。
- 可执行代码段。此时这个标志位D标志并用于使用指出该段中指令引用有效和操作数的默认长度。如果该标志置位,则默认值是32位地址和32或32位或8位的操作数;如果该标志为0,则默认值是16位地址和16位或8位的操作数。指令前缀0x66可以用来选择非默认值的操作数大小;前缀0x67可用来选择非默认值的地址大小。
- 栈段(由SS寄存器指向的数据段)。此时该标志为B标志,用于指明隐含堆栈操作(PUSH、POP或CALL)时的栈指针的大小。如果该标志置位,则使用32位栈指针并存放在ESP寄存器中;如果该标志为0,则使用16位栈指针并存放在SP寄存器中。如果堆栈段被设置成一个下扩数据段,这个B标志也同时指定了堆栈段的上界限。
- 下扩数据段。此时该标志称为B标志,用于指明堆栈段的上界限。如果设置了该标志,则堆栈段的上界限是0xFFFFFFFF(4GB);如果没有设置该标志,则堆栈段的上界限是0xFFFF(64KB)。
G
粒度标志G(granularity):该字段用于确定段限长字段Limit值的单位。
- G为0,则Limit的单位是字节。
- G为1时,Limit的单位是4KB。
这个标志不会影响基地址的单位,基地址的颗粒度总是单位字节。若设置了G标志,那么使用Limit来检查偏移值时,并不会检查偏移值的12位最低有效位。例如,当G=1时,Limit为0时,表明有效偏移值为0~4095。
L
64位代码段标志L(64-bits code segment):在IA-32模式,第二个双字的第21字节指示一个代码的是否包含本地64位代码。L置1表示这个代码段的指令执行在64位模式,置0表示执行在兼容模式。如果L位被设置了,那么D标志一定要置0。当不处于IA-32模式时,和对于非代码段,这个位被保留并且总是应该被置0。
AVL
可用和保留位AVL(available and reserved bits):段描述符第2个双字的第20个字节是否可供系统软件使用。
段描述符表
x86架构提供两种段描述表,全局段描述表(Global Descriptor Table,GDT)和本地段描述表(Local Descriptor Table,LDT)。
系统至少有一个GDT可以被所有进程访问。相对的,系统中可以有一个或多个LDT,可以被某个进程私有,可以个被多个进程共享。GDT仅仅是内存中的一个数据结构,可以将它看作是一个数组,由基地址(Base)和长度(Limit)描述。与之相反,LDT是一个段,它需要一个段描述符来描述它。LDT的段描述符存放在GDT中,当系统中有多个LDT时,GDT必须有对应数量的段描述符。
为了加速对GDT和LDT的访问,x86提供了GDTR寄存器和LDTR寄存器。它们的描述如下:
- GDTR:包括一个32位的基地址(Base)和一个16位长度(Limit)。
- LDTR:结构同段寄存器(包括对程序不可见的段描述符寄存器)。
可以使用LGDT/SGDT指令对GDTR进行读取/存储,类似地,可以使用LLDT/SLDT对LDTR进行同样的操作。通常在进程切换时,LDTR中的值会被换成新进程对应的LDT的段描述符。
GDTR/LDTR为GDT/LDT提供基地址,段选择符的TI为确定索引GDT还是LDT。
总结
假设程序中某条语句访问了数据段:
1 | int a = 100; // 全局变量 |
程序从加载到变量a的逻辑地址转换为线性地址的过程如下:
- 程序加载
- 通过该进程LDT的段选择符索引GDT,获得LDT的段描述符,被加载到LDTR寄存器中。
- 该进程的CS、DS、SS被加载入相应的段选择符。同时,CPU根据段选择符的TI字段,索引GDT/LDT,获得相应的段描述符并载入CS、DS、SS对应的不可见的段描述符寄存器。
- 程序执行到b=a,需要从a所在的内存中取值,必须先把a的逻辑地址转换为线性地址。
- 进行必要的属性、访问权限检查。
- 从DS对应的段描述符寄存器中获得该段的基地址。
- 将变量a的32位偏移量和描述符中的基地址相加,获得变量a的线性地址。
分页机制
分页是更加粒度化的内存管理机制,与分段机制将内存划分成为以基地址和长度描述的多个段进行管理不同,分页机制是用粒度化的单位“页”来管理线性地址空间和物理地址空间。x86架构下一个典型的页大小是4KB,则一个4GB的虚拟地址空间可以划分为1024 * 1024个页面。物理地址空间的划分同理。x86架构允许大于4KB的页面大小(如2MB、4MB)。
同时,分页机制让现代操作系统中的虚拟内存机制成为可能,由于这种机制,一个页面可以同时存在于物理内存中,也可以存放在磁盘的交换区域(如Linux下的Swap分区,Windows下的虚拟内存文件)中,程序可以使用比机器物理内存更大的内存区域。
操作系统在启动过程中,通过将CR0寄存器的PG位置1来启动分页机制。
分页机制主要由页表、CR3寄存器和TLB三个部件组成。
页表
页表(Page Table)是用于将线性地址转换成物理地址的主要数据结构。
一个地址对齐到页边界后的值称为页帧号(或页框架),它实际是该地址所在页面的基地址。线性地址对应的页帧号即虚拟页帧号(Virtual Frame Number,VFN),物理地址对应的页帧号即物理页帧号(Physical Frame Number,PFN)或机器页帧号(Machine Frame Number)。故也可以认为,页表是存储VFN到PFN映射的数据结构。
4KB大小的页面使用两级页表,如下图所示。
页目录项
页目录项(Page Directory Entry):包含页表的物理地址。页目录项存放在页目录(Page Directory)中,CPU使用线性地址的22 ~ 31位索引页目录,以获得该线性地址对应的页目录项。每个页目录为4B大小,故页目录占用一个4KB大小的物理页面,共包含1024的页目录项。
- Avail: Available for system programmer’s use
- G: Global page (Ingnored)
- PS: Page size (O Indl cates 4KBytes)
- O: Resrvered (set to O)
- A: Accessed
- PCD: Cache disabled
- PWT: Write-through
- U/S: User/Supervisor
- R/W: Read
- P: Present
页表项
页表项(Page Table Entry):页表项包含该线性地址对应的PFN。页表项存放在页表(Page Table)中,CPU使用线性地址的12 ~ 21位索引页表,获得该线性地址对应的页表项。通过线性地址的0 ~ 11位偏移量和基地址相加,就可以得到线性地址对应的物理地址。页表项为4B大小,故页表项包含1024个页表项,占用1个4KB页面。
- Avail: Available for system programmer’s use
- G: Global page (Ingnored)
- PAT: Page Table Attribute Index
- D: Dirty
- A: Accessed
- PCD: Cache disabled
- PWT: Write-through
- U/S: User/Supervisor
- R/W: Read
- P: Present
P(Present)字段是虚拟内存的实现成为可能。
- P = 1:物理页面存在于物理内存中,CPU完成地址转换后,可直接访问该页面。
- P = 0:页面不在物理内存中,当CPU访问该页面时会产生一个缺页错误(Page Fault)并交由操作系统的缺页错误处理程序处理。通常操作系统会将存放在磁盘上面的页面调入物理内存,使访问可以继续。P = 0 时,页目录、页表项格式会变为下图的格式。此时1 ~ 31位供操作系统使用以记录物理页面在磁盘上的信息,通常是物理页面在磁盘上的位置。
CPU在索引页目录前,必须知道页目录所在的物理地址,该物理地址存放在CR3(Control Register 3)寄存器中,也称为页目录基地址寄存器(Page-directory base register,PDBR)。一个进程在运行前,必须将其页目录的基地址存入CR3。页目录的基地址必须对其到4K边界。
TLB
为了提高地址转换效率,x86架构使用TLB(旁路转换缓冲区,Translation Lookaside Buffer)对最近用到的页面映射进行缓存,当CPU访问某个线性地址,其所在页面的映射存在于TLB中时,无须查找页面即可进行地址转换。注意,TLB存放的不是线性地址到物理地址的转换,准确地说是,VFN到PFN的转换。也就是说,CPU从TLB获得一个线性地址对应的PFN后,仍然要和线性地址的偏移相加,才能得到最后的物理地址,而非直接从TLB获取物理地址。
TLB作为缓存,其能存放的映射条目是有限的,当TLB中没有空闲条目可用时,替换哪一条旧目录由CPU决定。
TLB也存在缓存一致性的问题,这主要是指TLB中的映射条目和页表中的映射条目的一致性。当操作系统对页表进行修改后,要负责对TLB中对应的条目或者整个TLB进行刷新。从软件角度,x86提供了两种方式刷新TLB:
- 更新CR3:此操作可以导致TLB被整体刷新,TLB中所有映射条目失效(全局TLB除外)。操作系统将当前CR3中的值重新写回CR3以后刷新整个TLB。进程切换时,新进程的页目录基地址会写入CR3,使老进程在TLB中的条目失效。
- INVLPG指令:这是一种更细粒度的刷新,操作系统可以用它对TLB中单独的页目录项、页表项进行刷新。这通常是在操作系统修改页表后进行的(如分配/释放了页面)。
总结
CPU使用分页机制,将线性地址转换成物理地址的过程:
CPU访问一个线性地址,映射在TLB中调到步骤6。如果映射不存在与TLB中,我们称一次TLB Miss(TLB缺失)发生,进行下一步。
查找页表,页面在物理内存中跳到步骤4,不再进行下一步。
操作系统中的缺页处理函数接管,通常会进行如下操作:
- 将页面从磁盘复制到物理内存中。
- 更改对应页表项,将P设置为1,并对其他字段进行相应设置。
- 刷新TLB中对应的页表项。
- 从缺页错误处理函数中返回。
到这一步,页面已经存在在物理内存中,并且页表已经包含该映射。此时,重新执行引发TLB Miss指令。
TLB Miss再次发生,CPU重新查页表,把对应的映射插入到TLB中。
到这一步,TLB已经包含了该线性地址对应的PFN。 通过线性地址中的偏移部分和PFN相加,就得到了对应的物理地址。
x86架构的基本运行环境
三种基本模式
实际上,x86有4种运行模式:实模式、保护模式、SMM模式(System Management Mode,系统管理模式)和虚拟8086模式。除SMM模式外,其他三种模式最为常见。
- 实模式(Real Mode):当CPU加电并经历最初的混沌状态后,首先进入的就是实模式,它是早期Intel 8086处理器工作的模式。在该模式下,逻辑地址转换后即为物理地址,CPU可以访问1MB的物理地址空间(实际上是1MB + 64KB)。操作系统或BIOS通常在该模式下准备必要的数据结构和初始化关键的寄存器,然后切换入保护模式。
- 保护模式(Protect Mode):操作系统运行时最常用的模式。该模式下,CPU的所有功能几乎都能得到使用,可以访问架构允许的所有物理地址空间(例如x86是4GB),接来下的讲解,如无特殊说明,都是基于保护模式进行的。
- 虚拟8086模式(Virtual 8086 mode):为了使早期的8086程序能在保护模式下运行,x86提供了虚拟8086模式。该模式可以让CPU在保护模式下为8086程序虚拟实模式的运行环境,使这些程序在执行时无须真正的从保护模式切换到实模式。
基本寄存器组
寄存器是软件操作CPU的最基本部件,x86架构的寄存器可以粗略分为以下几类。
- 通用寄存器:共有8个32位的通用寄存器,例如常见的EAX、EDX等,用来保存程序运行时的临时变量、栈指针等数据。
- 内存管理寄存器:包括段寄存器和描述符表寄存器。
- EFLAGS寄存器:32位寄存器,用来保存程序运行中的一些标志信息,如溢出、开启中断与否、分支跳转等信息。
- EIP寄存器:32位的寄存器,用来保存指向当前指令的地址。通常称该寄存器为PC指针。
- 浮点运算寄存器:对于浮点运算,x86会通过一个浮点运算协处理器来处理。协处理器中包括8个80位的浮点数据寄存器,1个16位的控制寄存器,1个16位的状态寄存器,1个16位标志寄存器,1个11位的指令码寄存器,1个48位的浮点指令指针寄存器和1个48位的浮点数据指针寄存器。这些浮点运算寄存器为浮点运算提供了一个基本的运行环境。
- 控制寄存器:x86提供了5个控制寄存器,分别是CR0 ~ CR4寄存器。这些控制寄存器决定了CPU运行的模式和特征等。
- 其他寄存器:x86还提供了其他一些寄存器,包括8个调试寄存器(DR0 ~ DR7)、内存区域类型寄存器(MTRR)、机器检查寄存器(Machine Check Register)以及性能监控寄存器。
权限控制
权限控制是指CPU对资源进行分类,使不同权限的程序只能访问自身权限所允许访问的资源。操作系统的用户态和内核态之分就是最常见的权限控制,内核态程序具有最高权限,用户态程序具有最低权限。x86架构提供两种权限控制机制。
x86架构提供两种权限控制机制——段保护和页保护。这两种机制分别对应内存管理中的段机制和分页机制。
段保护
段保护引入了3中属性对权限属性进行控制。
当前权限级别(Current Privilege Level,CPL):CPL表示当前运行的代码的权限。通过CS的0、1位记录代码的CPL值,CPL可以有0 ~ 3共4个级别,这是常说的Ring级别(实际上,Ring级别有更广阔的含义)。其中,Ring0对应CPL = 0,具有最高权限,操作系统的内核运行 在该权限;Ring3对应CPL = 3,用户程序运行在Ring3。CPL值越高权限越低。
描述符权限级别(Descriptor Privilege Level,DPL):DPL表示段和门(Gate)所具有的权限。它表示代码访问某个段或通过某个门是所需要的最低权限。例如,某个数据段描述符有DPL = 2,则只有CPL = 0、1、2的代码可以访问该数据段,CPL = 3的不能访问。
所要求权限级别(Requested Privilege Lelve,RPL):RPL比较特殊,它存在于段寄存器的0 ~ 1位(CS寄存器的0 ~ 1位是CPL),用于程序在访问段时增加一级检查。
程序在访问一个段,要通过段寄存器得到段描述符,这样会产生2次检查,参与检查的3个属性分别是:程序本身的CPL、段寄存器的RPL、段描述符的DPL。CPL、DPL、RPL组合起来的情况有很多种,但只有当CPL <= DPL且RPL <= DPL时,访问才被允许,其余情况均被拒绝。通常可以把RPL设置成0来简化检查,此时,满足CPL <= DPL访问即被允许。
页保护
页保护的思想比段保护简单,它通过在页目录项、页表项中引入一个User/Supervisor位,将页面(或整个页目录项)分成User和Supervisor两个特权级。该位为0时表示Supervisor模式,对应CPL = 0、1、2的情况;为1表示User模式,对应CPL = 3的情况。
当程序运行在CPL = 0、1、2也就是Supervisor模式下时,可以访问所有页面;运行在CPL = 3下的程序处于User模式,只能访问User页面。
中断与异常
如果程序总是顺序执行,那么事情将变得非常简单。但事情往往和人们所期望的不太一样,中断和异常会打断顺序执行的程序流,转而转入一条完全不同的执行架构。
中断架构
中断提供给外部硬件设备一种“打断CPU当前执行任务,并响应自身服务”的手段。
可编程中断控制器
中断从设备发送到CPU需要由被称为“中断控制器”的部件转发(Message Signaled Interrupt,MSI,消息告知中断除外)。中断控制器发展至今,经历了PIC(Programmable Interrrupt Controller,可编程中断控制器)和APIC(Advanced Programmable Interrupt Controller,高级可编程中断控制器)两个阶段。
PIC
8259A芯片即常说的PIC,它具有IR0 ~ IR7共8个中断管脚连接外部设备。中断管脚具有优先级,其中IR0优先级最高,IR7最低。
PIC有如下三个重要的寄存器。
- IRR(Interrupt Request Register,中断请求寄存器):共8位,对应IR0 ~ IR7这个8个中断管脚。某位置为1,代表收到对应管脚的中断但还没提交给CPU。
- ISR(In Service Register,服务中断寄存器):共8位。某位置为1,代表对应管脚的中断已经提交给CPU处理,但CPU还未处理完。
- IMR(Interrupt Mask Register,中断屏蔽寄存器):共8位。某位置为1,对应的中断管脚被屏蔽。
除此之外,PIC还有个一个EOI位,当CPU处理完一个中断时,通过写该位告知PIC中断处理完成。PIC向CPU递交中断的流程如下。
- 一个或多个IR管脚上产生电平信号,若对应的中断没有被屏蔽,IRR中对应的位置被置为1。
- PIC拉高INT管脚通知CPU中断发生。
- CPU通过INTA管脚应答PIC,表示中断请求收到。
- PIC收到INTA应答后,将IRR中具有最高优先级的位清0,并设置ISR中对应的位。
- CPU通过INTA管脚第二次发出脉冲,PIC收到后计算最高优先级中断的vector,并将它提交到数据线上。
- 等待CPU写EOI。收到EOI后,ISR中最高优先级的位被清0。如果PIC处于AEOI模式,当第二个INTA脉冲收到后,ISR中最高优先级的位自动清0。
APIC
PIC可以在UP(单处理器)平台上工作,但无法用于MP(多处理器)平台。为此,APIC应运而生。APIC由位于CPU中的本地高级可编程中断控制器(Local Advanced Programmable Interrupt Controller,LAPIC)和位于主板南桥中I/O高级可编程中断控制器(I/O Advanced Programmable Interrupt Controller,IOAPIC)两部分构成。
其中,IOAPIC通常有24个不具有优先级的管脚,用于连接外部设备。当收到某个管脚的中断信号后,IOAPIC根据软件(通常是操作系统)设定的PRT(Programmable Redirection Table)表,查找到管脚对应的RTE(Redirection Table Entry,PRT的表项)。通过RTE的各个字段,格式化出一条包含该中断所有信息的中断消息,再由系统总线(老式的通过专门的APIC总线)发送给特定CPU的LAPIC,LAPIC收到消息后择机将中断递交给CPU处理。
在LAPIC内部,也有类似IRR、ISR和EOI寄存器,其中IRR、ISR为256位,EOI为32位,它们的功能和PIC的大体类似。APIC系统中,中断的发起大致流程如下。
- IOAPIC收到某个管脚产生的中断信号。
- 查找PRT表获得该管脚对应的RTE。根据RTE各字段格式化出一条中断消息,并确定发送给哪个(或多个)CPU的LAPIC。
- 通过系统总线或APIC总线发送中断信息。
- LAPIC收到中断消息,判断是否自己接收。
- 如确定接收,将IRR中对应的位置为1。同时确定此时是否将该中断交由CPU处理。
- 如确定提交中断给CPU处理,从IRR获取最高优先级的中断,将ISR中对应的位置为1,并提交中断。对于edge触发中断,IRR中对应位此时清0。
- CPU处理完中断,软件写EOI寄存器告知中断处理完成,对于level触发中断,IRR中对应位此时清0.LAPIC可提交下一个中断。
处理器间中断
在MP(多处理器)平台上,多个CPU要协同工作,处理器间中断(Inter-processor Interrupt,IPI)提供CPU之间互相通信的手段,CPU可以通过LAPIC的ICR(Interrupt Command Register,中断命令寄存器)向指定的一个/多个CPU发送中断。
中断的分类
中断可以从多个方面进行分类。从中断源的角度来看,可以分为如下几类。
- 外部中断:指连接在IOAPIC上设备产生的中断、LAPIC上连接的设备或LAPIC内部中断源产生的中断以及处理器间中断。
- 可屏蔽中断:指可以通过某种方式(例如CLI命令、TPR)进行屏蔽的中断。与之对应的概念是不可屏蔽中断(non-maskable interrupt)。
- 软件产生中断:指通过INT n指令产生的中断。
这样的分类并非绝对,例如外部中断通常是可屏蔽中断,但也可能属于不可屏蔽中断。通常,根据外部中断的触发方式,又把它们分为如下几类。
- edge触发中断:指中断边沿方式触发(例如上升沿)。ISA设备、时钟设备多使用这种触发方式。
- level触发中断:指中断以电平方式触发,在中断程序应答设备前,该电平一直有效。PCI设备使用这种触发方式。
中断的优先级
在使用PIC的系统中,PIC的管脚决定了中断的优先级,连接IR0的设备具有最高优先级,连接IR7的设备优先级最低。在APIC系统中,IOAPIC的管脚不再具备优先级,设备的中断优先级由它所连接管脚对应RTE中的vector字段决定。vector是x86架构用于索引IDT表的下标,范围从0~255,值越大优先级越高。其中,32~255可以供外部中断使用。
在现代操作系统中,有几个概念和vector常联系在一起使用。
- IRQ:PIC时代的产物,由于ISA设备通常是连接到固定的PIC管脚,所以说一个设备的IRQ实际上是指它连接的PIC管脚号。IRQ暗示中断优先级,例如IRQ0比IRQ3有更高的优先级。当进入APIC时代后,人们仍习惯用IRQ表示一个设备的中断号,但对于16以下的IRQ,它们可能不再与IOAPIC的管脚对应。
- GSI(Global System Interrupt):ACPI引入的概念,它为系统中每个中断源指定一个唯一的中断号。IRQ和GSI在APIC系统中常常被混用,实际上对于15以上的IRQ,它和GSI相等。
在这里,GSI和IRQ可以看作等同的概念,表示某个设备的中断号。它们与vector的关系由操作系统决定,通常是在设备驱动注册中断处理程序由操作系统分配。
中断的屏蔽
无论是在PIC收到中断信号后,还是LAPIC收到中断消息后,并不一定都是马上交给CPU处理的,这还要取决于CPU当前是否屏蔽中断(不可屏蔽中断除外)。当CPU屏蔽中断时,中断会被依附(pending)在PIC/LAPIC的IRR寄存器中,一旦CPU开启中断,会在第一时间响应PIC/LAPIC所依附的中断。CPU可以通过以下几种方法屏蔽/开启中断。
- CLI/STI指令:这是操作系统最常用的屏蔽/开启中断的方法。CLI指令将本CPU的EFLAGS寄存器的IF位清0,阻止接收中断;STI指令将IF位置为1,允许接收中断。这两条指令都只对当前CPU起作用,而不影响平台上的其他CPU。
- TPR寄存器:根据该寄存器值代表的优先级,部分屏蔽外部中断。
- PIC/IOPIC的中断屏蔽位:PIC可以通过IMR寄存器屏蔽对应管脚。IOAPIC可通过RTE中的mask位屏蔽对应管脚。该方法不会将中断依附(pending)到IRR,而是直接忽略,对于edge触发中断可能导致中断丢失。
IDT表
IDT表实际就是个大数组,用于存放各种“门”(中断门、陷阱门、任务门),这些“门”是中断和异常通往各自处理函数的入口。当一个中断或异常发生时,CPU用它们对应的vector号索引IDT表以获得对应的“门”。每个“门”占8B,x86最多有个256个vector,故IDT表长度最大为8 x 256 = 2048B。
IDT表的基地址存放在IDTR寄存器中,该寄存器和GDTR类似,由一个基地址(Base)字段和长度(Limit)字段构成,通过LIDT/SIDT指令可以加载和存储IDTR寄存器。IDT表要求被对齐到8B边界以提高效率。
中断门
“门”是入口,中断门就是中断的入口。中断门实际上是一种段描述符,称为系统描述符(System Descriptor),由段描述符的S位控制。中断门的格式如下图所示。
其中,段选择符、偏移量字段可以看成一个逻辑地址,通过索引GDT将该逻辑地址转换成中断处理函数入口的线性地址。这里要注意的是DPL字段,很多操作系统吧门的DPL设置成0,而之前讲过,只有当CPL <= DPL、CPL <= RPL访问才被允许。这就引出一个问题:程序在用户态时(CPL = 3)发生中断,岂不是不能放过一个DPL = 0的中断门?实际上,中断门和陷阱门的DPL只在使用INT n指令引起中断/异常时才检查,硬件产生的中断/异常不检查。P字段表示该中断门是否有效,清0无效。
中断门和陷阱门的唯一区别是程序通过中断门跳转后,EFLAGS寄存器的IF位自动清0,中断关闭。而陷阱门没有这样的效果。
异常架构
和中断相比,异常最大的不同在于它是在程序的执行过程中同步发生的。
1 | void main() { |
程序运行到a = a / 0一句时必然引起一个除0异常,但不能预料该程序在执行时是否会发生中断。异常根据产生的原因和严重程度可以分为如下三类。
- 错误(Fault):由某种错误情况引起,一般可以被错误处理程序纠正。错误发生时,处理器将控制权转移给对应的处理程序。例如,常见的缺页错误就属于此类。
- 陷阱(Trap):指在执行了一条特殊指令后引起的异常。例如,Linux中用于实现系统调用的INT 80指令就属于此类。
- 终止(Abort):指严重的不可恢复的错误,将导致程序终止的异常。例如MCA(Machine Check Architecture)。
和中断门一样,陷阱门存放在IDT表中。异常发生后,CPU用该异常的vector号索引其对应的陷阱门。x86架构将vector 0~19预留给各个异常。
陷阱门的格式如下图。
操作系统对中断/异常的处理流程
虽然各个操作系统对于中断/异常处理实现不同,但基本流程遵循如下的顺序。
一个中断/异常发生,打断当前正在执行的任务。
- CPU通过vector索引IDT表得到对应的“门”,并获得其处理函数的入口地址。
- 程序跳转到处理函数执行,由于处理函数存放在CPL = 0的代码段,程序可能会发生权限提升。处理函数通常执行下列几个步骤。
- 保存被打断任务的上下文,并开始执行处理函数。
- 如果是中断,处理完成后需要写EOI寄存器(伪中断不需要)应答,异常不需要。
- 恢复被打断的任务的上下文,准备返回。
- 从中断/异常的处理函数返回,恢复被打断的任务,使其继续进行。
进程
“尽管表面上看起来程序和进程非常类似,但本质上它们却是截然不同的。程序是指一个静态的指令序列,而进程是一个容器,其中包含了当一个程序的特定实例所用到的各种资源”。
进程是系统进行资源分配和调度的基本单位,是操作系统结构的基础。进程是各种资源的合集:通常一个进程包含下面几种资源。
- 私有的线性地址空间:这是进程可以使用的线性地址的总和,其中内核部分可能和其他进程是共享的。
- 可执行的程序:也就是前面说的二进制序列,包含代码和数据。
- 一些已经获得的其他资源:如打开的文件、管道等。
- 进程的权限:指进程的运行权限,例如在Linux中就有root用户和非root用户之分。
- 进程的描述符:有的操作系统称为控制块,包含操作该进程的一些必要的信息,例如进程的ID号。
上下文
上下文就是程序(进程/中断)运行时所需要的寄存器的最小集合。这些寄存器的后面可能代表着程序运行的一类资源。例如,CR3寄存器就可以概括进程的私有线性地址空间(分页机制启动时)。
x86架构下上下文包含哪些寄存器。
- 通用寄存器组:即EAX、EBX、ECX、EDX、ESI、EDI这6个加上ESP(栈指针)、EBP(框架指针)。
- 段相关寄存器组:CS、DS、SS,如果程序使用了ES等额外段寄存器,也要包括进来。
- 标志寄存器:主要指EFLAGS寄存器。
- 程序指针寄存器:EIP。
- GDT基地址:用于访问GDT,GDTR中内容。
- LDT段选择符:如果程序使用了私有的LDT,LDTR的内容。
- IDT基地址:用于访问IDT表,IDTR的内容。
- 控制寄存器组:CR系列,表示当前程序运行的CPU控制状态。
- 浮点相关寄存器组:用于浮点计算的一些寄存器组。
- 一些特殊用途的寄存器:例如x86架构下的MSR(Model-Specific Register)。
一个程序的上下文可能是上面列出内容的一个子集(例如进程),也可能是全部(例如虚拟机)。从程序员的观点看,上下文的概念有些微改变,通常对于上下文切换时不需要改变的寄存器,也可以说它不是该程序的上下文。例如进程切换时,GDTR中的内容不要改变,为了方便,通常在讲一个进程的上下文时不把GDTR算进去。在后面的内容中,包括虚拟机部分,提到上下文都是指在上下文切换时必须更改的寄存器的集合。
上下文切换
上下文切换是指程序从一种状态切换到另一种状态(例如用户态切换到内核态),或从一个程序切换到另一个程序(例如进程的切换)时,导致上下文相关寄存器值的变化行为。这种变化是指旧程序(切换前的程序)上下文相关寄存器的值被保存到内存中,新程序(切换后的程序)上下文相关寄存器的值被加载到寄存器中。在操作系统中,通常只有三种情况会发生上下文切换。
- 用户态到内核态的切换:此时的上下文切换是因为进程的用户态和内核态运行在不同的Ring级别,对资源的访问权限不同,需要切换部分上下文。例如,从用户态的栈切换到内核态的栈。
- 进程切换:由于一个CPU在同一时刻只能有一个进程运行,所以在新的进程运行前,需要把上下文相关寄存器的值换成新进程的相关值,例如把CR3换成新进程也目录的地址,EIP指向新进程运行的第一条指令。这通常是个全上下文的切换。
- 中断上下文的切换:中断的处理函数运行在特殊的上下文环境,称为中断上下文。CPU处理一个中断时,不管当前CPU在运行一个进程,还是本身就在一个中断上下文中,都要切换到新中断的上下文。例如,更改栈指针、EIP变化等。这通常是部分上下文的切换,例如CR3寄存器的值就不需要更改。根据x86架构的特点,无论是Linux的硬中断机制,还是Windows、Solaris的中断线程化机制,处理中断必然经过一个中断上下文阶段,可能的情况如下。
- 进程上下文 -> 中断上下文(处理中断) -> 进程上下文(中断返回执行)。
- 进程上下文 -> 中断上下文 -> 新进程上下文(处理中断) -> 进程上下文(最先被打断进程的上下文)。
- 中断上下文 -> 新中断上下文 -> … …
x86只有一种机制,即任务门(Task Gate)可以使中断的处理不经过中断上下文而直接进入进程上下文,但几乎没有操作系统使用。
上下文切换通常有2个步骤:
- 保存旧上下文:将被切换出去的程序(如一个被新进程代替的旧程序)或被切换出去的状态(如程序的用户态)的上下文相关寄存器的值保存在内存中。
- 加载新上下文:将要运行的程序(如新进程)或新状态(如程序的内核态)运行需要的上下文相关寄存器的值从内存中读入,加载入对应寄存器中。
需要注意的是,保存旧的上下文动作在x86架构下有时会由CPU自动完成一部分(例如中断发生时、使用TSS),但在现代操作系统中,通常是由软件完成的。
I/O架构
将计算机进行的任务进行一个粗略的分类,其实只有两种:CPU运算和I/O操作。I/O架构毫无疑问是现代计算机体系的重要组成部分。
x86的I/O架构
I/O(输入输出)是CPU访问外部设备的方法。设备通常是通过寄存器和设备RAM将自身的功能展现给CPU,CPU读写这些寄存器和RAM即可完成对设备的访问和操作。通过访问方式的不同,可以将x86架构的I/O分为如下两类。
Port I/O(端口I/O):即通过I/O端口访问设备寄存器。x86有65536(2^16)个8位的I/O端口,编号为0x0~0xFFFF。如果端口号看做访问设备端口的地址,那么这个65536个端口就构成了64KB的地址空间,称为I/O端口地址空间。与线性地址空间和物理地址空间不同,I/O端口地址空间是独立的,也就是说它并不是线性地址空间或物理地址空间的一部分。使用IN/OUT指令访问端口时,CPU通过一个特殊的管脚标识这是一次I/O端口访问,于是芯片组知道地址线上的地址是I/O端口号并进行相应操作。此外,2个或4个连续的8位I/O端口,可以组成16位或32位I/O端口。
MMIO(Memory Map I/O,内存映射I/O):即通过内存访问的形式访问设备寄存器或设备RAM。x86架构下,MMIO和Port I/O最大的不同是,MMIO要占用CPU的物理地址空间。它把设备的寄存器或设备RAM映射到物理地址空间某段地址,使用MOV这样的访存指令访问此段地址即可访问到映射的设备。很多CPU架构都没有Port I/O,采用统一的MMIO方式。由此可见,MMIO是一种更加先进的I/O访问方式。
对于Port I/O,由于编译器不能产生IN/OUT指令,操作系统通常吧汇编命令封装成类似inb()、outb()这样的函数。对于MMIO,由于整个物理地址空间都会被映射到线性地址空间,程序访问I/O资源时,也要做线性地址到物理地址的转换。与普通物理地址到线性地址的映射不同,MMIO地址通常是不可缓存的(un-cacheable)。
DMA
DMA(直接内存访问)是将CPU从I/O操作中解放出来的一种技术。如果设备向内存复制数据都经过CPU,则会消耗大量的CPU时间,不利于系统性能。通过DMA,驱动程序可以事先(或在需要的时候)设定一个内存地址,设备就可以在绕开CPU直接向内存中复制(或读取)数据。根据发起者不同,DMA可以被分为2种。
同步DMA:是指DMA操作由软件发起。一般的流程是设备驱动在设定好需要被DMA访问的内存地址后,写某个寄存器来通知设备发起DMA。此时,设备会直接从该内存地址空间读取内容并操作。一个典型的例子就是声卡,当播放一段音频时,驱动将该音频存放的地址通知声卡,设备从内存直接读取数据并播放,完成后以一个中断通知驱动操作完成。
异步DMA:是指DMA操作由设备发起。一般的流程是设备将数据直接复制到一个事先设定好的内存地址,再通过一个中断通知驱动程序。典型的例子就是网卡收包,当网卡接收到数据包后,会直接复制到驱动程序设定好的内存地址去,并以中断的形式通知网络包的到来。
设备的DMA操作都是使用物理地址访问内存,不经过线性地址到物理地址的转换。但IOMMU(Input/Output Memory Management Unit,输入输出内存管理单元)出现后,这个情况就改变了。从驱动的角度来看,驱动要提供一片内存区域供设备访问,DMA要求这片内存区域在物理上是连续的。
现代设备支持一种称为“分散————聚合(Scatter-Gather)”DMA的机制,允许驱动向设备提供不连续的物理内存。实际上,驱动是将一组以“起始地址——长度”为属性的内存描述符提供给设备,每个描述符描述了一块连续的物理内存,但连续两个描述符描述的内存不需要是连续的。从宏观上来看,通过这组内存描述符可以向设备提供一片不连续的内存区域;但从微观的角度看,DMA操作访问的仍然是连续的物理内存。
PCI设备
PCI总线无疑是总线中的王者。在它之前,各种平台都拥有自己特定的总线,例如x86的ISA总线、Power PC的VME总线。PCI出现后,由于速度快、具有动态配置功能和独立于CPU结构等特点,迅速被各种平台接受,成为一种通用的总线架构。
PCI总线架构
PCI总线是一种典型的树结构。把北桥中HOST-PCI桥看作根,总线中其他PCI-PCI桥、PCI-ISA桥(ISA总线转PCI总线桥)等桥设备和直接接PCI总线的设备看作节点,整个PCI架构可以概括成下图所示。
通过桥,PCI总线可以很容易地被扩展,并且与其他总线互相挂接,构成整个系统的总线网络。与HOST-PCI相连的总线被称为总线0,其他层次总线的编号,是在BIOS(或操作系统)枚举设备时确定的。
设备标识符
设备标识符可以看作是设备在PCI总线上的地址,它的格式如下图所示。
其中,8位的Bus字段代表设备所在的总线号,故系统最多有256条总线。Device字段表示设备号,代表在Bus所表示总线的某个设备。Function字段表示功能号,标识具体设备上的某个功能单元(逻辑设备)。
一块PCI卡,它上面有两个独立的设备,这两个设备共享了一些电子线路,那么这两个设备就是这块PCI卡的两个功能单元。但从软件的角度来看,它们和两个独立接入PCI总线的设备无异。
如同Function字段长度所暗示的,一个独立的PCI设备上最多有8个功能单元。Device和Function两个字段一般结合起来使用,表示一条总线上最多有256个设备。
通常,用这三个字段的缩写BDF来代表设备标识符。
当程序通过BDF访问某个设备时,先通过Bus字段选定特定的总线,再根据Device字段选定特定的设备,最后通过Function字段就可以选定特定的功能单元(逻辑设备)了。
PCI配置空间
PCI设备规范规定,设备的配置空间最多为256个字节,其中前64个字节的格式和用途是统一的。如下图所示。
Base Address Registers:基地址寄存器(PCI Bar)。
它报告设备寄存器或设备RAM在I/O端口地址空间(或地址空间中)的地址。一改ISA设备通过跳线配置的不灵活的特点,地址是由软件(BIOS或操作系统)动态配置的。
通常枚举PCI设备的软件(BIOS或操作系统)会在获得平台所有PCI设备后,根据设备数量,依照固定的算法为每个设备的PCI Bar分配I/O端口(或物理地址)。设备的电子线路负责把这些端口(或地址)映射到自身的寄存器(设备RAM)上,这样,CPU就可以通过端口号(Port I/O方式)、物理地址(MMIO方式)访问到设备了。使用哪种方式访问,由PCI Bar的最后一位表示。当该位为1时,表示是Port I/O方式;该位为0时,表示是MMIO方式。有些架构根本就没有Port I/O方式,全部采用MMIO。
根据访问目标性质不同,PCI Bar又可以划分为:- 可预取(Prefetchable)类型。这主要是设备RAM。由于RAM具有在每次操作后内容不自动改变的性质,所以可以使用预读机制。例如程序在读第N个字节的内容时,总线可能已经读出了第N + 1个字节的内容。当预读出的内容不需要时,只要简单地抛弃就可以了,不会有什么影响。
- 不可预取(Non-Prefetchable)类型。这里主要指设备寄存器。寄存器和RAM有着不同的性质,有些寄存器本身就是设备的FIFO队列的接口。很有可能当一次读操作完成后,寄存器的值就改变了。如果使用预读机制,例如程序本身只读了寄存器的第一个字节,而总线却连续读入了4个字节,那么后面3个字节的内容可能就会改变,下次程序真正访问它们时,得到的就是错误的值。
Interrupt Pin:中断针脚。PCI中断线的标准设计是4条:INTA、INTB、INTC和INTD,分别对应值0~3。该寄存器的值表示设备连接的是哪个中断针脚。
Interrupt Line:设备的中断线。该寄存器只起一个保存作用,BIOS和操作系统和以自由使用它。BIOS通常用它保存设备所连的PIC/IOAPIC的管脚号。
x86架构把I/O端口地址空间中的0xCF8~0xCFF段预留给PCI总线,用于访问设备的配置空间。其中,前32位的寄存器为“地址寄存器”,后32位为“值寄存器”。软件通过把设备的BDF和要访问的配置空间的字节偏移写入“地址寄存器”中,就可以通过“值寄存器”读写该配置空间了。
PCI设备枚举过程
PCI设备的枚举和资源分配(即配置PCI配置空间)通常是由BIOS完成的,并提供特殊的PCI设备枚举接口供保护模式下的操作系统使用,这些接口称为PCIBIOS。
由于某些平台,例如嵌入式,是没有BIOS的,并且操作系统厂商对BIOS的可靠性也不信任,故某些操作系统也实现了自己的PCI设备枚举接口。无论是BIOS,还是操作系统,其枚举设备的过程都遵循着一般规律。
PCI设备和总线一起构成了树结构,其中PCI-PCI桥(或PCI-ISA等其他桥)是子树的根节点,设备枚举的过程就是要在内存中建立一棵和实际总线情况向符合的设备树。枚举过程中最关键的步骤是发现PCI-PCI桥,这个可以通过PCI配置空间的Header Type字段判断,该字段为1时,表示为桥设备。
PCI-PCI桥主要有三个属性。
- Primary Bus:表示该桥所属的根总线。
- Secondary Bus:表示以该桥为根节点的子总线。
- Subordinate Bus:表示该桥为根的子树中,最大的总线号。
下图说明三者的关系。
对于PCI-PCI桥1,其Primary Bus是总线0,Secondary Bus是总线1,而以它为根的总线中最大的总线号为2,所以其Subordinate Bus为总线2。
设备枚举从根节点HOST-PCI桥开始,首先探测总线0上的各个设备。当探测到第一个桥设备时,为其分配Primary Bus号和Secondary Bus号,其中Secondary Bus号为1(即当前系统中最大总线号加1),Subordinate Bus号暂设为和Secondary Bus相同,当在子树中发现新总线后会动态调整该值接着以该桥为根节点,继续探测其下属总线,其过程和前面相同,发现第一个桥设备后则以其为根往下探测,如此反复直到所有子树都探测完毕。
当PCI-PCI桥收到写入0xCF8中的BDF后,会将Bus字段与自身的Secondary Bus相比,相符则在下属总线上搜寻设备;如果不相符,但Bus值落在Subordinate Bus范围内,则把该地址传递给下属总线中各桥,否则不予理睬。
通过这种方式,BIOS或操作系统可以枚举出总线上所有设备并为之分配资源,一旦PCI配置空间设定好,软件就可以直接通过PCI Bar访问设备了。
PCI Express
PCI Express的设计目标是用来代替之前广泛使用的PCI、PCI-X和AGP等总线标准,称为新一代通用、高速的IO互联标准,同时保持对PCI标准的软件兼容性。
PCI Express架构
PCI Express抛弃了PCI所采用的多个设备共享的并行的总线结构,转而使用了与网络协议类似的点对点的串行通信机制。多个PCI Express设备(Endpoint)通过交换器(Switch)相互连接。与PCI总线中的桥设备类似,通过交换器,可以搭建一个树形的PCI Express的拓扑结构。
标准的PCI Express拓扑结构如下图。树的根节点是Root Complex,用来连接处理器、内存系统和IO系统,其作用类似PCI总线树中的HOST-PCI桥。
PCI Express的优点
PCI Express比PCI总线拥有更高的带宽。串行通信机制可以让物理链路工作在很高的频率。点对点的通信方式使得链路两端的设备可以独占通信带宽,而且多个链路可以并发传输数据。
PCI Express在于PCI总线迥异的硬件基础之上,构建了与PCI总线完全兼容的软件接口。PCI Express定义了基于数据包的分层通信协议,包括物理层(Physical Layer)、数据链路层(Data Link Layer)和事务层(Transaction Layer)。在事务层协议中,PCI Express定义了内存读写、IO读写、配置空间读写和消息事务。通过这些事务的定义,PCI Express可以实现所有的PCI总线事务。
PCI Express将PCI总线的配置空间大小从256字节扩展到4KB字节,解决了PCI总线的配置空间过小的问题,可以容纳更多的设备功能配置。PCI Express还增加一种新的MMIO方法来访问扩充过的配置空间。为了保持兼容性,4KB字节配置空间中的前256字节仍然可以使用原来的方式访问。
PCI Express除了保留了PCI总线的优点以外,还增加了诸如QoS服务、高级错误报告(AER)等新特性。软件可以通过PCI Express提供的软件接口来配置和使用这些新的功能。
此外,PCI Express标准具有良好的扩充性。PCI-SIG的SR-IOV标准在PCI Express基础上做了扩展,支持该标准的设备可以动态地生成新的逻辑设备。DMA重映射可以利用PCI Express内存读写事务数据包中所包含的设备标识符合地址信息,为每个逻辑设备提供独立的地址转换。
时钟
在现代计算机架构中,时钟有着重要的地位,操作系统中的很多时间都是由时钟驱动的,例如进程调度、定时器等。
时钟根据工作方式不同,可以分成如下两类。
- 周期性时钟(Periodic Timer):这是最常见的方式,时钟以固定频率产生时钟中断。通常,周期性时钟会有一个计数器,要么以固定值递减到0产生中断,例如PIT;要么固定增长,当达到某个阈值时产生中断,同时自动将阈值增加一个固定值,计数器继续递增,例如HPET。
- 单次计时时钟(One-shot Timer):大多数时钟都可以配置成这种方式,例如PIT、HPET。其工作方式和到达阈值产生中断的周期性时钟类似,不同的是产生中断后阈值不会自动增加,而是需要软件(通常是时钟中断处理函数)增加该阈值。这提供给软件动态调整下一次时钟中断到来时间的能力,使一些新技术,例如无滴答声内核(Tickless Kernel)的实现成为可能。
x86平台的常用时钟
PIT
PIT(Programmable Interrupt Timer或Programmable Interval Timer,可编程中断/间隔时钟):随IBM PC平台产生,被广泛应用,其频率为1000Hz作用,即每次中断间隔约为1ms,通常接IRQ0,软件可以通过0x40~0x43 I/O端口进行操作。PIT是一种低精度的时钟,容易溢出(16位),已经渐渐被后来出现的高精度时钟取代。PIT支持周期性和单次计时两种工作方式。
RTC
RTC(Real Time Clock,实时时钟):通常是和CMOS集成在一起的,由CMOS电池供电,故能在关机后继续计时。其频率范围在2~8192Hz,通常接IRQ8,软件可以通过0x70~0x71 I/O端口操作。RTC支持周期性和单次计时两种方式,此外,它还可以配置成没秒产生一次中断,具有闹钟功能。由于具有关机继续计时的功能,RTC常被用作为操作系统提供日期,即“年/月/日”。
TSC
TSC(Time Stamp Counter,时间戳计时器):和普通计时器不同,它可以看作一个单调递增的计数器(64位),由x86架构引入的。其时钟频率和CPU频率相关,操作系统在使用前需要计算其频率,例如1GHz的TSC,其值每纳秒增加1。通过rdtsc指令,可以读取当前TSC的值。由于不产生时钟中断,故无所谓周期性和单次计时方式。
LAPIC Timer
LAPIC Timer:该时钟是根据LAPIC所在总线(系统总线或者APIC总线)频率产生的。32位,有如下两个特点。
- 由于LAPIC是每个CPU一个,故其中断也是对于本地CPU的。
- 可以通过寄存器配置,对总线周期进行不同的分频而产生不同频率的时钟中断。
LAPIC Timer可配置成周期性和单次计时两种工作方式。
HPET
HPET(High Precision Event Timer,高精度时间时钟):是Intel和微软共同开发的新型高精度时钟,其最低频率为10MHz,可以作为64位或32位时钟使用。HPET可以提供最多8个时钟,典型的实现至少有一个时钟可用。
HPET的时钟通过一个主计数器,和32个比较器、匹配器一起,又可以被配置成32个子时钟(又称为channel),每个子时钟可以按不同频率产生不同的中断。例如,可以将一个子时钟配置成每毫秒产生一个IRQ8中断,另一个子时钟可以被配置成每微秒产生一个IRQ0中断。
HPET可用于替代传统的PIT和RTC,此时平台的IRQ0、IRQ8中断被HPET占用。HPET支持周期性和单次计时两种工作方式。
小结
x86平台提供如此多的时钟,操作系统可以根据不同的需要使用其中的一个或多个。同时使用多个时钟带来的一个明显的缺点是过多的时钟中断会影响系统性能。所以,当有高精度时钟可用时,操作系统通常会禁用低精度时钟,并根据需要使用高精度时钟模拟低精度时钟。例如,可以用HPET代替PIT,并模拟RTC。
操作系统的时钟观
从操作系统的角度看,时钟的作用可以分为以下两类。
- 提供统计值及驱动事件:提供统计值是指操作系统用时钟来维护一些必需的数据,例如,一个进程在用户态/内核态的时间,系统的日期、时间等。驱动事件是指驱动以时间为资源的程序,典型的就是进程。例如,分时操作系统为每个进程分配固定的时间片,调度时间片耗尽的进程睡眠,唤醒分配到新时间片的进程运行。
- 维护定时器(Timer):定时器是程序中最常用的组件,用于在某个时间到达后执行特定的操作。定时器大量运用于操作系统中,例如内核为I/O操作注册的超时定时器、操作系统提供给应用程序使用的定时器接口等。
从上图可以看出,操作系统使用时钟的功能,是以时钟中断为基础的。
操作系统中往往会对时钟架构进行封装以方便维护和使用,但从硬件的角度来看,时钟中断仍然是所有封装的基础,故虚拟化中对时钟的处理主要是提供准确的时钟中断以模仿硬件行为。