120 / 163 · C11 · 约 8 分钟
分段
先记住这句话
分段用多组独立的基址与界限寄存器把代码、堆、栈分别放到物理内存的不同位置,从而避免稀疏地址空间中的空洞浪费物理页。
单一基址界限对的浪费
把整个虚拟地址空间连续搬进物理内存时,堆与栈之间的空洞仍然占用真实RAM。32位空间里程序可能只用几兆,空洞却可达数GB,既浪费又难以运行大于物理内存的映像。
每段独立的基址与界限
硬件为每个逻辑段(代码、堆、栈)配备一对寄存器。操作系统因此可以把各段放到物理内存任意空闲区域,只为真正使用的字节分配空间,稀疏地址空间因而变得可行。
用高位选择段并检查偏移
虚拟地址的高几位直接索引段寄存器,剩余位作为段内偏移。硬件先比较偏移与界限,越界立刻陷入;合法时再把基址加上偏移得到物理地址。
段扩展与外部碎片
虽然空洞不再占用内存,但要把某个段变大仍需找到足够大的连续物理空洞。长期运行后物理内存会出现许多不规则碎片,这是分段留下的主要管理难题。
常见误区
- 把整个虚拟地址直接加到基址上而忘记先抽出段内偏移
- 用高位选段会把每个段的最大长度锁死在地址空间的固定切片内
运行一个例子
最低标准 C11 · 完整程序 · 下载 .c
#include <stdio.h>
#include <stdint.h>
int main(void) {
const uint32_t base[3] = {32u * 1024, 34u * 1024, 28u * 1024};
const uint32_t bounds[3] = {2u * 1024, 3u * 1024, 2u * 1024};
const char *names[3] = {"code", "heap", "stack"};
const uint32_t vas[3] = {100, 4200, 7168};
for (int i = 0; i < 3; i++) {
uint32_t va = vas[i];
uint32_t seg = (va >> 12) & 3u;
uint32_t offset = va & 0xFFFu;
printf("VA %u (0x%x): ", va, va);
if (seg > 2u) {
printf("invalid segment\n");
continue;
}
if (offset >= bounds[seg]) {
printf("segmentation fault (offset %u >= %u in %s)\n",
offset, bounds[seg], names[seg]);
} else {
uint32_t pa = base[seg] + offset;
printf("segment %s, offset %u -> PA %u\n",
names[seg], offset, pa);
}
}
return 0;
}
在本地编译
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror ostep-16-segmentation.c -o example && ./example预期结果
VA 100 (0x64): segment code, offset 100 -> PA 32868
VA 4200 (0x1068): segment heap, offset 104 -> PA 34920
VA 7168 (0x1c00): segmentation fault (offset 3072 >= 3072 in heap)
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
14位虚拟地址0x0500,高2位选择段。代码段基址32768、大小2048。请给出物理地址或说明是否产生段错误。
查看参考答案
高2位为00,选中代码段。偏移0x500=1280小于2048,合法。物理地址=32768+1280=34048。
继续查证
标准草案与官方章节会更新;版本标记只说明示例最低要求。