LINUX内存管理之x86地址转换
1 地址转换过程
计算机刚刚加电后,运行在实模式下。在实模式下,程序直接操作的物理地址,以80386CPU为例,它只使用32根物理地址线中的20根,所以在实模式下最多能使用IM内存;经过BIOS的引导最终执行到linux内核后,经过前半部分的实模式和保护模式的交替运行,最终会运行在保护模式下。
同样以80386CPU为例,保护模式下可以访问4G内存地址。显然远远超过了1M的内存限制。保护模式不是直接操作物理地址,而是经过系列转换最终转到物理地址执行。其过程如下图:
逻辑地址:用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段标识符和偏移量组成。偏移量表示实际地址到段起始地址的差值。
线性地址:又称虚拟地址,顾名思义就是非真实物理的地址。以80386CPU为例,它的范围从0x00000000到0xffffffff,所以其范围为4G。
物理地址:为CPU访问硬件的真实地址。以80386CPU为例,同样为4G范围。特别地,开启物理地址扩展后,可以访问64G内存。
注:实模式下虽然也有段的概念,但此段非保护模式下的段。以80386为例,实模式的段到物理地址的转换为:32位物理地址 = 段基地址(32位) + 段内偏移(32位)。
2 分段转换
2.1 逻辑地址结构
CPU的分段单元负责将逻辑地址转为线性地址。前面所述,段地址由一个段标识符和偏移量组成,段标识符为16位;偏移量为32位。处理器的段寄存器存储段标识符,段标示符的高13位为段描述符在GDT(即全局描述符表)或LDT(即局部描述符表)中的索引;中1位(即第三位)为TI,即表指示器;低2位为RPL,即请求特权级。TI=0表示描述符在GDT中;TI=1表示描述符在LDT中。RPL对内存访问起到隔离保护作用,共有0至3四个特权级(即CPL),对于Linux而言,0级代表内核态,3级代表用户态,1和2未使用。
以80386CPU为例,段寄存器主要有cs,ss,ds,es,fs和gs。
- cs:代码段寄存器。
- ss:栈段寄存器。
- ds:数据段寄存器。
注:其他3个可以指向任意的数据段。
2.2 段描述符
每个段由一个8字节(即64位)的段描述符来表示段的特征。它存储在全局描述符表(global descriptor table,GDT)或局部描述符表(local descriptor table,LDT)中。其中GDT为系统全局的,为所有进程共享,GDT的起始地址存储在gdtr寄存器中,gdtr中存放的是GDT在内存中的基地址和表长度,基地址指定GDT表的起始线性地址而非物理地址,表长度指明GDT表的字节长度;LDT本身同样是一段内存,其描述符记录在GDT中,可以理解LDT是GDT的二级扩展。
段描述符除了包含一个表示段起始的线性地址的Base字段外,还有如下信息:
- G:为0则段大小以字节为单位,为1则段大小以4k字节为单位;
- Limit:最后一个单元的的偏移量,表示段长度;
- S:为0表示系统段,为1表示普通的代码段或数据段;
- Type:表示该段是一个代码段、数据段、任务状态段或LDT;
- DPL:特权级别。为0表示只能内核态访问,为3表示都可以访问,在Linux下1和2不使用;
- P:为0表示不在内存中,Linux下总为1,因为它不会被换出到磁盘;
注:其他信息可以略过。
32位Linux下,所有的进程使用相同的段寄存器。共分为如下四种段类型,即用户态代码段、用户态数据段、内核态代码段、内核态数据段。其Type依次为10,、2、10、2;DPL依次为3、3、0、0。其他字段相同,值如下:
- Base:0x00000000;
- G:1
- Limit:0xffffffff;
- D/B:1;
- P:1.
因为Base为0x00000000,所以linux线性地址等于逻辑地址;一般只有GDT没有LDT。此外,Linux的GDT除了上面四种段外,还包括TSSD(任务状态段描述符)、中断/异常处理程序段描述符等。
2.3 段地址转换
分段单元转换逻辑地址到线性地址的大致过程如下:
2.4 分段转换速度优化
在80x86CPU中,段寄存器在装入段选择符的时候,相应的段描述符就会被装入到一个非编程CPU寄存器中,此后,分段转换就不用去RAM中的GDT或LDT获取段描述符。仅当段寄存器值改变,才会重新访问GDT或LDT,然后重新将段描述符装载到非编程CPU寄存器。
3 分页转换
3.1 线性地址结构
在x86下,线性地址如下图。共分为三段:高10位为页目录索引;中10位为页表索引;低12位为页内偏移量。
页框:x86下,物理页以4K为单位分成一组。每连续的这4K内存,称为一个页框。
页: 又叫page,内核中是用来描述页框的数据结构。
页表:指向一组页框的连续内存。每个页表可以指向1024个页框,每个页表4K大小。每个指向叶框的数据叫页表项。
页目录:指向一组页表的连续内存。每哥页目录可以指向1024个页表,每个页目录4K大小。每个指向页表的数据叫页目录项。
页目录项和页表结构相同,还包含如下信息:
- Present:为1则所指向页表或页框已经分配,反之,则会产生缺页中断;
- Field:所指向页表或页框的基地址的高20位,因为每个页框以4K对齐,所以只需要高20位,低12位为0;
- Acessed:为1表示页在内存中,反之则被换出到磁盘;
- Dirty:只用于页目录项。对页框写时,与Acessed意义一样;
- Read/Write:指向页表或页框的读写权限,为0是只读。反之可读可写;
- User/Supervisor:指向的页表或页框的特权级(实现用户态和内核态隔离)。为0时,表示CPU的特权级别小于3才可以寻址该页框或页。反之都可以寻址;
- PCD&PWT:硬件高速缓存使用;
- Page Size:只用于页目录项。为1页表为2M或4M,反之为4K;
- Global:只用于页表项。防止页从高速缓存TLB刷新出去。
3.2 线性地址转换
分页单元转换线性地址到物理地址的大致过程如下:
注:在转换过程中,如果页表或页框没有分配,会产生缺页异常。这个时候操作系统会分配一页;此外,转换过程中还会检查权限,例如对只读页表或页框的写操作会产生异常。CPU当前特权级别为3时,访问User/Supervisor为0的页表或页框也会产生异常。
3.3 分页转换加速
当一个线性地址转换成物理地址后,会把该线性地址与物理地址的映射关系储存在转换后缓缓冲器(即TLB)中。每个CPU都有自己的TLB,当下次访问该线性地址时,先是从TLB中读取相应的物理地址,若没有则执行分页转换。特别地,当cr3寄存器被修改时,TLB表项将被刷掉。
3.4 扩展分页
80x86引入了扩展分页机制,当页目录项的Page Size标志被设置,开启扩展分页。在扩展分页下,线性地址分为两部分。如下图:
相对于常规分页模式,将页表索引与页内偏移量合并为页内偏移量。所以扩展分页机制下页内偏移量共22位,每个页框大小为4M。
3.5 物理地址扩展
英特尔引入了一种称为物理地址扩展机制,即PAE模式。开启PAE模式后,物理地址为36位,共可以访问64G内存。PAE模式下同样分为常规分页和扩展分页两种分页模式。
常规分页模式下页表和页目录同样是4K,只不过页表项和页目录项大小为64位而非32位,其中有24位表示基地址。每个页表或页目录共512个页表项或页目录项。其线性地址结构如下:
页目录指针表(PDPT):PAE模式下,比非PAE模式多了一个页目录指针表,用来存储页目录的基地址。PDPT共4项,每项64位。cr3存储PDPT的基地址。
扩展分页模式下,PAE页框大小为2M。相对于常规分页,少了页目录索引项。其线性地址结构如下:
3.6 64位分页转换
64位下的分页各个平台有所差异。以x86_64平台为例,采用4K页框大小、4级分页、每级9位加12位的页内偏移,即共可以访问48位的物理地址。
4 Linux分页实现
linux平台四级分页模式来满足64位下的分页,且兼容32位下非PAE模式和PAE模式的常规分页机制。其线性地址结构如下:
在32位非PAE模式下,页上级目录索引和页中间目录索引的位全为0,来消除页上级目录和页中间目录。这又叫折叠页上级目录和页中间目录。可以通过将页目录指向的页表的第一项设置指向页表本身的基地址来做到折叠效果,因为页表占4K,其空间本身也为单独一页。
在32位PAE模式下,页全局目录对应PDPT索引、页中间目录对应页目录;同时将页上级目录折叠。