80386内存管理
80386转换逻辑地址到物理地址经过下面的两步:
- 段翻译,把一个逻辑地址(包含一个段选择子和段偏移量)转换成一个线性地址
- 页翻译,把一个线性地址准换成一个物理地址,这一步是可选的,取决于系统软件的设计者,在保护模式下段翻译是必须的而页翻译是可选的
这些转换对应用程序而言都是不可见的,下图描述了两种翻译的过程。
段翻译
下图展现了段翻译的细节,显示了处理器如何把一个逻辑地址转换成一个线性地址
为了执行段翻译,处理器需要使用下面这些数据结构
- 段描述符
- 段描述符表
- 段选择子
- 段寄存器
段描述符
段描述符提供了提供了段翻译所需要的数据,段段描述符由编译器,链接器,装载器或者操作系统创建,不能由应用程序编写者创建,下图是一个段段描述符的常见结构。
可以看到DPL之后的一位把段描述符分为了两种类型,第一种用于存放代码段和数据段,第二种用于系统段。
接下来解释一些比较重要的位:
- BASE:由三个部分组成,表示4GB空间内段的位置。
- LIMIT:指定了端的大小,由两部分组成了一个20位大小的数字。具体表示的大小还要根据G(granularity,粒度)位来决定。
- G(granularity):如果该位被设置为0,LIMIT的一个单元大小为一个字节,也就是表示0~1MB大小的空间,如果该位被设置为1,LIMIT一个单元大小为4KB,表示0 ~ 4GB大小的空间。
- TYPE:区分各种类型的段描述符
- DPL(descriptor privilege level):被用作保护模式
- P(present):表示段描述符是否有效,下图描述了一个无效的段段描述符,其中被标记为AVAIABLE的位置可以被操作系统修改。
- A(accessed):表示段描述符是否可以被访问
段描述符表
段描述符储存在下面两种段描述符表中:
- The global descriptor table(GDT)
- A local descriptortable (LDT)
一个段描述符表的每一个元素是一个8字节的段描述符,一个段段描述符表最多可以有8192()个元素,同时要注意的是全局段段描述符表的第一个元素对处理器来说是不可用的。
那么处理器怎么知道段描述符表所在的位置的呢,操作系统把GDT和LDT的地址装在一个GDTR
和LDTR
寄存器中,这个寄存器存放着段描述符表的地址和段描述表的长度。我们可以通过指令LGDT和SGDT把给定的数据装载到GDT寄存器中,通过LLDT和SLDT把给定的数据装载到LDT寄存器中。
段选择子
段选择子储存了段描述符表的索引选择段描述符的类型。段选择子在指针变量中对于应用程序可能是可见的,但是它的值往往由链接器指定,下图展示了段选择子的结构。
- index:段描述符表的索引(下标),用于指定选择段描述符表中哪个元素,因为index是一个13位的数字,所以一个段描述符最多可以有个元素。
- TI(table indicator),用于指定从GDT还是LDT中选择,0选择GDT,1选择LDT。
- RPL(Requested Privilege Level),用于保护模式。
由于全局段描述符表的第一个元素不可用,所以当INDEX和TI都被设置为0时我们认为这是一个空段选择子。
段寄存器
80386会把段寄存器对应的段描述符信息储存在段寄存器中,这避免了频繁访问内存,下面是段寄存器的结构,应用程序可见和可操控的实际上只有16位,剩下的位由处理器处理,会把段寄存器对应的信息储存在段寄存器中,由于这种机制的存在,所以我们实际上访问数据的效率还是相当高的。
页翻译
在第一个阶段,80386把一个逻辑地址通过段翻译转换成了一个线性地址,在第二个阶段,80386会通过也页翻译把一个线性地址翻译成真实的物理地址。
页帧
页帧是在地址上连续的4KB大小的单元。
线性地址
如下图所示,线性地址会被拆分成三个部分,第一个部分的值被用作页目录的索引,第二个部分的值被用作页表的索引,第三个值被用作页的偏移量。
整个翻译的过程由下图所示:
页目录
页目录是一个长度的1K的32位的数组,总共占用4KB的空间,正好是一页。页目录的每个元素定位了一个页表所在的位置,页表也是一个长度为1K的32位数组,一个页表总共占用4KB空间,定位了一个页所在的位置。在一个32位虚拟地址中前10位表示页目录的索引,中间10位表示页表的索引,最后12位表示页内偏移量,正好表示32位空间。
32位的虚拟地址分成了三个部分,但是这三个部分并没有页目录的地址,所以硬件是怎么知道页目录的地址的呢。硬件依赖的是CR3寄存器,也叫做page directory base register(PDBR),这个寄存器存放了页目录的首地址,借助于这个寄存器我们可以完成整个翻译操作。
页目录的结构和页表类似,可以在页表部分找到页表的结构。
页表
页表的结构和页目录相似,都是占用一页的空间,是一个长度为1K的32位数组。
下图是一个页表实体有效和无效时的格式。
-
P:最低1位表示存在位Presnt,当该位为1时表示该页表实体存在,当该位1为0时表示页表实体不存在。
-
R/W:读写权限
-
U/S:权限等级
-
D:dirty,脏页
-
A:access,是否可以被访问
-
AVAILABLE:可以被操作系统修改的部分
页翻译缓存
TLB
还是仰赖于局部性原理,页翻译缓存TLB(translation look-aside buffer)的存在大大加速了页翻译的过程。
结合段翻译和页翻译机制
了解了段翻译和页翻译的机制之后,我们来看看整体的翻译过程。
最后,我们需要把两种机制结合起来,我们有多种架构可以选择。
平坦架构
这是一种简单的架构,在平坦架构中,我们不使用段,仅仅通过32位的偏移量来表示地址。在80386中我们不能禁止段机制,但是我们可以通过把段全部设置为0实现相同的效果。
一个段跨越多个页
80386允许一个段大小或者小于一个页,举个例子,当我们使用了一个占用空间大小为132KB的数据结构时,我们显然无法在1个页内放下如此多的数据,我们需要133个页才能放下全部的内容。
一个页跨越多个段
从另一个方面来说,段也可以比页小,比如我们可以考虑一个比较小的数据结构——信号,因为段的保护和共享机制,所以我们可能要为每个信号都去分配一个段,但是如果我们为每一个段都分配一个页,可能会有较大的空间浪费,所以在这种情况下把多个段分配在一个页中是比较合适的选择。
非对齐的页边界和段边界
80386不强制页和段的边界对齐,也就是说一个页可能同时包含一个段的开始和一个段的结束。
对齐的页边界和段边界
但是对齐的边界会让软件设计更加简单,举个例子,我们可以给每个段分配一个固定大小的页,这样页和段的关系就很非常清晰。
一个段对应一整个页表
这种方式采用了一个更加简单的映射机制,我们为每一个段创建一个对应的描述符,这样一个段就可以对应一个页表,这样的段最大可以拥有1K个数的页,也就是4MB大小的空间,这在很多情况下是足够的。