从零写 OS 内核-第十九篇:用户与内核地址空间 —— 构建安全的虚拟内存边界
“所有进程共享内核代码,但绝不能互相窥探;用户程序自由驰骋,却无法越界半步。 今天,我们设计高半内核映射,实现用户与内核地址空间的完美隔离与高效共享!”
在前面的篇章中,我们实现了分页、多进程、多核,但所有进程的页目录都 完全独立 —— 包括内核代码、数据、页表本身!这导致:
每次进程切换都要刷新 TLB
(性能极差)
无法高效共享内核资源
内核无法直接访问用户内存
真正的操作系统,必须采用 高半内核(Higher Half Kernel) 设计: ✅ 低地址空间(0x00000000 – 0xBFFFFFFF) :用户空间(每个进程独立) ✅ 高地址空间(0xC0000000 – 0xFFFFFFFF) :内核空间(所有进程共享)
今天,我们就来重构内存布局,实现 安全又高效 的地址空间管理!
🏗️ 一、为什么需要高半内核?
传统独立页目录的问题:
| 问题 | 后果 |
|---|---|
| TLB 频繁刷新 | 每次进程切换,所有 TLB 条目失效 → 性能下降 30%+ |
| 内核无法访问用户内存 | 系统调用需复杂地址转换 |
| 内核内存浪费 | 每个进程都映射一份内核代码(4GB × N 进程) |
高半内核的优势:
TLB 友好
:内核映射不变,切换进程时 TLB 无需刷新
高效访问
:内核可直接通过高地址访问用户内存(通过临时映射)
节省内存
:内核代码/数据只映射一次
💡 Linux、Windows、macOS 全部采用高半内核设计 !
🗺️ 二、地址空间布局设计
32 位 x86 经典布局(4GB 虚拟地址):
关键常量:
🔑 内核直接映射: virtual_addr = physical_addr + KERNEL_VIRTUAL_BASE
🔧 三、重构页目录:共享内核映射
- 创建内核页目录模板
🌟 PAGE_GLOBAL 标志 :此页在进程切换时 不刷新 TLB (性能关键!)
- 为每个进程创建页目录
✅ 每个进程页目录的高 256 项(768-1023)与内核页目录相同 !
🔄 四、进程切换优化:无需刷新 TLB
传统切换(低效):
高半内核切换(高效):
📊 性能提升 :TLB 未命中率降低 50%+,尤其在频繁系统调用时!
📞 五、系统调用:安全访问用户内存
内核现在运行在高地址(0xC0000000+),但用户指针是低地址(0x00000000+)。 如何安全访问?
- 验证用户指针
-
临时映射(推荐)
使用
固定内核虚拟地址窗口
(如 0xFFC00000)临时映射用户物理页
避免直接使用用户虚拟地址(可能触发 Page Fault) -
系统调用中的使用
⚠️ 绝对不要直接解引用用户指针 !可能触发 Page Fault 或访问非法地址。
🧪 六、测试:地址空间隔离验证
用户程序 1:
用户程序 2:
内核行为:
test1
触发
Page Fault
→ 内核发送
SIGSEGV
→ 进程终止
test2
正常执行
✅ 用户无法访问内核空间,隔离成功!
⚠️ 七、高级话题:递归页表与页表自映射
问题:内核如何操作任意进程的页表?
页表本身也在虚拟内存中
需要一种方式通过虚拟地址访问页表
解决方案: 递归页表(Recursive Page Table)
将页目录的最后一项(PDE 1023)指向
页目录自身
形成虚拟地址 → 页目录 → 页表 → 页 的映射
虚拟地址布局:
代码:
💡 递归页表是 Linux 内核操作页表的核心机制 !
💬 写在最后
用户与内核地址空间的设计, 是操作系统 安全与性能的平衡艺术 。 高半内核不仅提升了性能, 更构建了坚不可摧的内存隔离墙。
今天你设置的 0xC0000000 , 正是无数操作系统内核的“安全港湾”。
🌟 最好的隔离,是让用户感觉不到隔离的存在。
📬 动手挑战 : 修改你的内核,将内核映射到 0xC0000000 ,并验证用户程序无法访问高地址。 欢迎在评论区分享你的地址空间布局图!
👇 下一篇你想看: 进程间通信(管道/Pipe) ,还是 信号(Signal)机制 ?
#操作系统 #内核开发 #虚拟内存 #高半内核 #地址空间 #内存隔离 #从零开始
评论