← 返回 操作系统

操作系统

第四章

第四章的主线是地址空间。前三章里,用户程序已经可以被加载、触发系统调用、参与任务切换,但它们还没有真正拥有彼此隔离的虚拟内存世界。ch4 引入 RISC-V Sv39 页表机制,让每个用户进程都有自己的 AddressSpace:用户程序看到虚拟地址,页表负责把虚拟页号 VPN 映射到真实物理页号 PPN,CPU 的 MMU 再根据 satp 指向的根页表自动完成地址翻译。

本章要掌握的核心关系可以这样串起来:ch2 解决用户态进入内核态,ch3 解决多个任务之间的调度与切换,ch4 则解决每个任务如何拥有自己的虚拟内存空间。切换进程时,不只是寄存器上下文在变化,satp 对应的页表也在变化,因此同一个虚拟地址在不同进程里可以指向不同的物理页。

实验实现上,Process::new() 是把 ELF 用户程序变成可运行进程的关键入口。它需要读取 ELF 入口地址和 Program Header,遍历 PT_LOAD 段,根据 R/W/X/U/V 权限建立页表映射,把代码段、数据段复制进分配好的物理页,再映射用户栈,构造 ForeignContext,并保存该进程对应的 satp。换句话说,ELF 只是程序文件格式,Process::new() 才真正为它搭出运行时的虚拟地址空间。

ch4 最容易踩坑、也最重要的点是用户指针不能在内核里直接解引用。比如 write(fd, buf, count) 中的 buf 是用户虚拟地址,内核必须先找到当前进程的 address_space,通过 translate 检查映射和权限,得到内核可访问的位置后才能读写数据。因此 write/read/clock_gettime/trace,以及后续图形帧提交,都要围绕地址翻译来改造。

mmap、munmap、sbrk 这几个系统调用本质上都在修改当前进程的地址空间。mmap 是在一段虚拟地址范围内建立映射并设置权限,munmap 是撤销映射,sbrk 是扩展或收缩用户堆。它们不是简单地申请一个数组,而是在改变页表,所以访问越界、访问未映射区域、权限不匹配时都应该体现为 PageFault 或相应错误。

这次 ch4 扩展还做了用户态 Tetris 俄罗斯方块实验。用户态 ch4_tetris 负责生成方块、移动旋转、碰撞检测、自动下落、硬降、消行、计分和速度递增;内核态负责把用户提交的 TetrisFrame 翻译并渲染到 VirtIO-GPU,同时通过 VirtIO-keyboard 把 a/d/w/s/space/q 等输入暴露给用户程序。这个设计和 ch3 snake 类似,但更强调 ch4 的地址空间安全访问。

Tetris 的图形输出走 write(fd=3, frame_bytes)。用户程序不直接操作 GPU,而是把 10x20 棋盘、分数、等级等数据打包成一帧;内核收到 fd=3 后,先翻译用户虚拟地址,再检查 magic 和长度,随后初始化或复用 VirtIO-GPU,把棋盘格子画成彩色块并 flush framebuffer。这个过程很好地说明:用户态表达需求,内核检查和翻译用户数据,最后由内核代表用户访问硬件。

本章调试里最关键的 bug 是 QEMU GTK 窗口能打开,但没有游戏画面,随后定位到访问 0x10001000 触发 LoadPageFault。根因是 ch4 开启页表后,内核访问设备 MMIO 地址也必须被映射;之前内核地址空间没有覆盖 VirtIO-GPU 和 VirtIO-keyboard 的设备区。修复方式是在 kernel_space() 中映射 0x1000_0000..0x1000_3000,覆盖 UART、VirtIO-GPU、VirtIO-keyboard。修复后日志出现 virtio-gpu ready 和 virtio-keyboard ready,说明设备链路打通。

如果用一句话总结第四章:它让内核从“能运行和调度用户程序”进一步走向“能为每个用户程序建立独立虚拟内存世界,并在系统调用、堆管理、mmap/munmap、图形设备和键盘输入中正确处理地址翻译”的阶段。Tetris 实验也把这个抽象概念落到了一个能看见、能操作、能调试的完整小应用上。

GitHub 仓库:flyyansii/Tg-rCore-Tutorial-2026S

本地提交:9374824 add ch4 learning notes

ch4 Tetris 分支:ch4-tetris-demo

源码入口:tg-rcore-tutorial-ch4

用户态 Tetris:ch4_tetris.rs

ch4 文档目录:doc/ch4