C++ / a working model

122 / 163   ·   C11   ·   约 8 分钟

分页:引言

先记住这句话

分页将虚拟地址空间和物理内存都切成固定大小的页与页框,彻底避开分段的外部碎片。每个进程用私有页表记录映射,硬件仅需把虚拟页号换成物理框号即可完成转换。

本篇内容
  1. 分页为何优于分段
  2. 地址如何被拆开再拼回
  3. 页表藏在哪里、有多大
  4. 运行示例
  5. 动手练习

官方章节 PDF

分页为何优于分段

可变大小的段很快把空闲空间撕成无法利用的碎片,分配器不得不费力寻找合适空洞。分页把一切变成相同大小的积木,空闲框链表里随便拿一块就能放下任意一页,空间管理瞬间变得简单,也不再需要猜测堆栈的生长方向。

地址如何被拆开再拼回

虚拟地址的高位是虚拟页号,低位是页内偏移。页号拿去页表里查出物理框号,再把框号和原来的偏移直接拼在一起就得到物理地址。偏移永远原样保留,因为它只负责指出页里面的第几个字节。

页表藏在哪里、有多大

页表是进程私有的内核数据结构。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。

继续查证

标准草案与官方章节会更新;版本标记只说明示例最低要求。

回到目录