第 15 讲:分段、段页式与 x86 地址映射
分页主要回答“怎么高效分配物理内存”,分段则从程序的逻辑结构出发:代码、数据、栈、共享库是性质不同的对象,应当能独立增长、保护和共享。
分段的地址是二维的
分段地址写作二元组 :
- 是段号,表示选哪个逻辑对象;
- 是段内偏移,每个段都从 0 开始。
段表项至少包含物理基址 base、段长 limit 和权限。转换顺序是:
- 检查段号 是否落在段表长度内;
- 找到段表项,检查存在位与访问权限;
- 检查 ;
- 计算 。
越界检查必须在加基址前做,不能只看加出来的物理地址“似乎在内存中”。
转换算例
设段 3 的段表项为 base=40000, limit=1200:
- 合法,物理地址 ;
- 越过段长,应产生保护异常,不能算成 41500。
为什么分段更易共享和保护
一个 160 KiB 编辑器代码被 40 个用户进程共享:
- 分页时,4 KiB 页需要让每个进程的 40 个页表项指向共享页框;
- 分段时,每个进程只需一个代码段描述符指向同一物理区,并标记只读可执行。
段边界与逻辑边界对齐,所以“这段代码可共享但不可写”很自然。页是物理粒度,一页可能恰好混入两个逻辑模块,语义不如段直观。
分页与分段的对比
| 角度 | 分页 | 分段 |
|---|---|---|
| 单位 | 固定大小的物理管理单位 | 可变长的逻辑信息单位 |
| 地址空间 | 一维 | 段号 + 偏移的二维结构 |
| 碎片 | 页内的内部碎片 | 可变段之间的外部碎片 |
| 对用户 | 通常透明 | 可见或与程序模块对应 |
| 共享/保护 | 按页 | 按逻辑段,更自然 |
分段的突出问题是可变长连续分配依然有外部碎片,段增长可能迫使移动。
段页式:逻辑按段,物理按页
段页式将一个进程先分成段,再将每段分成页。逻辑地址变成 :
- 段号 查段表,得到该段的页表基址和页数;
- 检查页号 是否越过该段页数;
- 用 查该段页表,得到 PFN;
- 用 得物理地址。
好处是段能按逻辑独立保护和共享,段内又不必占连续物理内存;代价是表更多,转换链更长。
x86 的两阶段地址映射
课件以 32 位 x86 保护模式为例:
逻辑地址:段选择子 : 段内偏移
↓ 分段机制(GDT/LDT + 段描述符)
线性地址:Directory | Table | Offset
↓ 分页机制(CR3 + PDE + PTE)
物理地址:PFN | Offset
第一阶段:段选择子找段描述符
x86 有 CS、DS、SS、ES、FS、GS 等段寄存器。寄存器中不直接存整个段描述符,而存一个选择子,其中包含:
- 描述符表内索引;
- 选 GDT 还是当前任务的 LDT;
- 请求特权级。
GDT 是全局描述符表,LDT 是局部/任务相关表。段描述符记录基址、界限、类型、特权级、存在位和粒度等。处理器检查权限和界限后:
第二阶段:线性地址查两级页表
4 KiB 页的 32 位线性地址常拆为 10 | 10 | 12。
- CR3 指向页目录的物理基址;
- 高 10 位索引 PDE,PDE 指向二级页表;
- 中 10 位索引 PTE,PTE 给出物理页框;
- 低 12 位偏移原样拼接。
PDE/PTE 中的存在位 P、R/W、U/S、访问位 A、脏位 D 等决定页面当前是否在内存、允许谁怎样访问,也为页面置换提供状态。
Linux 为什么说是“扁平段”
x86 保护模式的分段机制不能简单消失,但 Linux 通常将主要代码/数据段设为基址 0、覆盖整个线性空间。这样:
真正的虚拟内存隔离和粒度保护主要由分页完成。“扁平”不是说硬件完全没有段检查,而是把段的地址变换影响降到最小。
三类越界不要混
- 段号越界:选不到合法段描述符;
- 段内偏移越界:偏移超过 limit;
- 分页保护/不存在:线性地址已经形成,但 PDE/PTE 不允许或页尚未驻留。
转换是分阶段的,所以题目问“是否能得到物理地址”时,必须沿段检查—线性地址—页检查的顺序逐步回答。