三种地址
今天我们来学习一下 Linux 下的内存寻址,通常我们在谈内存地址的时候,我们在谈什么呢?所以首先我们得明确三种地址(以80x86微处理器为例):
- 逻辑地址(logical address):机器语言指令中用来指定一个操作数或者一条指令的地址,每一个逻辑地址由一个段和偏移量组成,偏移量指明了从段开始的地方到实际地址之间的距离
- 线性地址(linear address 也叫做虚拟地址 virtual address):是一个 32 位无符号整数,可以用来表达 4GB 的地址,通常用十六进制数表示
- 物理地址(physical address):用于内存芯片内的内存单元寻址,它们从微处理器的地址引脚发送到内存总线上的电信号对应。
以上内容来自 《Understanding The Linux Kernel》
内存管理单元(Memory Management Unit, MMU)通过分段单元的把一个逻辑地址转换成线性地址,通过分页单元把线性地址转换成物理地址。
从 80286 开始,Intel 处理器以两种不同的方式执行地址转换,分别为实模式(real mode)和保护模式(protected mode)。下面我们就展开描述,在保护模式下,硬件的分段机制和分页机制
分段机制
段选择符和段寄存器
逻辑地址有两部分组成:一个段标识符和一个偏移量。短标识符是一个 16 位的字段,成为段选择符;偏移量是一个 32 位长的字段。
为了快速找到段选择符,处理器提供了段寄存器用来存放段选择符,分别为 cs,ss,ds,es,fs,gs。
其中有三个有专门的用途:
- cs:代码段寄存器,指向包含程序指令的段
- ss:栈段寄存器,指向包含当前程序栈的段
- ds:数据段寄存器,指向包含静态数据或者全局数据段
其中,cs 含有一个两位的字段,用来指明当前的 CPU 特权等级(CPL),0 代表最高等级、3 代表最低等级。 Linux 只用到了 0 和 3,分别称为 内核态 和 用户态
段描述符
每个段由一个 8 字节的段描述符表示,描述了段的基本信息。段描述符放在全局描述符表(GDT)或者局部描述符表(LDT)中。
通常只会定义一个 GDT,每个进程除了放在 GDT 中的段以外,如还需要创建附加的段,就可以有自己的 LDT。GDT 在主存中的地址和大小存放在 gdtr 控制寄存器中,LDT 的地址和大小则存放在 ldtr 中。
段描述符包涵以下关键字段:
- Base:包含段的首字节的线性地址
- Type:描述了段的类型特征和它的存取权限
- DPL:限制对这个段的存取权限,表示访问这个段的要求的最小 CPU 特权等级
- P:Segment-Present 标志,表明当前段是否在内存中。Linux 总是把这个标志设为 1,从来不会把整个段交换到磁盘上去
分段单元
介绍完上述的概念,那么逻辑地址是如何转换到线性地址的呢?我们通过下面的步骤来简单说明:
- 先检查段选择符的 TI 字段,以决定段描述符保存在 GDT 中还是在 LDT 中。如果在 GDT 中,分段单元从 gdtr 中得到 GDT 的线性基地址,如果在 LDT 中,分段单元从 ldtr 中得到 LDT 的线性基地址
- 从段选择符的 index 字段计算段描述符的地址,计算方法为 index * 8 (一个段描述符为 8 字节),将这个结果与 gdtr 或者 ldtr 中的基地址相加
- 把逻辑地址中的偏移量与段描述符中的 Base 值相加,就得到了线性地址
快速访问分段机制
如果每次都执行上述的过程,可能会比较耗时,因为 GDT 是存储在主存中的,每次都访问主存,可能会比较慢,所以为了提高逻辑地址到线性地址的转换速度,80x86 处理器提供了一组6个不可编程寄存器。每一个不可编程寄存器含有 8 个字节的段描述符,具体的值由相对应的段寄存器中的段描述符确定。每当一个段选择符被装入段寄存器,相对应的段描述符就由主存装入到对应的不可编程寄存器,这样就可以不需要上面三个过程中的前两个,就可以得到线性地址了。
分页机制
页、页框和页表
分页单元把线性地址转换成物理地址,其中的关键任务是把所请求的访问类型与线性地址的访问权限做对比。
- 页:为了更高效和更经济的管理内存,线性地址被分为以固定长度为单位的组,成为页。页内部连续的线性地址空间被映射到连续的物理地址中。这样,内核可以指定一个页的物理地址和对应的存取权限,而不用指定全部线性地址的存取权限。这里说页,同时指一组线性地址以及这组地址包含的数据
- 页框:分页单元把所有的 RAM 分成固定长度的页框,每一个页框包含一个页。页框是主存的一部分,因此也是一个存储区域。页和页框相比,前者只是一个数据块,可以存放在页框或者磁盘中。
- 页表:把线性地址映射到物理地址的数据结构成为页表,页表存放在主存中,并在启用分页单元之前必须由内核对页表进行适当的初始化
常规的分页
从 80386 开始,Intel 处理器的页大小为 4KB。
32 位的线性地址被分为 3 个域:
- Directory(目录):最高 10 位
- Table(页表):中间 10 位
- Offset(偏移量):最低 12 位
线性地址的转换分两步完成,每一步都基于一种转换表,第一种转换表成为页目录表,第二种转换表成为页表。
为什么需要两级呢?目的在于减少每个进程页表所需的 RAM 的数量。如果使用简单的一级页表,将需要高达 2^20 个表项来表示每个进程的页表,即时一个进程并不使用所有的地址,二级模式通过职位进程实际使用的那些虚拟内存区请求页表来减少内存容量。每个活动的进程必须有一个页目录,但是却没有必要马上为所有进程的所有页表都分配 RAM,只有在实际需要一个页表时候才给该页表分配 RAM。
页目录项和页表项的结构如下:
- Present 标志:为 1 则表示页在主存中;如果为 0 则表示不在内存中,如果执行一个地址转换的时候,所需的页表项或者页目录项中的该标志为 0,那么分页单元就把该线性地址存在在控制寄存器 cr2 中,并产生 14 号异常:缺页异常。
- 包含页框物理地址最高 20 位的字段
- Dirty:当对页框进行写操作时就设置这个标志
- Read/Write 标志:含有页或者页表的存取权限
- User/Supervisor:含有访问页或者页表所需的特权等级
了解了以上结构之后,我们看看如何从线性地址转换到物理地址的:
- 线性地址中的 Directory 字段决定页目录中的目录项,目录项指向适当的页表
- 线性地址中的 Table 字段又决定页表的页表项,页表项含有页所在页框的物理地址
- 线性地址中的 Offset 地段决定了页框内的相对位置,由于 offset 为 12 为,所以一页含有 4096 字节的数据
以上描述的为 80x86 微处理器硬件分页机制,不同架构的 64 位处理器分页机制,大体的思路就是将二级模式拓展为三级(ia64)或者四级(x86_64),以达到对更大范围寻址空间的支持。具体到 Linux 中如何使用操作系统的分段分页机制以及进程的地址空间管理,后续再谈