Labs 导读
标准服务器技术是网络功能虚拟化(NFV)实现的一个关键因素,了解一些x86架构的基础知识对大家后续了解电信云关键技术,尤其是掌握虚拟化技术原理和关键优化方案是必须具备的。本文主要从x86架构的CPU指令集增强,内存管理、中断和异常、IO架构等部分进行阐述,以及包含一些基础IT的基本概念的讲解。
1 x86-64指令集的增强
Intel的x86体系结构是世界上最流行的处理器架构,从1978年8086/8088处理器问世到现在的Core i7和Core i9,以及Xeon系列处理器,Intel x86体系结构已经在CPU领域叱咤40多年。
x86-64是x86架构的延伸产品,是一种64位微处理器架构及其相应的指令集。在x86-64出现以前,Intel与惠普联合推出IA-64架构,此架构不与x86兼容,且市场反应冷淡。于是,与x86兼容的x86-64架构应运而生。1999年,AMD首次公开64位指令集为IA-32提供扩展,称为x86-64(后来改名为AMD64)。此架构后来也为Intel所采用,也就是现在的Intel 64。
x86-64能有效地把x86架构移植到64位环境,并且兼容原有的x86应用程序,市场前景广阔。外界使用x84-64或者x64称呼这个64位架构,以保持中立,不偏袒任何一家厂商。
AMD 64指令集主要特点有:支持64位通用寄存器、64位整数及逻辑运算和64位虚拟地址。
Intel 64架构加入了额外的寄存器和其他改良的指令集,可使处理器直接访问超过4GB的内存,允许运行更大的应用程序。通过64位的存储器地址上限,其理论存储器容量上限达16EB,目前大多数操作系统和应用程序已基本支持完整的64位地址。
2 x86的内存架构
硬件架构中最复杂、最核心的部分就是其内存架构。此部分详细内容因为篇幅有限无法详细展开,面向的人员主要包括CPU架构设计、操作系统开发和内核底层优化等领域,至于运维方面后期如果不做内核优化的同事了解即可,如感兴趣可参考《手把手教你设计CPU-RISC-V处理器》、《嵌入式操作系统原理》和《处理器虚拟化技术》等书籍。
2.1 地址空间
地址空间是所有可用资源的集合,我们姑且将它看做一个大大的数组,那么地址就是这个数组的索引。地址空间可以划分为物理地址空间和线性地址空间两大类。
2.1.1 物理地址空间
硬件平台通常划分为CPU、内存和其他硬件设备三个部分。其中,CPU 是整个硬件平台的主导者,内存和其他硬件设备都是CPU 可以使用的资源。这些资源组合在一起,分布在CPU的物理地址空间内,CPU使用物理地址索引这些资源。物理地址空间的大小由CPU实现的物理地址位数所决定,物理地址位数由CPU经过MMU(Memory Management Unit,内存管理单元)转换后的外地址总线位数决定。外地址总线位数与CPU处理数据的能力(即CPU位数)没有必然的联系,例如:16位的8086 CPU具有20位地址空间。
一个硬件平台只有一个物理地址空间,但每个程序都认为自己独享整个平台的硬件资源。为了让多个程序能够有效地相互隔离,也为了它们能够有效地使用物理地址空间的资源,引入了线性地址空间的概念。
2.1.2 线性地址空间
线性地址空间的大小由CPU实现的线性地址位数决定,线性地址位数由CPU的内地址总线位数决定。由于CPU的内地址总线与CPU的执行单元直连,所以,内地址总线位数往往与CPU位数一致,如果是32位处理器,那么它就实现了32位线性地址,其线性地址空间为4GB,如果是64位处理器,那么它的线性地址空间的为2的64次方,即16384GB。需要注意的是,线性地址空间的大小与物理地址空间的大小没有必然联系,Intel的PAE平台具有4GB的线性地址空间,而其物理地址空间为64GB。但是,线性地址空间会被映射到某一部分物理地址空间或整个物理地址空间。也就是说,线性地址空间小于等于物理地址空间。
CPU负责将线性地址空间转换成物理地址空间,保证程序能够正确访问到该线性地址空间所映射到的物理地址空间。在现代操作系统中,每个进程通常都拥有自己的私有线性地址空间。一个典型的线性地址空间构造如下图所示。
线性地址空间
2.2 地址
地址是访问地址空间的索引。根据访问地址空间的不同,索引可以分为物理地址和线性地址。但由于x86特殊的段机制,还存在一种额外的地址——逻辑地址。
2.2.1 逻辑地址
逻辑地址是程序直接使用的地址(x86无法禁用段机制,逻辑地址一直存在)。逻辑地址由一个16位的段选择符和一个32位的偏移量(32位平台)构成。下面以具体程序为例进行解释。比如:我们写下面一段c语言代码:
int a = 100; # 定义一个整型变量
a int *p = &a; # 定义一个整型指针p,指向变量a在内存中的地址
上述语句中的指针变量p存储的就是变量a的逻辑地址。实际上,p中存储的仅是逻辑地址的偏移部分,而偏移对应的段选择符位于段寄存器中,并没有在程序中显示。
2.2.2 线性地址
线性地址又称虚拟地址。线性地址是逻辑地址转换后的结果,用于索引线性地址空间。当CPU使用分页机制时,还需要将线性地址转换成物理地址才能访问物理平台内存或其他硬件设备;当分页机制未启用时,线性地址与物理地址相同。
2.2.3 物理地址
物理地址是物理地址空间的索引,是CPU提交到总线用于访问物理平台内存或其他硬件设备的最终地址,在x86下,物理地址有时也被称为总线地址。
根据上面的描述,我们可以总结如下:
分段机制启用,分页机制未启用:逻辑地址--->线性地址=物理地址
分段机制、分页机制同时启用:逻辑地址--->线性地址--->物理地址
3 x86内存管理机制
x86架构的内存管理机制分为两部分:分段机制和分页机制。分段机制为程序提供彼此隔离的代码区域、数据区域、栈区域,从而避免了同一个处理器上运行的多个程序互相影响。
分页机制实现了传统的按需分页、虚拟内存机制,可以将程序的执行环境按需映射到物理内存。此外,分页机制还可以用于提供多任务的隔离。
分段机制和分页机制都可以通过配置,支持简单的单任务系统、多任务系统或共享内存的多处理器系统。需要强调的一点是,处理器无论在何种运行模式下都不可以禁止分段机制,但是分页机制却是可选选项。
3.1 分段机制
分段机制是x86架构下的朴素内存管理机制,不可以禁用。了解分段机制有利于对后续内存虚拟化原理和优化方案有更深的了解。
分段机制将内存划分成以基地址(Base)和长度(Limit)描述的块。段可以与程序最基本的元素联系起来,程序可以简单地划分为代码、数据和栈,段机制就有相应的代码段、数据段和栈段。
一个程序根据分段机制在内存中由逻辑地址、段选择符、段描述符和段描述符表4个基本部分构成。
1)当程序使用逻辑地址访问内存的某个部分时,CPU通过逻辑地址中的段选择符索引段描述符表,进而得到该内存对应的段描述符(段描述符描述段的基地址、长度以及读/写、访问权限等属性信息)
2)根据段描述符中的段属性信息检测程序的访问是否合法,如果合法,再根据段描述符中的基地址将逻辑地址转换为线性地址。
这个流程可以用如下图示进行总结:
内存分段机制
段选择符是逻辑地址的一个组成部分,用于索引段描述符表以获得该段对应的段描述符。段选择符作为逻辑地址的一部分,对应用程序是可见的。但是,正如前面在逻辑地址中介绍的,应用程序中只存储和使用逻辑地址的偏移部分,段选择符的修改和分配由连接器和加载器完成。
为了使CPU能够快速地获得段选择符,x86架构提供了6个段寄存器存放当前程序中各个段的段选择符。这6个段寄存器分别如下:
CS(Code-Segment,代码段):存放代码段的段选择符。
DS(Data-Segment,数据段):存放数据段的段选择符。
SS(Stack-Segment,栈段):存放栈的段选择符。
ES、FS、GS:可以存放额外三个数据段的段选择符,由程序自由使用。
由于段选择符的存在最终是为了索引段描述符表中的段描述符,为了加速段描述符的访问,x86架构在不同的段寄存器后增加了一个程序不可见的段描述符寄存器。当相应段寄存器中加入一个新的段选择符后,CPU自动将该段选择符索引的段描述符加载到这个不可见的段描述符寄存器中。各个段寄存器的构造如下:
段寄存器
段描述符描述某个段的基地址、长度以及各种属性(例如,读/写属性、访问权限等)。这是分段机制的核心思想。当CPU通过一个逻辑地址的段选择符获得该段对应的段描述符后,会使用段描述符中各种属性字段对访问进行检查,一旦确认访问合法,CPU将段描述符中的基地址和程序中逻辑地址的偏移量相加就得到程序的线性地址。
正如前面讲到的,x86架构在每个段寄存器后增加了一个程序不可见的段描述符寄存器,每当段寄存器加入一个新的段选择符后,CPU自动将该段选择符索引的段描述符加载到这个段描述符寄存器中。后续只要不发生段寄存器的更新操作,CPU就不再查询段描述符表而是直接使用这个段描述符寄存器中的值,从而加快CPU的执行效率。
x86架构提供了两种段描述符表:GDT(全局段描述符表Global Descriptor Table)和LDT(本地段描述符表Local Descriptor Table)。具体选择哪个段描述符表,由段选择符中的TI字段决定,当TI=0时,索引GDT,当TI=1时索引LDT。系统中至少有一个GDT可以被所有的进程访问。与此同时,系统中可以有一个或多个LDT,可以被某个进程私有,也可以被多个进程共享。
GDT是内存中的一个数据结构。简单地讲,可以将GDT看成是一个数组,由基地址(Base)和长度(Limit)描述。
LDT是一个段,需要用一个段描述符来描述。LDT的段描述符存放在GDT中,当系统中有多个LDT时,GDT中必须有对应数量的段描述符。
为了加速对GDT和LDT的访问,x86架构提供了GDTR寄存器和LDTR寄存器。关于这两种寄存器的具体描述如下:
GDTR:包括一个32位的基地址(Base)和一个16 位的长度(Limit)。
LDTR:结构与段寄存器相同(同样包含对程序不可见的段描述符寄存器)。
通过段选择符索引GDT/LDT的过程如下图所示:
段选择符索引
➢ x86架构内存管理中分段机制总结:
1)在程序加载阶段,该进程LDT的段选择符首先索引GDT,获得LDT的段描述符并将其加载到LDTR寄存器中。此外,该进程的CS、DS、SS中加入相应的段选择符,CPU根据段选择符的TI字段索引相应的段描述符表,获得相应的段描述符,并加载入CS、DS、SS对应的程序不可见的段描述符寄存器。
2)程序执行到读/写内存中的数据时,把程序中相应变量的逻辑地址转换为线性地址:
进行必要的属性、访问权限检查;
从DS对应的段描述符寄存器获得该段的基地址;
将变量的32位偏移量和段描述符中的基地址相加,获得该变量的线性地址。
3.2 分页机制
分段机制的目的是将内存中的线性地址空间划分成以基地址和长度描述的多个段进行管理,程序对应的逻辑地址以基地址和偏移量来描述,实现逻辑地址到线性地址空间的映射。而分页机制是使用单位“页”来管理线性地址空间和物理地址空间的映射关系。同时,分页机制允许一个页面存放在物理内存中或磁盘的交换区域(如Linux下的Swap分区,Windows下的虚拟内存文件)中,程序可以使用比机器物理内存更大的内存区域,从而实现现代操作系统中虚拟内存机制。(注意:操作系统的虚拟内存原理和映射关系和后面要讲的计算虚拟化技术中内存虚拟化技术基本一致,只是VMM实现时又多了一层嵌套)。
在x86架构下,页的典型大小为4KB,于是一个4GB的线性地址空间被划分成1024×1024个页面,参见本文线性地址空间示意图。物理地址空间的划分与此类似。x86架构允许大于4KB的页面大小(如2MB、4MB、1GB)等。
分页机制的核心思想是通过页表将线性地址转换为物理地址,并配合旁路转换缓冲区(Translation Lookaside Buffer,后面简称为TLB)来加速地址转换的过程。分页机制主要由页表、CR3寄存器和TLB三个部件构成,如下图所示。
内存分页机制
页表是用于将线性地址转换成物理地址的主要数据结构。一个地址对齐到页边界后的值称为页帧号(或者页框架),它实际上就是该地址所在页面的基地址。比如:一个页大小为4kB,那么第一个页帧号就是0,第二个页帧号就是4097,依次类推。线性地址对应的页帧号叫做虚拟页帧号(Virtual Frame Number,下面简称为VFN),物理地址对应的页帧号叫做物理页帧号(Physical Frame Number,下面简称为PFN)或机器页帧号。页表实际上是存储VFN到PFN映射的数据结构。
在传统的32位的保护模式中(未启用物理地址扩展PAE功能),x86处理器使用两级转换方案,在这种方案中,CR3寄存器指向一个4KB大小的页目录表,页目录中共有1024个记录,每一项记录大小4B空间,都指向一个4KB大小的页表,页表中也有1024项,每项大小4B空间,所以,最后整个线性地址空间大小就是1024 个长为4KB的页,即总共4GB大小的空间。未启用PAE 的4KB大小的页面如下图所示。
页表结构
页目录项(Page Directory Entry,下面简称为PDE),包含页表的物理地地址,PDE存放在页目录表中。
页表项(Page Table Entry,下面简称为PTE):包含该线性地址对应的物理页帧号PFN,PTE存在页表中,确定物理页帧号PFN 后,再将线性地址的0~11位偏移量与其相加,就可以确定该线性地址对应的物理地址。
虚拟内存实现的关键在于PDE和PTE都包含一个P(Present)字段:
当P=1时,物理页面存在于物理内存中,CPU完成地址转换后可以直接访问该页面。
当P=0时,物理页面不在物理内存中(在硬盘的交换分区中),当CPU访问该页面时,会产生一个缺页错误中断,由操作系统的缺页处理机制将存放在硬盘上的页面调入物理内存,使访问可以继续。同时,由于程序的局部性特点,操作系统会将该页面附近的页面一起调入物理内存,方便CPU的访问。所以,为了减少内存占用,要求程序开发人员尽量少的使用全局索引或递归调用等机制。
P=0时的PDE和PTE的1~31位都将为操作系统提供物理页面在硬盘上的信息,这些位存储着物理页面在硬盘上的位置。
启用物理地址扩展(之后简称为PAE)后,页表结构将发生相应的变化。页表和页目录的总大小仍是4KB,但页表和页目录中的表项都从32位扩为64位,以使用附加的地址位。这样,页表和页目录都只有512个表项,变成了原来方案的一半,所以又加入了一个级:CR3指向页目录指针表,即一个包含4个页目录指针的表。启用PAE 的4KB大小的页面使用的三级页表如下图所示:
三级页表结构
CR3寄存器也称为页目录基地址寄存器(Page-Directory Base Register,PDBR),存放着页目录的物理地址。一个进程在运行前,必须将其页目录的基地址存入CR3,而且,页目录的基地址必须对齐到4KB页边界。启用PAE时,CR3指向页目录指针表,每一项都指向一个页目录表,共有4个页目录表。
为了提高地址转换的效率,x86架构使用TLB对最近用到的页面映射进行缓存。TLB中存放着VFN到PFN的转换记录,当CPU访问某个线性地址时,如果其所在页面的映射存在于TLB中,无须查找页表,即可得到该线性地址对应的PFN,CPU 再将它与线性地址的偏移相加,就能得到最后的物理地址。
➢ x86架构内存管理中心分页机制总结:
1)CPU访问一个线性地址,在TLB中进行匹配,如果地址转换在TLB中,则跳到步骤6。否则,发生了一次TLB Miss(TLB 缺失),继续步骤2。
2)查找页表,如果页面在物理内存中,则跳到步骤4。
3)如果页面不在物理内存中,则产生缺页错误,由操作系统的缺页错误处理程序进行以下处理。
将页面从磁盘复制到物理内存中。
更改对应的PTE,设置P 位为1,并对其他字段进行相应的设置。
刷新TLB 中对应的PTE。
从缺页错误处理程序中返回。
4)此时,页面已经存在于物理内存中,并且页表也已经包含了这个映射。重新在TLB中进行匹配,如果地址转换在TLB中,则跳到步骤6。否则,发生了一次TLB Miss(TLB 缺失),继续步骤5。
5)CPU重新查页表,把对应的映射插入到TLB中。
6)此时,TLB已经包含了该线性地址对应的PFN。将PFN和线性地址中的偏移量相加,就得到了对应的物理地址。
未完待续......