xv6-k210/doc/内核原理-内存管理.md

71 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 页表
页表是操作系统用来为每个进程提供专有地址空间和内存的一种机制。页表不仅决定了虚拟地址的真实含义物理地址还决定了一个进程能够访问内存的哪些地方。页表机制的存在使得xv6能够隔离不同进程的地址空间以及将一块物理内存复用为多个虚拟地址空间。同时页表所提供的这种映射机制也使得xv6的内存管理十分巧妙。比如将同一片物理内存映射到多个虚拟地址空间、不映射内核和用户进程的堆栈等等。本章接下来的部分将解释RISC-V硬件所提供的页表机制以及xv6是如何实现页表机制的。
## 分页硬件
需要提到的是RISC-V提供了操纵虚拟地址的指令内核态和用户态都有。机器的RAM或者说物理内存是通过物理地址来索引的。RISC-V的与页表相关的硬件通过将虚拟地址映射回物理地址起到了虚拟地址和物理地址的桥梁作用。
xv6跑在Sv39 RISC-V上这意味着64位的虚拟地址实际上只有低39位被用到了而高25位并没有被使用。根据Sv39规范一个RISC-V页表在逻辑上拥有 $2^{27}=134217728$ 个页表项page table entries, PTEs。每个PTE包含一个44位的物理页号physical page number, PPN和一些标志位。分页硬件通过被用到的那39位的高27位在页表中索引PTE而剩下的那低12位则为页内偏移offset。由于Sv39的物理地址为56位且这56位中的低12直接由虚拟地址中的低12位页内偏移得到则物理页号有 $56-12=44$ 位这44位物理页号PPN由被索引的PTE所指向的物理页号PPN得到。一个页表可以简单理解为一个存放了很多页表项PTEs的数组下图描述了这种逻辑关系。页表为操作系统提供了虚实地址转换的控制这种控制以 $4096(2^{12})$ 字节的对齐块为粒度。这样的块就是一个页面。
![图1](https://github.com/mit-pdos/xv6-riscv-book/raw/xv6-riscv/fig/riscv_address.svg)
在Sv39标准的RISC-V中不使用虚拟地址的高25位进行转换。将来RISC-V可能会使用这些位来定义更多级别的转换。物理地址也有增长的空间。在PTE格式中物理页号PPN字段可以再增长10位。
如图2所示实际的虚实地址转化分为三个步骤。页表以高度为3的树的形式存储在物理内存中。根节点是一个4096字节的页表页其中包含512个页表项PTE这些页表项PTE包含树的下一级页表页的物理地址。这些页面中的每一页面都包含512个页表项PTE这些页表项PTE是树的叶子节点。分页硬件使用这27位中的前9位在根页面表页面中选择一个页表项PTE中间9位在树的下一级的页表页中选择一个页表项PTE底部9位选择最后一个页表项PTE。综上所述RISC-V采取三级页表机制。
如果不存在转换地址所需的三个PTE中的任何一个则分页硬件会引发页面错误异常page-fault exception)并将这个异常交给内核来处理请参见第4章。一种比较常见的情况是在页表页中大量虚拟地址没有映射这种三层树状页表机制将忽略整个页表页。
每个页表项PTE都包含标志位这些标志位的存在使得分页硬件知道如何使用这些被关联的虚拟地址。标志位PTE\_VVALID指示该页表项PTE是否有效如果该位被置0则对该页面的引用会引发异常即不允许引用。PTE\_RREAD控制是否允许指令读取该页面。PTE\_WWRITE控制是否允许指令写入该页面。PTE\_XEXECUTE控制CPU是否可以将该页面的内容解释为指令并执行它们。PTE\_UUSER控制是否允许用户模式下的指令访问页面如果该位被置0 ,则只能在超级用户模式下使用该页面项所指向的页面。下图展示了它们如何工作。标志和所有其他与页面硬件相关的结构都在[(kernel / riscv.h)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/riscv.h)中定义。
![图2](https://github.com/mit-pdos/xv6-riscv-book/raw/xv6-riscv/fig/riscv_pagetable.svg)
为了告诉硬件使用页表映射机制内核必须将根页表页的物理地址写入satp寄存器。每个CPU都有自己的satp 。CPU将使用其自己的satp指向的页表来转换后续指令生成的所有地址。由于每个CPU都有自己的satp因而不同的CPU可以运行不同的进程每个进程都有一个由其自己的页表描述的专用地址空间。
关于术语的一些注释物理内存是指DRAM中的存储单元。物理内存中每一个字节都有一个地址通常情况下按字节编址称为物理地址。指令仅使用虚拟地址分页硬件负责将其转换为物理地址然后将其发送至DRAM硬件以读取或写入额你存。与物理内存和虚拟地址不同虚拟内存不是物理对象而是内核提供的抽象和机制的集合用于管理物理内存和虚拟地址。
## 内核地址空间
xv6为每个进程维护一个页表用于描述每个进程的用户地址空间。除此之外还有单独一个页表用于描述内核的地址空间。内核配置其地址空间的布局以使其能够以可预测的虚拟地址访问物理内存和各种硬件资源。下图显示了此布局如何将内核虚拟地址映射到物理地址。文件[(kernel / memlayout.h)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/memlayout.h)声明了xv6内核内存布局的常量。
![图3](https://github.com/mit-pdos/xv6-riscv-book/raw/xv6-riscv/fig/xv6_layout.svg)
QEMU可以模拟一个包含RAM物理内存的机器该RAM从物理地址0x80000000开始一直延伸到至少0x86400000xv6将其称为PHYSTOP。QEMU模拟还包括I/O设备例如磁盘接口。QEMU以物理地址空间中0x80000000以下的内存映射控制寄存器的形式为软件提供设备接口。内核可以通过读/写这些特殊的物理地址与设备进行交互这样的读写实际上是在与设备硬件进行通信而不是RAM。第4章介绍了xv6如何与设备交互。
内核使用“直接映射”来获取RAM和内存映射的设备寄存器。也就是说将资源映射到与物理地址相等的虚拟地址。例如内核本身在虚拟地址空间和物理内存中都位于KERNBASE = 0x80000000 。直接映射简化了读取或写入物理内存的内核代码。例如当fork为子进程分配用户内存时分配器返回该内存的物理地址fork将父进程的用户内存复制到子进程时fork直接将该地址用作虚拟地址。
有几个内核虚拟地址不是直接映射的:
- trampoline页面。它映射在虚拟地址空间的顶部。用户页表具有与此相同的映射。注意到一个物理页面包括trampoline代码在内核的虚拟地址空间中映射了两次一次在虚拟地址空间的顶部一次是直接映射。
- 内核堆栈页面。每个进程都有自己的内核堆栈该堆栈被映射到较高的位置因此xv6在其下方可以留下未映射的保护页。保护页的PTE无效PTE\_V置0因此如果内核溢出内核堆栈则很可能会导致异常并且内核会出现紧急情况。如果没有保护页溢出的堆栈将覆盖其他内核内存从而导致错误的操作系统很可能会崩溃。
内核通过高内存映射使用其堆栈时,内核也可以通过直接映射的地址访问它们。替代设计可能只具有直接映射,并在直接映射的地址处使用堆栈。然而,在这种安排下,提供保护页将涉及取消映射虚拟地址,否则这些虚拟地址将引用物理内存,因此将很难使用。
内核通过置标志位PTE\_RREAD和PTE\_\_XEXECUTE映射trampoline的页面和内核文本。内核从这些页面读取并执行指令。内核通过置标志位PTE\_RRREAD和PTE\_WWRITE映射其他页面以便它可以在那些页面中读取和写入内存。保护页面的映射无效。
## 物理内存分配
内核必须在运行时为页表,用户内存,内核堆栈和管道缓冲区分配和释放物理内存。
xv6使用内核末尾与PHYSTOP之间的物理内存进行运行时分配。它以一个页面4096个字节为粒度进行分配和释放。它通过一个链表来记录那些空闲的页面。分配即将被分配的页面从链表中删除。释放即将被释放的页面添加到列表中。
## 进程地址空间
每个进程都有一个单独的页表并且xv6在进程之间切换时它也会切换对应的页表。如下图所示进程的用户内存从虚拟地址0开始可以增长到MAXVA[(kernel / riscv.h348)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/riscv.h#L348)从而允许进程在原则上寻址256GB的内存。
![图4](https://github.com/mit-pdos/xv6-riscv-book/raw/xv6-riscv/fig/processlayout.svg)
当进程向xv6请求更多的用户内存时xv6首先使用kalloc分配物理页面。然后它将页表项PTE添加到流程的页表中该页表指向新的物理页。xv6在这些页表项PTE中设置PTE\_W PTE\_X PTE\_R PTE\_U和PTE\_V标志位。大多数进程不会使用整个用户地址空间。对于未使用的页表项PTExv6将PTE\_VVALID标志位置0。
我们在这里看到一些使用页表的带来的好处。首先不同进程的页表将用户地址转换为物理内存的不同页面以便每个进程都有专用的用户内存。其次每个进程将其内存视为具有从零开始的连续虚拟地址而进程的实际物理内存可能是非连续的。第三内核在用户地址空间的顶部映射一个带有trampoline代码的页面因此有一页物理内存会出现在在所有的用户地址空间。
图3.4更详细地显示了xv6中正在执行的进程的用户内存的布局。堆栈是单独的一个页面包含了由exec创建时的初始内容。包含命令行参数以及它们的指针数组的字符串位于堆栈的最顶端。在该值之下是允许程序从main开始的值就像函数mainargcargv刚刚被调用一样。
为了检测用户堆栈是否溢出已分配的堆栈内存xv6在堆栈的正下方放置了一个无效的保护页。如果用户堆栈溢出并且该进程尝试使用堆栈下面的地址则硬件将引发页面错误异常page fault exception),因为该映射无效。实际的操作系统可能会在用户堆栈溢出时自动为其分配更多的内存。
## 实际情况
与大多数操作系统一样xv6使用分页硬件来保护和映射内存。大多数操作系统在分页上有更精妙的用法比如通过结合分页和页面错误异常xv6并没有这样做。
xv6的内核使用虚拟地址和物理地址之间的直接映射以及它假定在地址0x8000000处存在物理RAM内核期望加载的位置这些使得xv6得以简化。这适用于QEMU但是在真正的硬件上这样做并不好。实际的硬件将RAM和设备放置在不可预测的物理地址上因此例如在0x8000000处可能并没有RAM而xv6却希望可以在这里存储内核。更厉害的内核设计充分利用页表使得可以将任意硬件物理内存布局转换为可预测的内核虚拟地址布局。
RISC-V支持物理地址级别的保护但是xv6没有使用这个特性。
在具有大量内存的机器上可以使用RISC-V对“超级页面”的支持。当物理内存较小时小页面是更有意义的它允许以精细的粒度分配分页到磁盘。例如如果一个程序仅使用8KB的内存那么为它提供整个4MB的物理内存超级页面就会造成浪费。而在具有大量RAM的计算机上较大的页面会更有意义这样可以减少页表操作的开销。
xv6内核缺少类似malloc这样的可以为小对象提供内存的分配器这使得内核无法使用需要动态分配的复杂数据结构。
内存分配是一个长期的热门话题其基本问题是如何有效地利用有限的内存以及如何为将来未知的请求做准备。今天人们更关心速度而非空间效率。此外一个更精细的内核可能会分配许多不同大小的小块而不是分配固定大小的块如xv6中是4096个字节。一个真正的内核分配器既要处理大的分配。也要处理小的分配。