当使用80x86微处理器时,必须区分以下三种不同类型的地址:
逻辑地址(Logical Address)
: 每个逻辑地址都有一个段(segment)
和偏移量(offset)
,偏移量指明了从段起始的地方到实际地址之间的距离。线性地址(Linear Address)
或也称为虚拟地址(Virtual Address)
: 32位无符号整数来表示的高达4GB的地址。物理地址(Physical Address)
: 用于内存芯片级内存单元寻址,和从CPU地址引脚发送到内存总线上的电信号相对应。物理地址由32位或36位无符号数整数表示。
逻辑地址
到物理地址
的转换,需要经过内存控制单元(MMU)的分段单元
和分页单元
的硬件电路(如下图)。
x86微处理器中的分段
一个逻辑地址由两部分组成:
段选择符(segment selector)
: 长度为16bit,如下图所示,后面会详细说明各个字段的用途。偏移量
: 相对于段起始地址的偏移量,长度为32bit。
CPU提供段寄存器用于存放段选择符,这些段寄存器有6个:cs(代码段寄存器)
、ss(栈段寄存器)
、ds(数据段寄存器)
、es(附加数据段寄存器)
、fs
和gs
。cs
寄存器还包含一个2bit的字段,用于指明CPU的当前特权级别(Current Privilege Level, CPL)
,值为0代表最高优先级,值为3代表最低优先级。Linux只使用了0级和3级,分别对应内核态
和用户态
。
每个段由一个8字节的段描述符表示,它描述了段的特征。段描述符放在全局描述符表(Global Descriptor Table, GDT)
或局部描述符表(Local Descriptor Table, LDT)
。GDT在内存中的基地址和大小存放在gdtr
控制寄存器中,当前正在使用的LDT基地址和大小存放在ldtr
控制寄存器中。
通常只定义一个GDT,而每个进程除了存放在GDT中的段之外,如果还需要创建附加的段,就可以有自己的LDT。
段描述符的格式见下图:
数据段描述符和代码段描述符的格式基本类似,以下是对其中一些重要字段的说明:
字段名 | 描述 |
---|---|
Base |
32bit,包含段的首字节的线性地址 |
G |
粒度标志,该位清0,则段大小以字节为单位;否则以4KB的倍数计 |
Limit |
20bit,决定段的长度,G标志位清0,段的大小在1个字节到1MB之间;否则将在4KB到4GB之间 |
S |
系统标志位,该位清0,则说明这是一个系统段。数据段描述符和代码段描述符都将该位置1(非系统段) |
Type |
描述了段的类型特征和它的存取权限(请看表下面的描述) |
DPL |
描述符特权级别(Descriptor Privilege Level),它表示为访问这个段而要求的CPU最小的优先级。 因此,DPL设为0的段只能当CPL为0时(内核态)才是可访问的,而DPL设为3的段对任何CPL值都是可访问的 |
P |
存在位标志,该位清0,则表示当前段不在主存中。Linux总是将该位设为1,因为它永远不会把整个段交换到磁盘上去 |
D或B |
取决于是数据段还是代码段。D或B的含义在两种情况下稍微有所区别,但是如果段偏移量的地址是32位长, 就基本上把它置为1,如果这个偏移量是16位长,它被清0(更详细的说明参见Intel使用手册) |
AVL |
可以由操作系统使用,但是被Linux忽略 |
有了以上这些对段的基本概念的说明之后,我们来看看逻辑地址是如何转换到线性地址的(见下图)。
- 首先,从段寄存器中获取段选择符
- 检查段选择符中的TI字段,决定是从GDT中还是从LDT中读取段描述符,GDT和LDT的起始地址分别存于gdtr和ldtr寄存器中
- 段选择符中的索引号字段的值乘以8(因为段描述符大小为8个字节)再和gdtr或ldtr寄存器中的内容相加,得到了段描述符在GDT或LDT中的真实地址
- 段描述符中Base字段的值和逻辑地址的偏移量相加就得到了线性地址
x86微处理中的分页
分页单元把线性地址转换成物理地址。线性地址被分成以固定长度为单位的页(page)
。页内部连续的线性地址被映射到连续的物理地址中,这样,内核可以指定一个页的物理地址和其存取权限,而不用指定页所包含的全部线性地址的存取权限。遵照习惯说法,术语“页”既指一组线性地址,又指包含在这组地址中的数据。
把线性地址映射到物理地址的数据结构称为页表(page table)
。页表存放在主存中,并在启用分页单元之前必须由内核对页表进行适当的初始化。80x86处理器通过设置cr0寄存器
的PG
标志启用分页,当PG=0
时,线性地址就被直接解释成物理地址。
常规分页
从80386起,Intel处理器的分页单元处理4KB的页。32位的线性地址被分成3个域:
目录(directory)
: 最高10位页表(table)
: 中间10位偏移量(offset)
: 最低12位
线性地址的转换分两步完成: 第一步基于页目录表,第二步基于页表,如下图所示。
使用两级转换表的好处是可以减少每个进程页表所需RAM的数量。如果只使用简单的一级页表,那将需要高达\( 2^{20} \)个表项,也就是说,在每项4个字节时,需要4MB RAM来表示每个进程的页表(如果进程使用全部4GB线性地址空间的话)。
每个活动进程必须有一个分配给它的页目录,不过只有在进程实际需要一个页表的时候才会给该页表分配RAM。当前正在使用的页目录表的起始地址存放在cr3控制寄存器
中。线性地址中的目录
字段决定页目录中的目录项,而目录项指向适当的页表。线性地址中的页表
字段又决定页表中的表项,而表项含有页所在页框的物理地址。偏移量
字段决定页框内的相对位置,由于它的长度是12位,故每一页含有4KB字节的数据。
页目录项和页表项有相同的结构,每项都包含如下一些字段:
字段名 | 描述 |
---|---|
Present |
该标志位置1,则说明所指的页(或页表)在主存中。当执行地址转换所需的页表项或页目录项中的Present
标志被清0时,分页单元会把该线性地址存放到 |
Base |
长度为20bit,等于页框物理地址的最高20位。 |
Accessed |
每当分页单元对相应页框进行寻址时就设置这个标志位。当选中的页被交换出去时,这一标志就可以 由操作系统使用。分页单元从来不重置该标志,必须由操作系统去做。 |
Dirty |
只应用于页表项中。每当对一个页框进行写操作时就设置这个标志。和 |
Read/Write |
页或页表的存取权限标志位(参见 |
User/Supervisor |
含有访问页或页表所需的特权级别(参见 |
PCD和PWT |
控制硬件高速缓存处理页或页表的方式。 |
Page Size |
只应用于页目录项,如果设置为1,则页目录项指的是2MB或4MB的页框(参见下一节
|
Global |
只应用于页表项,用来防止常用页从TLB(translation lookaside buffer)高速缓存中刷新
出去。只有在 |
小结
本文基于80x86对硬件的寻址方案进行了详细的说明,具体描述了分段单元如何将逻辑地址转换成线性地址,以及分页单元进而把线性地址转换成物理地址的细节。然而,针对分页单元,x86还有更多高级的支持,由于篇幅限制,将在下一篇文章中具体介绍分页单元中的高级特性,包括扩展分页、特权保护方案、高速缓存、TLB等。
-->