122 / 163 · C11 · 约 8 分钟
分页:引言
先记住这句话
分页将虚拟地址空间和物理内存都切成固定大小的页与页框,彻底避开分段的外部碎片。每个进程用私有页表记录映射,硬件仅需把虚拟页号换成物理框号即可完成转换。
分页为何优于分段
可变大小的段很快把空闲空间撕成无法利用的碎片,分配器不得不费力寻找合适空洞。分页把一切变成相同大小的积木,空闲框链表里随便拿一块就能放下任意一页,空间管理瞬间变得简单,也不再需要猜测堆栈的生长方向。
地址如何被拆开再拼回
虚拟地址的高位是虚拟页号,低位是页内偏移。页号拿去页表里查出物理框号,再把框号和原来的偏移直接拼在一起就得到物理地址。偏移永远原样保留,因为它只负责指出页里面的第几个字节。
页表藏在哪里、有多大
页表是进程私有的内核数据结构。32位空间配上4KB页会产生大约一百万个表项,每项哪怕只占4字节,一张表就要4MB。上百个进程同时运行时,光翻译信息就会吃掉几百兆内存,因此必须寻找更紧凑的表示法。
常见误区
- 把页内偏移也拿去查页表
- 误以为全系统共用一张页表
- 没意识到页表本身可能比用户数据还大
运行一个例子
最低标准 C11 · 完整程序 · 下载 .c
#include <stdio.h>
#include <stdint.h>
int main(void) {
const uint8_t page_table[4] = {3, 7, 5, 2};
const uint8_t virtual_addrs[4] = {21, 0, 32, 48};
for (int i = 0; i < 4; ++i) {
uint8_t va = virtual_addrs[i];
uint8_t vpn = va >> 4;
uint8_t offset = va & 0x0F;
uint8_t pfn = page_table[vpn];
uint8_t pa = (uint8_t)((pfn << 4) | offset);
printf("VA %u (VPN %u, offset %u) -> PA %u (PFN %u)\n",
va, vpn, offset, pa, pfn);
}
return 0;
}
在本地编译
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror ostep-18-paging.c -o example && ./example预期结果
VA 21 (VPN 1, offset 5) -> PA 117 (PFN 7)
VA 0 (VPN 0, offset 0) -> PA 48 (PFN 3)
VA 32 (VPN 2, offset 0) -> PA 80 (PFN 5)
VA 48 (VPN 3, offset 0) -> PA 32 (PFN 2)
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
64字节虚拟空间、16字节页。虚拟地址21的二进制是010101,其VPN=1被映射到物理框7。最终物理地址是多少?
查看参考答案
117。把框号7左移4位得到112,再与偏移5相加即得117。
继续查证
标准草案与官方章节会更新;版本标记只说明示例最低要求。