1. print a page table
void _vmprint(pagetable_t pagetable, int level){ //传递入页表
for(int i=0; i<512; i++){ //遍历512个页表项
pte_t pte = pagetable[i];
if(pte & PTE_V){ //有效的页表项,也就是直接指向物理项
for(int j =0; j<level; j++){
if(j) printf(" ");
printf("..");
}
uint64 child = PTE2PA(pte); //转换为实际的物理地址
printf("%d: pte %p pa %p\n", i, pte, child);
if((pte & (PTE_R|PTE_W|PTE_X)) == 0){ //指向下一级
_vmprint((pagetable_t)child, level+1);
}
}
}
}
void vmprint(pagetable_t pagetable){
printf("page table %p\n", pagetable); //输出一个指针,即pagetable的地址
_vmprint(pagetable, 1);
}
其中:
在多级页表系统中(以三级页表为例),有效的页表项(即PTE_V
位被设置的页表项 pte & PTE_V)可以出现在任何级别的页表中。这些有效的页表项的作用可以分为两类:
-
指向下一级页表的页表项:这些页表项存在于第一级和第二级页表中,它们的作用是指向下一级的页表。这些页表项通常不包含访问权限位(
PTE_R|PTE_W|PTE_X
),因为它们不直接映射到物理内存页,而是指向另一个页表。 -
指向物理页的页表项:这些页表项通常存在于最后一级的页表中,它们直接指向物理内存的页。这些页表项包含访问权限位(
PTE_R|PTE_W|PTE_X
),这些权限位指定了对应的物理页可以被如何访问(读、写、执行等)。
因此,当检查一个页表项是否包含访问权限位(即pte & (PTE_R|PTE_W|PTE_X) != 0
)时,我们实际上是在查看这个页表项是否是最后一级的页表中的项,且它直接指向一个物理页,并具有相应的访问权限。这样的页表项只存在于多级页表的最后一级中,因为只有这些页表项是用于定义虚拟地址到物理地址的直接映射关系的。
2. A kernel page table per process
目标:未修改xv6之前,用户通过系统调用进入内核,这时如果传入用户的指针,会导致因为找不到相应指向的物理地址而失败,因为在虚拟地址到物理地址的转换中,我们使用的是内核kernel pagetable而不是用户自定义的usr pagetable,所以我们要做的是构造一个usr-kernel-pagetable在用户进入内核时使用,将我们的虚拟地址转换为正确的物理地址。也就是说,为每个进程新增一个内核态的页表,然后在该进程进入到内核态时,不使用公用的内核态页表,而是使用进程的内核态页表,这样就可以实现在内核态直接使用虚拟地址的功能了。
p->kstack
和p->kpagetable
是相关联的,它们共同支持每个进程在内核模式下运行时的内存管理。先来分别解释这两个字段的作用:
-
**
p->kstack
**:这是每个进程的内核栈的虚拟地址。内核栈是操作系统为进程在内核模式下运行时使用的栈空间。每当进程从用户模式切换到内核模式(如系统调用或处理中断时),内核会使用这个栈来保存进程的状态和局部变量等。因此,每个进程都需要拥有自己的内核栈,以保证在内核模式下的操作互不干扰。 -
**
p->kpagetable
**:这是每个进程的内核态页表的地址。在提供的代码片段中,p->kpagetable
是一个实验性质的设计,允许每个进程在内核态有自己的虚拟地址映射。这意味着每个进程可以有不同的内核虚拟地址空间配置,尽管这在大多数操作系统设计中并不常见。通常,所有进程共享相同的内核地址空间映射,只有用户空间的映射是进程独立的。
这两者的关系在于,每个进程的内核栈虚拟地址(p->kstack
)需要通过页表(p->kpagetable
)来映射到物理内存。这样,当进程运行在内核模式下,并使用其内核栈时,CPU可以通过p->kpagetable
提供的映射找到对应的物理
思路:首先需要在vm.c中为每个进程创建自己的内核页表,仿照kvminit及kvmmap仿写ukvminit, ukvmmap, 然后再在proc.c中的allocproc(新进程的分配,查找进程表中一个未使用的条目并初始化它,为新的进程准备必要的结构)调用ukvmmit这个函数,给每个进程创建页表(这个kpagetable要放在proc 结构体中)。再接着,需要在该内核态页表中初始化内核栈(在一个典型的操作系统中,每个进程的页表中都有一个专门的区域用于映射其内核栈,这个内核栈旨在进程处于内核模式时被使用)。现在已经为每个进程都分配好了内核页表。接下来就是有两步,第一步就是要在进程调度的时候,切换内核页,也就是进程和进程切换的过程中,要切换SATP寄存器的代码。在kernel/proc.c 的scheduler()调度器(选择一个可运行RUNNABLE的进程并将处理器的控制权转交给它,让他运行)中,进行SATP寄存器内容的切换,并且在调度以后切换回来。同时要在freeproc中释放资源,也就是释放内核栈以及内核页表(单独一个函数调用,因为要遍历所有pte)
最后修改kvmpa,使得将虚拟地址转换为相应的物理地址始,用的不是内核页表,而是每个进程自己独有的内核态页表。
内核文件解读:
kernel/proc.c
文件: 包含了与进程管理相关的核心功能,比如进程的创建、调度、退出和切换等
kernel/vm.c文件(大多数用于操作地址空间和页表的xv6代码)解读:
kvminit(): 初始化内核的页表。它首先分配一个页表并将其清零,然后通过调用kvmmap
函数将不同硬件设备的物理地址映射到虚拟地址空间,包括UART(通用异步接收/发送器)、VIRTIO(虚拟I/O设备)、CLINT(核间中断控制器)、PLIC(平台级中断控制器)、内核代码段和数据段。因此,内核页表是包括硬件设备的映射信息的
kvminithart(): 设置SATP初始化每个硬件线程的内核页表。【SATP寄存器:存放的是当前页表的物理基地址(PPN)。当CPU处于内核态时,SATP
寄存器通常包含内核页表的物理基地址。当CPU处于用户态时,SATP
寄存器则包含当前用户进程页表的物理基地址。在操作系统进上下文切换时——即从一个进程切换到另一个进程时,它会更新 SATP
寄存器来指向即将运行的进程的页表。这样,当CPU执行新的进程时,它会使用关联的页表来进行地址转换,确保每个进程的虚拟地址空间是独立的。因此,SATP
的值会根据当前执行上下文的需求而变化。对于需要在内核态运行的代码,操作系统会确保 SATP
寄存器指向内核的页表;而对于用户态的应用程序,SATP
寄存器则指向该应用程序专属的页表。】
kvmmap(): 用于在内核页表中添加一个新的映射。它调用mappages
函数来实现这一映射。也就是:将范围虚拟地址到同等范围物理地址的映射装载到同一个页表中
walk(): 在一个页表中遍历来定位或创建页表项PTE的函数(在给定的页表和虚拟地址的前提下,遍历页表来定位对应的页表项PTE)。
3. copyin / copyinstr 修改内核
使得每个进程的内核页表中添加用户映射,也就是将用户进程的页表的所有内容都复制到内核页表中。
copyout()
, copyin()
, copyinstr()
:这些函数用于在内核空间和用户空间之间复制数据。copyout
将数据从内核复制到用户空间,copyin
从用户空间复制到内核,而copyinstr
用于复制一个以null结尾的字符串。