- 4.1.2 段式内存管理
-
除了用虚拟地址来实现有效和灵活的内存管理以外,在计算机发展史上,另一种重要的内存管理方式是将物理内存划分成若干个段(segment),处理器在访问一个内存单元时,通过“段基址+偏移”的方式计算出实际的物理地址。每个段都可以有自己的访问属性,包括读写属性、特权级别等。例如,在Intel x86 处理器中,有专门的段寄存器,允许每条指令在访问内存时指定在哪个段上进行。段的概念在Intel 8086/8088 实模式中已经使用了,但当时段的用途是扩展地址范围,将系统的内存从 64 KB 扩展到 1 MB。处理器中的寄存器(包括段寄存器)都是 16 位的,但地址线有 20 根。为了访问整个地址范围,处理器的做法是,将段寄存器中的值左移 4 位再加上一个 16 位地址值,就形成了目标地址。实际上,这么做可以达到的最大地址值是(64 K−1) ×16+(64 K −1)=1 114 095。到了 80286 处理器(24 位地址),在保护模式下,段变成了一个索引,指向段描述符表中的某一个段描述符,而段描述符才真正指定了段的基地址、段长度以及一些保护属性。而到了80386 以后,处理器中的寄存器才真正转移到了 32 位。本节接下来将基于 32 位的段寻址模式来讨论段式内存管理。
在Intel x86 中,逻辑地址的段部分称为段选择符(segment selector ),指定了段的索引以及要访问的特权级别。段寄存器 cs 、ss 、ds 、es 、fs 和gs 专门用于指定一个地址的段选择符。虽然只有这六个段寄存器,但是软件可以灵活地使用它们来完成各种功能。其中
有三个段寄存器有特殊的用途:• cs :代码段寄存器,指向一个包含指令的段,即代码段。
• ss :栈段寄存器,指向一个包含当前调用栈的段,即栈段。
• ds :数据段寄存器,指向一个包含全局和静态数据的段,即数据段。
虽然地址寄存器和数据寄存器都是32 位,但是段选择符只有16 位,其格式如图 4.5所示。
在段选择符中,段索引指定了一个段在段描述符表中的编号,它有13位,这也说明了一个段描述符表只包含213=8 192 个段;表指示位说明了此段位于全局描述符表(GDT,Global Descriptor Table )还是局部描述符表(LDT ,Local Descriptor Table)中。当前特权级(CPL ,Current Privilege Level),也称为请求者特权级,是一个两位的值(0~3 ),代表了请求者的当前特权级别。特权级是 CPU 的运行模式,0 表示最高特权级,3 表示最低特权级。Windows 和Linux 只使用这两种特权级,分别称为内核模式(kernel-mode)和用户模式(user-mode)。
在介绍GDT和LDT 两张表以前,我们先来看一看段描述符的内容。每个段描述符用来定义一个段,其中包括段的起始地址、有效范围和一些属性。图 4.6给出了段描述符的结构。段描述符指定了 32位基地址,以及 20位段长度(即段内最大偏移)。当 G 位为0时,此长度单位为字节;当G 位为1 时,此长度单位为4 096 字节。所以,段长度可达220×4 096=4 GB ,即整个32位线性地址空间。描述符特权级(DPL ,Descriptor Privilege Level)是允许访问此段的最低特权级,比如,DPL 为0 的段只有当CPL=0 时才可以访问,而DPL 为3 的段,可由任何 CPL 的代码访问。类型域(共 4 位)指定了段的类型,包括代码段、数据段、TSS 段和LDT 段。
接下来讨论GDT和LDT 。顾名思义,全局描述符表 GDT是系统全局范围内有效的一张表,它包含最多8 192 个段描述符,所以,一张完全的 GDT表需要8 192×8=64 KB内存空间。CPU 有一个寄存器gdtr 包含了GDT的地址。除了GDT外,处理器另有一个LDT ,它也同样最多包含 8 192个段描述符,对应于 LDT 的寄存器为ldtr ,它包含了 LDT的地址。现在我们可以理解Intel x86 转译一个逻辑地址的过程,如图4.7所示。
处理器在解析一个“段+ 偏移”的逻辑地址时,首先根据段寄存器中的表指示位确定应该使用GDT(若表指示位为0)还是 LDT (表指示位为1),然后从 gdtr 或ldtr 中得到描述符表的地址,再加上段索引部分乘以 8,即得到段描述符的地址,然后根据段描述符的格式,拼出 32位段基地址,最后加上 CPU 指令中的偏移值,得到最终的线性地址。在实际执行指令过程中,每个段寄存器内部都有一个8 字节的缓存(或称为内部寄存器),存放了对应于段寄存器的段描述符,如果段寄存器没有改变,则以上的地址计算过程可以省略查描述符表的步骤,从而直接在处理器内部计算出线性地址。
现在我们可以想象一下,如何利用以上介绍的段式内存访问机制来实现操作系统中的多进程地址空间。显然,一种自然的设计思想是,系统全局共享的空间可以通过GDT来安排和访问,比如,操作系统本身的代码和数据是系统全局可见的,各个进程的地址空间中不可避免地要包含这部分内存。此外,各个进程私有的数据和代码存放在LDT 中,因而进程切换时,只需改变 LDT 表,即可实现进程私有地址的切换。在现代的应用程序中,每个进程往往包含多个二进制模块,以及一些动态数据区;各个二进制模块中既有代码,也有数据(比如全局变量和静态变量等)。所以,如果用段机制来管理内存的话,每个模块都需要一个段,动态数据区往往需要一个或多个段(比如全局堆和局部堆,以及栈等)。对于绝大多数应用程序,8 192个段(指 LDT 中的段)足够使用了。对于系统全局空间,只要小心地安排好段的使用,8 192 个段(指 GDT中的段)也能满足正常的内存分配。利用这种方法,既可以做到进程之间的空间隔离性,也可以很方便地在进程之间共享数据。
最后需要说明的是,段式内存管理和页式内存管理并不是对立的,它们可以组合起来在同一个系统中使用。事实上,Intel x86 处理器的内存管理单元(MMU,Memory Management Unit )结合了这两种寻址方法。例如,图4.8显示了一个逻辑地址被解析成虚拟地址,再进一步被解析成物理地址的全过程。操作系统可以有选择地使用段式内存管理单元或页式内存管理单元来管理进程地址空间和系统物理内存。Windows 和Linux 都选择了页式内存管理作为主要的内存管理手段,但同时也不可避免地涉及了段机制。我们在 Windows 和Linux的内核代码中都可以看到有关GDT和段操作的代码。