从零写 OS 内核-第五篇:启动分页机制 —— 构建虚拟内存的基石
“段机制只是过渡,分页才是现代操作系统的真正护城河。 今天,我们亲手开启 x86 的分页功能,让内核拥有虚拟内存!”
在上一篇中,我们深入理解了 保护模式与段式内存管理 ,并亲手实现了从实模式到保护模式的切换。 但你可能已经听说: 现代操作系统几乎不用段机制 ,而是依赖 分页(Paging) 实现内存隔离与虚拟化。
为什么? 因为分页提供了更灵活、更高效的内存管理方式: ✅ 支持非连续物理内存映射为连续虚拟地址 ✅ 实现进程间内存隔离(安全!) ✅ 支持按需换页、共享内存、写时复制等高级特性
今天,我们就来 在 32 位 x86 保护模式下,启动最基础的分页机制 ,为后续的内核开发打下坚实基础。
🧱 一、为什么需要分页?段机制不够吗?
段机制虽然能实现内存保护和特权级,但它存在致命缺陷:
| 问题 | 说明 |
|---|---|
| 段必须连续 | 一个段在物理内存中必须是连续的,无法利用碎片内存 |
| 段大小固定 | 段界限一旦设定,难以动态扩展 |
| 切换开销大 | 切换任务需切换整个段描述符表 |
| 硬件支持弱 | 现代 CPU 优化重点在分页,而非分段 |
而 分页机制 将内存划分为固定大小的 页(Page) (x86 中通常为 4KB),通过 页表(Page Table) 将 虚拟地址 → 物理地址 映射,完美解决上述问题。
💡 Linux、Windows 等现代 OS 都采用“平坦段模型 + 分页” : 所有段都设为 0x00000000 ~ 0xFFFFFFFF , 段机制“退居二线” , 分页扛起内存管理大旗 。
🗺️ 二、x86 分页机制原理(32 位)
x86 32 位分页采用 两级页表结构 :
- 页目录(Page Directory)
:1 个,1024 项,每项 4 字节 →
4KB -
页表(Page Table)
:最多 1024 个,每个 1024 项 →
每个 4KB
🔍 虚拟地址如何翻译?
32 位虚拟地址被划分为三部分:
翻译流程:
- CPU 从
CR3 寄存器
读取
页目录基地址(物理地址) - 用
高 10 位
作为索引,查页目录项 → 得到
页表基地址 - 用
中间 10 位
作为索引,查页表项 → 得到
物理页帧地址 - 拼接
低 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 并开启分页
🌟 开启分页后,所有内存访问都经过地址翻译!
🛠️ 五、完整启动流程(保护模式 + 分页)
- 实模式
→
保护模式
(设置 GDT,加载 CR0.PE) -
设置栈
,调用 C 代码
-
在 C 中初始化页目录/页表
(恒等映射)
- 加载 CR3
-
设置 CR0.PG = 1
→
分页开启!
此时,你的内核已运行在 带虚拟内存的保护模式 下!
⚠️ 六、常见陷阱与注意事项
- 地址混淆:虚拟 vs 物理
页表/目录中存储的
必须是物理地址
若内核链接在高地址(如
0xC0000000
),需手动转换 -
对齐要求
页目录、页表
必须 4KB 对齐
使用
attribute((aligned(4096)))
或链接脚本控制 -
TLB 缓冲
修改页表后,需
刷新 TLB
(如
invlpg
指令,或远跳转)
否则 CPU 可能使用旧映射 -
栈位置
确保栈所在区域已被映射!否则开启分页后立即崩溃
🌱 七、下一步:更高级的内存布局
恒等映射只是起点。接下来你可以:
| 目标 | 说明 |
|---|---|
| 高半内核映射 | 将内核链接到 0xC0000000 以上,用户空间留低地址 |
| 动态页表管理 | 实现 map_page() / unmap_page() |
| 物理内存管理器 | 跟踪空闲物理页(Bitmap / Buddy System) |
| 缺页中断处理 | 实现按需分配、换页等高级功能 |
📚 这些将是后续 kmalloc 、进程管理、文件系统的基础!
💬 写在最后
分页机制看似复杂, 但它正是现代操作系统 安全、稳定、高效 的基石。
今天你开启的不只是一个 CPU 特性, 而是 虚拟内存时代的大门 。
🌟 从恒等映射开始,你终将构建属于自己的地址空间。
📬 动手挑战 : 尝试在你的内核中开启分页,并打印一个通过虚拟地址访问的变量地址。 欢迎在评论区分享你的实现或遇到的“玄学崩溃”!
👇 下一篇你想看: 物理内存管理器(Bitmap 实现) ,还是 中断与 IDT 实战 ?
#操作系统 #内核开发 #x86 #分页 #虚拟内存 #Paging #内存管理 #从零开始
📢 彩蛋 :关注后回复关键词 “paging” ,获取:
完整分页初始化 C 代码模板
恒等映射与高半内核映射对比图
Intel 手册分页章节精要(中文)
评论