从零写 OS 内核-第五篇:启动分页机制 —— 构建虚拟内存的基石

“段机制只是过渡,分页才是现代操作系统的真正护城河。 今天,我们亲手开启 x86 的分页功能,让内核拥有虚拟内存!”

在上一篇中,我们深入理解了 保护模式与段式内存管理 ,并亲手实现了从实模式到保护模式的切换。 但你可能已经听说: 现代操作系统几乎不用段机制 ,而是依赖 分页(Paging) 实现内存隔离与虚拟化。

为什么? 因为分页提供了更灵活、更高效的内存管理方式: ✅ 支持非连续物理内存映射为连续虚拟地址 ✅ 实现进程间内存隔离(安全!) ✅ 支持按需换页、共享内存、写时复制等高级特性

今天,我们就来 在 32 位 x86 保护模式下,启动最基础的分页机制 ,为后续的内核开发打下坚实基础。


🧱 一、为什么需要分页?段机制不够吗?

段机制虽然能实现内存保护和特权级,但它存在致命缺陷:

问题 说明
段必须连续 一个段在物理内存中必须是连续的,无法利用碎片内存
段大小固定 段界限一旦设定,难以动态扩展
切换开销大 切换任务需切换整个段描述符表
硬件支持弱 现代 CPU 优化重点在分页,而非分段

而 分页机制 将内存划分为固定大小的 页(Page) (x86 中通常为 4KB),通过 页表(Page Table) 将 虚拟地址 → 物理地址 映射,完美解决上述问题。

💡 Linux、Windows 等现代 OS 都采用“平坦段模型 + 分页” : 所有段都设为 0x00000000 ~ 0xFFFFFFFF , 段机制“退居二线” , 分页扛起内存管理大旗 。


🗺️ 二、x86 分页机制原理(32 位)

x86 32 位分页采用 两级页表结构 :

  1. 页目录(Page Directory)

    :1 个,1024 项,每项 4 字节 →
    4KB

  2. 页表(Page Table)

    :最多 1024 个,每个 1024 项 →
    每个 4KB

🔍 虚拟地址如何翻译?

32 位虚拟地址被划分为三部分:

翻译流程:

  1. CPU 从
    CR3 寄存器
    读取
    页目录基地址(物理地址)

  2. 高 10 位
    作为索引,查页目录项 → 得到
    页表基地址

  3. 中间 10 位
    作为索引,查页表项 → 得到
    物理页帧地址
  4. 拼接
    低 12 位偏移
    → 最终物理地址

📌 所有地址(页目录、页表、页帧)都是物理地址! 分页开启后,程序员看到的“地址”都是 虚拟地址 ,硬件自动翻译。


📦 三、页表项(PTE)与页目录项(PDE)结构

每个页表项(4 字节)包含:

含义
0 (P) 存在位 :1=页在内存,0=缺页
1 (R/W) 读写位 :1=可写,0=只读
2 (U/S) 用户/超级用户位 :1=用户态可访问,0=仅内核
3 (PWT) 页写通(缓存策略)
4 (PCD) 页缓存禁用
5 (A) 访问位(CPU 自动置 1)
6 (D) 脏位(仅页表项,写时置 1)
7 (PS) 页大小 (仅页目录项):1=4MB 大页,0=4KB
12-31 物理页帧地址高 20 位 (4KB 对齐)

✅ 我们先用最简配置: P=1, R/W=1, U/S=0 (内核可读写)


⚙️ 四、实战:开启分页(Identity Mapping)

为简单起见,我们先做 恒等映射(Identity Mapping) : 虚拟地址 = 物理地址 (例如:虚拟 0xC0000000 → 物理 0xC0000000 )

💡 恒等映射是启动阶段的标准做法,后续可再建立更复杂的映射(如内核高地址映射)。

步骤 1:准备页目录和页表

我们需要:

1 个页目录(4KB)
若干页表(每 4MB 虚拟地址需 1 个页表)

假设我们只映射前 4MB 内存 (足够内核初期使用):

🔑 aligned(4096) 确保页表/目录 4KB 对齐 (x86 强制要求)!

步骤 2:初始化页表(映射 0x00000000 ~ 0x003FFFFF)

步骤 3:初始化页目录(指向第一个页表)

⚠️ 关键点 : 如果你的内核链接在高地址(如 0xC0000000 ),但实际加载在 0x100000 , 则需计算 物理地址 : 物理 = 虚拟地址 – 虚拟基地址 。

步骤 4:加载 CR3 并开启分页

🌟 开启分页后,所有内存访问都经过地址翻译!


🛠️ 五、完整启动流程(保护模式 + 分页)

  1. 实模式


    保护模式
    (设置 GDT,加载 CR0.PE)

  2. 设置栈

    ,调用 C 代码

  3. 在 C 中初始化页目录/页表

    (恒等映射)

  4. 加载 CR3
  5. 设置 CR0.PG = 1


    分页开启!

此时,你的内核已运行在 带虚拟内存的保护模式 下!


⚠️ 六、常见陷阱与注意事项

  1. 地址混淆:虚拟 vs 物理

    页表/目录中存储的
    必须是物理地址
    若内核链接在高地址(如
    0xC0000000
    ),需手动转换

  2. 对齐要求

    页目录、页表
    必须 4KB 对齐
    使用
    attribute((aligned(4096)))
    或链接脚本控制

  3. TLB 缓冲

    修改页表后,需
    刷新 TLB
    (如
    invlpg
    指令,或远跳转)
    否则 CPU 可能使用旧映射

  4. 栈位置

    确保栈所在区域已被映射!否则开启分页后立即崩溃


🌱 七、下一步:更高级的内存布局

恒等映射只是起点。接下来你可以:

目标 说明
高半内核映射 将内核链接到 0xC0000000 以上,用户空间留低地址
动态页表管理 实现 map_page() / unmap_page()
物理内存管理器 跟踪空闲物理页(Bitmap / Buddy System)
缺页中断处理 实现按需分配、换页等高级功能

📚 这些将是后续 kmalloc 、进程管理、文件系统的基础!


💬 写在最后

分页机制看似复杂, 但它正是现代操作系统 安全、稳定、高效 的基石。

今天你开启的不只是一个 CPU 特性, 而是 虚拟内存时代的大门 。

🌟 从恒等映射开始,你终将构建属于自己的地址空间。


📬 动手挑战 : 尝试在你的内核中开启分页,并打印一个通过虚拟地址访问的变量地址。 欢迎在评论区分享你的实现或遇到的“玄学崩溃”!

👇 下一篇你想看: 物理内存管理器(Bitmap 实现) ,还是 中断与 IDT 实战 ?


#操作系统 #内核开发 #x86 #分页 #虚拟内存 #Paging #内存管理 #从零开始


📢 彩蛋 :关注后回复关键词 “paging” ,获取:
完整分页初始化 C 代码模板
恒等映射与高半内核映射对比图
Intel 手册分页章节精要(中文)

最后修改: 2026年7月1日

作者

评论

发表评论

您的邮箱地址不会被公开。