Chap3:页表
操作系统通过页表为每个进程提供了自己私有的地址空间和内存。页表决定了内存地址表示什么,也决定了哪部分物理内存可以被访问。它们允许xv6隔离不同进程的地址空间,并将它们复用到一个物理内存中。页表是一种流行的设计,因为它们提供了一定程度的间接性,允许操作系统执行许多技巧。
xv6中有一些小技巧:将同一段内存(trampoline页面)映射到不同的地址空间,并用一个未映射的页来保护内核和用户栈。本章的其余部分解释了RISC-V硬件所提供的页表,以及xv6如何使用它们。
1. 分页硬件
注意:RISC-V指令(包括用户和内核)使用虚拟地址。而机器的RAM,或物理内存使用物理地址进行索引。RISC-V页表硬件通过将每个虚拟地址映射到一个物理地址来连接这两种地址。
xv6运行在Sv39 RISC-V上,这表示只使用了64位虚拟地址中的低39位,而高25位未使用。Sv39 虚拟地址如下:
Sv39支持39位虚拟内存空间。每一页占用4KB内存,页内使用虚拟地址低12位寻址。虚拟地址的高27位划分为三级页号,每一级都有512个可用的页号。
Sv39的物理地址如下:
Sv39的页表项(PTE)如下:
Sv39的页表对应一个内存的物理页(即页表存放在一个物理页中),每一个页表项占用64bit内存,512个页表项 * 64bit正好是4KB。(三级页表就是三个物理页) PTE中存放的是下一级页表或最终物理页的物理地址,将下一级页表或最终物理页的物理地址经过移位操作,并加上相应的属性位即可得到PTE
// kernel/riscv.h
// shift a physical address to the right place for a PTE.
#define PA2PTE(pa) ((((uint64)pa) >> 12) << 10)
#define PTE2PA(pte) (((pte) >> 10) << 12)
#define PTE_FLAGS(pte) ((pte) & 0x3FF)
在这种Sv39配置中,RISC-V页表逻辑上是一个包含个页表项的向量表(因为顶级页表包含512()个页表项,每个页表项对应一个中级页表,每一个中级页表对应一个低级页表,而每种页表都有512个页表项,所以一共有个页表项)。
每个PTE包含一个44位的物理页号(PPN,physical page number)和一些标志位。分页硬件使用39位虚拟地址中的高27位作为索引,在页表中找到对应的PTE,然后用PTE中的44位地址作为高位(物理页号),虚拟地址中的低12位作为低位(页内偏移量),构造出一个56位的物理地址。
上图展现了虚拟地址到物理地址的过程。页表的逻辑视图被描绘为一个PTE构成的向量表(之后的图片给出真实情况)。页表使操作系统能够以4096()字节对齐块的粒度控制虚拟地址到物理地址的转换。这样的块称为页面。
在Sv39 RISC-V中,虚拟地址的高25位不用参与到地址转换中,未来RISC-V可能会使用这些位定义更多层的地址转换。 物理地址也还有增加的空间:PTE格式中的物理页号还有再增长10位的空间。 RISC-V的设计者根据技术预测选择了这些数字。
,对于运行在RISC-V计算机上的应用程序来说,这应该是足够的地址空间。为了满足不久的将来许多的I/O设备和DRAM芯片, 是足够的物理内存空间。如果需要更多,RISC-V设计人员已经用48位虚拟地址定义了Sv48。
如上图,RISC-V CPU通过三个步骤将虚拟地址转换为物理地址。页表以三层树的结构保存在物理内存中。树的根是包含512个PTE的4096字节的页表。每个PTE都包含了下一层页表存放的物理地址。对于树的最后一层,这些页面中的每一页包含512个PTE。分页硬件使用27位的最高9位查找根页表中的PTE(通过这个PTE找到中层页表的位置),中间9位查找中层页表中的PTE(通过这个PTE找到最低层页表的位置),最低9位来查找最后的PTE(通过这个PTE找到最后需要找的物理地址)。 (在Sv48 RISC-V中,页表有4级,虚拟地址的39位到47位索引到顶级)
翻译地址时,如果所需要的三个PTE中有任意一个不存在,分页硬件就会抛出一个缺页异常,让内核去处理这个异常。与第一个图的单级设计相比,第二个图的三级结构允许以一种节省内存的方式记录PTE。如果有虚拟地址没有映射,三级结构可以省略整个页面目录。而大段地虚拟地址没有映射是非常常见的情况。
比如:如果一个应用程序只使用从地址0开始的几个页面,那么顶层页面目录的条目1到511是无效的,内核不必为511个中间页面目录分配页面。此外,内核也不必为这511个中间页面目录的底层页面目录分配页面。所以在这个例子中,三级设计为中间页面目录节省了511页,为底层页面目录节省了 页。
作为执行加载或存储指令的一部分,CPU需要遍历硬件中的三级结构,三级结构的潜在缺点是CPU必须从内存中加载三个PTE来执行 加载/存储 指令中的虚拟地址到物理地址的转换。 为了避免从物理内存加载PTE的消耗,RISC-V CPU将页表条目缓存在翻译后备缓冲区(TLB)中。
每个PTE都包含标志位,用来告诉分页硬件,关联的页怎样使用。
- PTE_V:指示PTE是否存在,如果没有设置它,对这个页的引用会产生异常(即不允许)
- PTE_R:控制是否允许指令读取这个页面
- PTE_W:控制是否允许指令写这个页面
- PTE_X:决定CPU是否可以将页面内容解释为指令并执行它们
- PTE_U:控制是否允许用户模式下的指令访问页面,如果未设置(为0),那么这个PTE只能在管理者模式下使用
这些标志位和所有其他与分页硬件相关的结构体都定义在kernel/riscv.h中。
为了让硬件使用页表,内核必须将根页表页的物理地址写入satp寄存器。每个CPU都有自己的satp。CPU将使用它自己的satp指向的页表来翻译由后续指令产生的所有地址。因为每个CPU都有自己的satp寄存器,所以不同的CPU可以运行不同的进程,每个进程都有一个由自己的页表描述的私有地址空间。 通常,内核将所有的物理内存映射到它的页表中,这样它就可以使用 加载/存储 指令读写物理内存中的任何位置。由于页面目录位于物理内存中,内核可以通过使用标准存储指令在PTE的虚拟地址上写入,来对页面目录中的PTE内容进行编程。
名词解释:
- 物理内存:DRAM中的存储单元。
- 物理地址:每一字节的物理内存都有一个地址,叫物理地址。
- 虚拟内存:虚拟内存不像物理内存和虚拟地址那样,它不是物理实体。它指的是内核提供的用来管理物理内存和虚拟地址的抽象和机制的集合。
指令只使用虚拟地址,分页硬件将其转换为物理地址,然后发给DRAM硬件来读取或写入。
2. 内核地址空间
xv6为每个进程维护一张页表,用来描述进程的用户地址空间。为内核也维护了一张页表,用于描述内核地址空间(所有进程共享这一个描述内核地址空间的页表)。内核配置其地址空间的布局,来为自己提供在已知虚拟地址上访问物理内存和各种硬件资源的便利。下图展示了这个布局如何将内核的虚拟地址映射到物理地址上。kernel/memlayout.h声明了xv6内核布局的各种常量。
QEMU模拟了一台计算机,它的RAM(物理内存)从物理地址0x80000000开始,一直到至少0x86400000才结束,结束点xv6称之为PHYSTOP。 QEMU模拟还包括I/O设备,如磁盘接口。QEMU将设备接口以映射在内存的控制寄存器(memory-mapped control registers)的形式暴露给软件,它们位于0x80000000之下的物理地址空间。内核可以通过读/写这些特殊的物理地址的方式与设备交互;这些读写操作直接与设备硬件通信,而不是RAM。 Chap4 解释了xv6如何与设备交互。
内核使用"直接映射"来获取RAM和映射在内存上的设备寄存器。直接映射就是说,将资源映射到与物理地址相同的虚拟地址上。 比如,内核本身在虚拟地址和物理地址上都位于KERNBASE = 0x80000000。 直接映射简化了读写物理内存的内核代码。 比如,当fork为子进程分配用户内存时,分配器返回内存的物理地址;当fork将父进程的用户内存拷贝给子进程时,直接使用这个地址作为虚拟地址。
有几个内核虚拟地址不是直接映射的:
- trampoline 页。trampoline页被映射在虚拟地址空间的顶部;用户页表也有同样的映射。Chap4 会详细讨论trampoline页。一个物理页面(包含trampoline 代码)在内核的虚拟地址空间中被映射了两次:一次在虚拟地址空间顶部,一次是直接映射。
- 内核栈页。每个进程都有自己的内核堆栈,它被映射在高地址,以便xv6能在它下面放置一个未映射的保护页。保护页的PTE是无效的(即PTE_V未设置),因此当内核栈溢出时,有可能引起异常,内核将报错(the kernel will panic)。没有保护页的话,溢出的栈将覆盖别的内核内存,造成运行错误。相对来说,内核报错是更好的结果。
虽然内核通过高位内存映射来使用堆栈,但是内核页可以通过直接映射的地址来访问它们。另一种设计可能是只有直接映射,并在直接映射的地址使用堆栈。然而,在这种设计中,提供保护页将涉及取消保护页的虚拟地址映射,否则保护页将被映射到物理内存,这使用起来将会很麻烦。
内核给映射trampoline的页和内核代码的页PTE_R和PTE_X权限,因为内核需要读取并运行这些页上的指令。内核对映射的其他页赋予PTE_R和PTE_W权限,从而能够读写这些页上的内存,保护页的映射是无效的。
3. 代码:创建地址空间
xv6中,大部分操作地址空间和页表的代码都位于vm.c(kernel/vm.c)中。其中核心的数据结构是 pagetable_t,它是一个指向RISC-V根页表页的指针(指向页表的物理地址)。一个pagetable_t可能是内核页表,也可能是某个进程页表。
核心函数是walk和mappages。walk按照虚拟地址找到PTE;mappages为新的映射添加PTE。以kvm开头的函数用来操作内核页表;以uvm开头的函数用来操作用户页表;其他的函数两者都可以使用。
copyout函数和copyin函数的参数中都有一个参数代表用户的虚拟地址,copyout函数将数据从内核拷贝到该参数指定的用户虚拟地址,copyin函数将数据从该参数指定的用户虚拟地址拷贝到内核。因为它们需要显示的转换这些虚拟地址为对应的物理地址,所以它们也放在vm.c中。
在boot流程的早期,main调用kvminit(kernel/vm.c)来创建内核页表(使用kvmmake创建内核页表)。这个调用发生在xv6在RISC-V上启用分页之前,所以地址直接指向物理内存。kvmmake首先分配一页物理内存来保存根页表页,然后它调用kvmmap来设置内核需要的翻译。翻译包括内核的指令和数据,一直到PHYSTOP的物理内存,以及实际上是设备的物理内存。proc_mapstacks(kernel/proc.c)为每个进程分配一个内核堆栈。 它调用kvmmap将每个堆栈映射到KSTACK生成的虚拟地址,这就为无效的堆栈保护页留出了空间。
kvmmap(kernel/vm.c)调用mappages向一个页表设置一段(a range)虚拟地址向对应物理地址的映射。它以页大小为间隔,为地址段中每个虚拟地址分别执行此操作。对于每个需要映射的虚拟地址,mappages调用walk来找到对应该地址的PTE位置,然后它初始化这个PTE来存放对应的物理页号、需要的权限(PTE_W、PTE_X、PTE_R)、用PTE_V来标记PTE是否有效。
walk(kernel/vm.c)模仿RISC-V分页硬件为虚拟地址查找PTE的过程。walk每次使用9位,向下查找3级页表。它使用虚拟地址中每层的9位查找下层页表所在的PTE或者最终页所在的PTE。如果PTE无效,那么所需要的页面还没有分配;如果设置了alloc参数,walk将分配一个新的页表页,并将它的物理地址放在PTE中。walk返回最底层树的PTE地址。
上面的代码依赖于直接映射到内核虚拟地址空间的物理内存。比如,当walk向下层页表查找时,它将下层页表的物理地址从当前PTE取出,作为一个虚拟地址来寻找再下一层的PTE(通过当前PTE得到下一级的物理块号,通过9位得到下一级物理块的偏移量,然后找到下一级的PTE)。
main调用kvminithart(kernel/vm.c)来设置内核页表。它先将根页表页的物理地址写入寄存器satp。之后CPU就能使用内核页表翻译地址。因为内核使用恒等映射(identity mapping),下一条指令的虚拟地址将正确地映射到物理地址上。
procinit(kernel/proc.c)被main调用,为每个进程分配内核栈。它将每个栈映射到KSTACK生成的虚拟地址上。KSTACK生成虚拟地址时会预留不可用的栈保护页。kvmmap将KSTACK生成的虚拟地址对应的PTE映射到物理地址中,然后kvminithart将内核页表重载到satp,来告诉硬件新PTE的存在。
每个RISC-V CPU都会在TLB中缓存PTE,当xv6切换页表时,必须告诉CPU把TLB中的缓存失效化。否则,在后面一段时间内,TLB仍然持有老的缓存映射,这个缓存映射可能指向一个在此期间已经分配给另一个进程的物理页面。这样的话,进程可能会修改其他进程的内存。 RISC-V有一个指令sfence.vma用来刷新(flush)当前CPU的TLB。xv6在kvminithart函数中,重新加载satp寄存器后,会执行sfence.vma指令,在trampoline代码中,返回用户空间前,切换用户页表后也会执行这个指令。
为了避免刷新整个TLB,RISC-V CPU可能支持地址空间标识符(ASID)。然后,内核可以只刷新特定地址空间的TLB条目。
4. 物理内存分配
内核必须在运行时为页表、用户内存、内核堆栈和管道缓冲区分配和释放物理内存。
xv6使用内核终点与PHYSTOP之间的物理内存来进行运行时分配。它每次分配和释放的都是一整个4096字节的页。它通过维护一个物理页组成的链表来知道哪个物理页是未分配的(freelist,即这个链表中的节点就是一个物理页)。分配时,从这个链表中删除一个页面;释放时,则将被释放的页加入到链表中。
5. 代码:物理内存分配器
分配器代码位于kalloc.c(kernel/kalloc.c)中。分配器的数据结构是一个由可供分配的空闲物理内存页组成的链表(free list)。每个空闲页的链表元素是一个run结构体。分配器从哪里获得内存来保存数据结构呢?它将每个空闲页面的run结构体存放在页本身,因为空闲页面中没有存放任何其他东西。空闲页链表由一个自旋锁保护。链表与锁包装在一个结构体内,以保证自旋锁保护结构体中的字段。锁的调用有acquire和release,Chap6将详细讨论锁。
main函数调用kinit(kernel/kalloc.c)来初始化分配器。kinit初始化空闲页链表来存放内核末尾与PHYSTOP之间的每一个页。xv6应该通过解析硬件提供的配置信息来确定可用的物理内存大小。xv6假设计算机有128MB RAM。kinit调用freerange来向空闲页链表添加内存,每个页都调用一次kfree函数。PTE只能表示一个以4096字节为界对齐的物理地址(是4096的倍数),所以freerange调用PGROUNDUP来保证它只释放对齐的物理地址。分配器启动时没有内存,这些对kfree的调用给了它一些可管理的内存。
PGROUNDUP和PGROUNDDOWN宏的作用
// kernel/riscv.h
#define PGROUNDUP(sz) (((sz)+PGSIZE-1) & ~(PGSIZE-1))
#define PGROUNDDOWN(a) (((a)) & ~(PGSIZE-1))
PGROUNDUP和 PGROUNDDOWN是将地址四舍五入到 PGSIZE 的倍数的宏,它们通常用于获取页面对齐的地址。PGROUNDUP将地址四舍五入到 PGSIZE 的较高倍数而 PGROUNDDOWN将其四舍五入到 PGSIZE 的较低倍数.
比如,如果 PGROUNDUP在具有 PGSIZE 的系统上调用地址为 620 的 1KB(假设PGSIZE = 1024):
PGROUNDUP(620) ==> ((620 + (1024 -1)) & ~(1023)) ==> 1024
地址 620 被四舍五入为 1024
PGROUNDDOWN(2400) ==> (2400 & ~(1023)) ==> 2048
地址 2400 向下舍入为 2048
分配器有时将地址当成整数,用来做算术运算(例如,在freerange中遍历所有页时),有时将地址作为指针,用来读取和写入内存(例如,操作存储在每个页面中的run结构体)。地址的这种双重用法正是分配器代码中充满了C语言类型转换的主要原因。另一个原因是释放和分配本质上就会改变内存的类型。
kfree(kernel/kalloc.c)函数首先将要释放的内存中的每一个字节设置为1。这将使得释放内存后还继续使用内存的代码(使用空悬引用/指针(dangling references))读到垃圾而不是旧的有效内容;希望这样可以让代码更快出现问题。kfree将释放的页添加到空闲链表中:它将pa指针强制转换为指向结构体run的指针,将旧的空闲页链表的头记录为r->next,然后将空闲页链表设置为r(即将新插入的页面作为空闲页链表的头节点)。 kalloc移除空闲页链表中的第一个元素,然后返回它(即kalloc获取空闲页链表的头节点)。
6. 进程地址空间
每个进程都有一个独立的页表,当xv6在进程间切换时,也会切换页表。
如上图所示,进程的用户内存从虚拟地址0开始,能够增长到MAXVA(kernel/riscv.h),这让一个进程理论上能够寻址256GB的内存(32位系统可以寻址,而Sv39 RISC-V使用了64位地址中的39位)。
当进程向xv6请求更多的用户内存时,xv6使kalloc来分配物理页。然后它在进程页表中添加指向新物理页的PTE。xv6在这些PTE中设置PTE_W、PTE_X、PTE_R、PTE_U、PTE_V标志。大部分进程用不到完整的用户地址空间;在未使用的PTE中,xv6不设置PTE_V(xv6 leaves PTE_V clear in unused PTEs.)。
有一些使用页表的好的例子。
- 不同进程的页表将用户地址转换成不同的物理内存页,所以每个进程都拥有私有的用户内存。
- 每个进程都将其内存视为从0开始的连续的虚拟地址,而进程的物理内存可以是不连续的。
- 内核将包含
trampoline代码的页面映射到用户地址空间的顶部,因此这个物理内存页会出现在所有的地址空间中。
上图详细展示了xv6中的一个正在执行的进程的内存布局。栈是单独的一个页面,展示的是由exec创建的初始化内容。栈最顶部是包含命令行参数的字符串,即一个指向它们的指针(指向字符串)组成的数组。下面是程序从main开始执行时所需要的值,看起来好像main(argc, argv)刚刚被调用。
为了检测用户堆栈溢出分配的堆栈内存,xv6通过不设置PTE_U标志将一个不可访问的保护页放在堆栈的正下方。如果用户堆栈溢出,进程试图使用栈下面的地址,硬件将产生一个页错误异常,因为没有设置PTE_U,所以用户模式下运行的程序无法访问该保护页。真实世界的操作系统可能会在用户堆栈溢出时自动为其分配更多内存。
7. 代码:sbrk
sbrk是进程减少或增加内存使用的系统调用。该系统调用由函数growproc(kernel/proc.c)实现。growproc根据n的正负来调用uvmalloc或uvmdealloc。
uvmalloc(kernel/vm.c)使用kalloc分配物理内存,并使用mappages向用户页表添加PTE。uvmdealloc调用uvmunmap(kernel/vm.c),它使用walk来查找PTE,然后用kfree来释放它们指向的物理内存。
xv6不仅使用进程页表来告诉硬件如何映射用户虚拟地址,还将它作为分配给该进程的物理内存页的唯一记录。这就是为什么释放用户内存时(在uvmunmap中)需要检查用户页表的原因。
8. 代码:exec
exec是用来创建地址空间中用户部分的系统的调用。它从存储在文件系统中的文件初始化地址空间的用户部分。exec(kernel/exec.c)使用namei(kernel/exec.c)打开命名的二进制路径,这将在Chap8中解释。然后读取ELF头。xv6应用程序以广泛使用的ELF格式来描述,ELF格式定义在kernel/elf.h中。一个ELF二进制文件由ELF头,即elfhdr(kernel/elf.h)结构体,以及紧随其后的一系列程序段头构成,程序段头由proghdr结构体表示。每个proghdr描述了应用程序中必须被加载到内存的一段。xv6程序只有一个程序段头,但其他操作系统中可能有单独的指令段和数据段。
第一步是快速检查文件是否包含ELF二进制。ELF二进制文件以4字节的"magic number"开头:0x7F,'E','L'',F',或叫做ELF_MAGIC(kernel/elf.h)。如果ELF头有正确的"magic number",exec就假设这个二进制是符合格式的。
exec使用proc_pagetable(kernel/exec.c)来分配一个新页表,其中没,有任何的用户映射,使用uvmalloc(kernel/exec.c)为每个ELF段分配内存并在页表中建立映射,然后使用loadseg(kernel/exec.c)将程序段加载到内存中。loadseg使用walkaddr来查找所分配内存的物理地址(为ELF段分配的内存),在该地址上写入ELF段的每一页,然后使用readi来从文件中读取。
用exec创建的第一个用户程序/init的程序段头如下所示:
# objdump -p _init
user/_init: file format elf64-littleriscv
Program Header:
LOAD off 0x00000000000000b0 vaddr 0x0000000000000000
paddr 0x0000000000000000 align 2**3
filesz 0x0000000000000840 memsz 0x0000000000000858 flags rwx
STACK off 0x0000000000000000 vaddr 0x0000000000000000
paddr 0x0000000000000000 align 2**4
filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw-
程序段头部的filesz可能小于memsz,这表明它们之间的间隙应该用0填充(对C全局变量来说),而不是从文件中读取。对/init来说,filesz为2112字节,memsz为2136字节,因此uvmalloc分配了足够的物理内存来存放2136字节,但只从/init文件中读取2112字节。
现在exec分配内存并初始化用户堆栈。它只分配一个堆栈页面。exec将参数字符串一次一个地复制到堆栈的顶部,并将它们的指针记录在ustack中。它在将要传递给main的argv列表的末尾放置一个空指针。ustack中的前三个项分别是伪返回程序计数器指针,argc和argv指针。
exec在栈页的下方放置了一个不可访问的页面,所以想要使用一页以上的程序会报错。这个不可访问的页面也使得exec能够处理很大的参数,在这种情况下,exec用来将参数复制到堆栈的copyout(kernel/vm.c)函数会发现目标页无法访问,然后返回-1。
在准备新的内存映像的过程中,如果exec检测到一个错误,比如一个无效程序段,它将跳转到bad标签,释放新映像,然后返回-1。exec必须确认系统调用成功后才能释放旧映像:如果提前释放旧映像,那么旧映像不在了,如果调用失败,就无法向它返回-1。exec唯一出错的可能发生在创建映像的过程中。一旦映像创建完成,exec就可以提交新页表,并释放旧的页表。
exec将ELF文件中的内容加载到内存中ELF文件中指定的地址处。用户或进程可以将他们想要的任何地址放在ELF文件中。因此,exec是有风险的,因为ELF文件中的地址可能无意或有意地指向内核。如果内核对此毫无防备,可能引发严重后果,轻则系统崩溃,重则内核隔离机制被完全破坏(即一个安全漏洞)。为防范这些风险,xv6进行了一系列检查。
比如:
if (ph.vaddr + ph.memsz < ph.vaddr)
检查总和是否溢出了64位整数。(如果ph.vaddr + ph.memsz的结果溢出了64位整数,那么它们的和会是一个较小的值,会小于ph.vaddr,所以如果ph.vaddr + ph.memsz的和小于ph.vaddr说明溢出了64位整数)
危险在于,用户可能使用一个指向用户选择的地址的ph.vaddr和足够大的ph.memsz构造ELF二进制文件,使得它们俩的和溢出64位整数,变为0x1000,这看起来是一个有效的值。在xv6的一个老版本中,用户地址空间也包含内核(但在用户模式下不可读写),用户可以选择以一个指向内核内存的地址,从而将ELF二进制文件中的数据拷贝到内核。在xv6的RISC-V版本中,这种情况不会发生,因为内核有它自己单独的页表;loadseg加载到进程的页表(用户页表)而不是内核页表。
内核开发者很容易忽略一个关键的检查,而现实世界中的内核有很长一段遗漏检查的历史,用户程序可以利用这些漏洞来获得内核特权。xv6可能没有对提供给内核的用户层数据做完整的有效性检查,恶意用户程序可能利用这个漏洞,绕过xv6的隔离机制。
9. Real world
像大多数操作系统一样,xv6使用分页硬件进程内存保护和映射。大部分操作系统使用了远比xv6复杂的分页,将分页和页错误异常结合起来,具体的将在Chap4讨论。
xv基于下面的原因得到简化:
- 内核使用虚拟地址到物理地址的直接映射
- 假设在内核预期加载的地址0x8000000处有物理RAM
这在QEMU上是有效的,但是在真实的硬件上却不是个好的方法。真实硬件将RAM和设备放置在不可预测的物理地址,所以有可能在xv6期望能够存储内核的0x8000000处没有RAM。更多的内核设计利用页表将随机的硬件物理内存布局转化为可预测的内核虚拟地址布局。
RISC-V提供了物理地址级别的保护,但xv6没有使用这个功能。
在具有大量内存的机器上,使用RISC-V提供的"超级页"(super page)功能可能会很有用。当物理内存很小时,小页面(small page)是有意义的,它允许使用细粒度的分配内存,和细粒度的将页面换出到磁盘。 比如,如果一个程序只使用8KB内存,那么给它一个4MB的超级页的物理内存就很浪费。越大的页在拥有大量RAM的机器上越有用,可以减少页表操作上的开销。
xv6内核缺少类似malloc的分配器来为小尺寸对象提供内存,这使得内核无法使用需要动态分配的复杂数据结构。
内存分配是一个经久不衰的主题,基本问题是高效使用有限内存,和为未来的需求做准备。今天,人们更多地考虑速度而不是空间效率。此外,一个更复杂的内核可能会分配许多大小不同的块,而不是(像在xv6中)只有4096字节的块;一个真正的内核分配器既需要处理小内存分配也要处理大内存分配。