文章目录
- 前言
- 页表
- 实际情况
前言
一个本硕双非的小菜鸡,备战24年秋招。打算尝试6.S081,将它的Lab逐一实现,并记录期间心酸历程。
代码下载
官方网站:6.S081官方网站
页表
每个进程都有一个单独的页表,当xv6在进程之间切换时,也会更改页表。如图2.3所示,一个进程的用户内存从虚拟地址零开始,可以增长到MAXVA (kernel/riscv.h:348),原则上允许一个进程内存寻址空间为256G。
当进程向xv6请求更多的用户内存时,xv6首先使用kalloc来分配物理页面。然后,它将PTE添加到进程的页表中,指向新的物理页面。Xv6在这些PTE中设置PTE_W、PTE_X、PTE_R、PTE_U和PTE_V标志。大多数进程不使用整个用户地址空间;xv6在未使用的PTE中留空PTE_V。
我们在这里看到了一些使用页表的很好的例子。首先,不同进程的页表将用户地址转换为物理内存的不同页面,这样每个进程都拥有私有内存。第二,每个进程看到的自己的内存空间都是以0地址起始的连续虚拟地址,而进程的物理内存可以是非连续的。第三,内核在用户地址空间的顶部映射一个带有蹦床(trampoline)代码的页面,这样在所有地址空间都可以看到一个单独的物理内存页面。
图3.4更详细地显示了xv6中执行态进程的用户内存布局。栈是单独一个页面,显示的是由exec创建后的初始内容。包含命令行参数的字符串以及指向它们的指针数组位于栈的最顶部。再往下是允许程序在main处开始启动的值(即main的地址、argc、argv),这些值产生的效果就像刚刚调用了main(argc, argv)一样。
为了检测用户栈是否溢出了所分配栈内存,xv6在栈正下方放置了一个无效的保护页(guard page)。如果用户栈溢出并且进程试图使用栈下方的地址,那么由于映射无效(PTE_V为0)硬件将生成一个页面故障异常。当用户栈溢出时,实际的操作系统可能会自动为其分配更多内存。
XV6使用进程的页表,不仅是告诉硬件如何映射用户虚拟地址,也是明晰哪一个物理页面已经被分配给该进程的唯一记录。这就是为什么释放用户内存(在uvmunmap中)需要检查用户页表的原因。
实际情况
像大多数操作系统一样,xv6使用分页硬件进行内存保护和映射。大多数操作系统通过结合分页和页面故障异常使用分页,比xv6复杂得多,我们将在第4章讨论这一点。
内核通过使用虚拟地址和物理地址之间的直接映射,以及假设在地址0x8000000处有物理RAM (内核期望加载的位置) ,Xv6得到了简化。这在QEMU中很有效,但在实际硬件上却是个坏主意;实际硬件将RAM和设备置于不可预测的物理地址,因此(例如)在xv6期望能够存储内核的0x8000000地址处可能没有RAM。更严肃的内核设计利用页表将任意硬件物理内存布局转换为可预测的内核虚拟地址布局。
RISC-V支持物理地址级别的保护,但xv6没有使用这个特性。
在有大量内存的机器上,使用RISC-V对“超级页面”的支持可能很有意义。而当物理内存较小时,小页面更有用,这样可以以精细的粒度向磁盘分配和输出页面。例如,如果一个程序只使用8KB内存,给它一个4MB的物理内存超级页面是浪费。在有大量内存的机器上,较大的页面是有意义的,并且可以减少页表操作的开销。
xv6内核缺少一个类似malloc可以为小对象提供内存的分配器,这使得内核无法使用需要动态分配的复杂数据结构。
内存分配是一个长期的热门话题,基本问题是有效使用有限的内存并为将来的未知请求做好准备。今天,人们更关心速度而不是空间效率。此外,一个更复杂的内核可能会分配许多不同大小的小块,而不是(如xv6中)只有4096字节的块;一个真正的内核分配器需要处理小分配和大分配。