实验概要
本次实验将围绕操作系统的地址映射、页表结构、页表项标志位以及页表遍历等关键知识点进行深入实践。在任务1中,我们将通过给进程新增一个usyscall结构体,并将其映射到页表中,以实现加快系统调用的速度。任务2将带领我们了解页表的三级页表结构、页表项、页表项标志位的作用和页表的遍历过程,并实现一个vmprint函数来打印指定进程的页表信息。在任务3中,我们将利用之前实验中学习的知识,完善pgaccess函数,以检测哪些虚拟页面被访问过。通过这三个任务,我们将对操作系统的内存管理有更深入的理解和实践。
任务一
思路
在创建新的进程时,我们分配一个物理页,并将usyscall结构体放入其中。然后,我们在进程的页表中为USYSCALL页建立映射,这样不同的进程就可以通过相同的虚拟地址访问到这个物理页。
尽管不同的进程都使用相同的虚拟地址USYSCALL进行访问,但它们的页表映射关系是不同的。每个进程都有自己的虚拟地址空间,每个进程的页表中的USYSCALL虚拟地址指向各自的物理页,因此它们可以独立地访问自己的数据。
通过这种方式,我们可以在用户空间和内核之间共享只读数据,并且无需进行频繁的上下文切换和页表重置操作,从而提高系统调用的执行效率。
步骤
增加结构体指针
在进程结构体(在proc.h
)中增加usyscall结构体指针,可以模仿trapframe。
创建共享页
在创建进程时,为每个进程分配一个物理页。这个物理页就是共享页,用于存储usyscall结构体。
进程的创建由proc.c
中的进程分配函数 allocproc()
实现,其具体实现为为从进程表中找到一个状态为UNUSED(未使用)的进程,然后初始化一些内核运行所需的状态,并返回该进程的指针。
我们通过模仿trapframe物理页的分配来实现usyscall物理页的分配。
存储共享数据
分配了物理空间后,将当前进程的PID存储给当前进程指向的usyscall结构体。
映射共享页
在进程的页表中建立USYSCALL页的映射,将虚拟地址USYSCALL映射到刚刚分配的物理页上。
在创建用户页表的函数pagetable_t proc_pagetable(struct proc *p)
中,模仿trapframe物理页的映射来实现usyscall物理页的映射。
观察mappages
函数,该函数接受以下参数:
pagetable
:目标页表的指针,将在其中进行页表项的更新。va
:欲映射的虚拟地址的起始地址。size
:映射的内存大小(以字节为单位)。pa
:物理地址的起始地址,表示要映射到虚拟地址的物理内存页面。perm
:权限标志位,用于设置页表项的访问权限。
我们需要给页表项设置正确的访问权限,设置为PTE_R
和PTE_U
。
还需要注意的是,如果映射失败,需要调用 uvmunmap()
函数解除之前的映射,然后调用 uvmfree()
释放之前创建的页表。
完成上面的操作后,用户程序就可以通过访问固定的虚拟地址USYSCALL来获取进程的PID,而无需进行系统调用。通过虚拟地址的翻译,最终就可以找到共享页中存储的数据。
但是为了避免内存泄漏和资源浪费,当进程被销毁时,我们还需要将与USYSCALL相关的页表项清除,以便其他进程可以继续使用这些资源。具体的,分别在proc_freepagetable()
、freeproc()
中解除映射、清除物理页。
任务二
思路
知识学习
在操作系统中,页表是将虚拟内存地址映射到物理内存地址的重要数据结构。在RISC-V架构中,页表是由三级结构组成的,分别是一级页表、二级页表和三级页表。每个页表都是一个512项的数组,每个项占据8字节,即64位大小。页表的每个项叫做页表项(page table entry,PTE),用于描述虚拟页与物理页之间的映射关系。一个PTE如果有效(Valid,V)则表示这个虚拟页可以访问到对应的物理页。
整体思路
实现vmprint()
函数的整体思路如下:
遍历页表,从顶层页表开始逐级访问。可以使用循环或递归的方式进行遍历。
对于每个页表项,判断其有效位(Valid bit)。如果页表项无效,则跳过该项。
打印当前页表项的信息。包括缩进格式、页表项的索引、PTE位、以及从PTE中提取的物理地址。
如果当前页表项指向下一级页表,则递归调用自身,打印下一级页表的内容。
最后,在kernel/defs.h
文件中定义相应函数的原型,以便从exec.c
中调用该函数。
步骤
打印PID=1的进程的页表
依题意添加如下代码。
根据提示,参考freewalk
这段代码是用于递归释放页表的内存空间。在释放页表时,需要确保所有的叶子映射已经移除。
代码首先通过循环遍历一级页表中的所有PTE(共512个)。对于每个PTE,它会检查两个条件来确定下一步的操作:
首先,它使用位运算和条件判断
((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0)
来判断该PTE是否指向了一个更低级别的页表。其中,pte & PTE_V
检查PTE的有效位,如果为1表示有效;(pte & (PTE_R|PTE_W|PTE_X)) == 0
检查PTE的权限位,如果为0表示没有读、写、执行权限。如果两个条件都满足,说明该PTE指向一个更低级别的页表。如果条件1不满足,那么代码会再次检查
pte & PTE_V
,如果为1,则说明该PTE是一个叶子节点(leaf),即指向一个物理页的映射。由于该函数只能用于释放页表,而不能用于释放物理页,因此直接报错panic("freewalk: leaf")
。
对于满足条件1的情况,代码会执行以下操作:
从PTE中获取指向更低级别页表的物理地址,即
uint64 child = PTE2PA(pte)
。对更低级别的页表进行递归调用
freewalk((pagetable_t)child)
,以释放更低级别页表的内存空间。将当前PTE设置为0,表示将其无效化。
最后,代码通过 kfree((void*)pagetable)
来释放当前页表的内存空间。
模仿freewalk,实现vmprint
在freewalk
中,我们了解了如何遍历一个页表,并且知道了在遍历过程中如何针对每个 PTE判断是否存在下一级页表。其中判断是否存在下一级页表过程如下:
首先,检查 PTE 的有效位(Valid bit),如果为 0,表示该页表项无效,即不指向任何页表或物理页。
然后,检查 PTE 的访问权限位,包括读(Read)、写(Write)和执行(Execute)权限位。如果这些权限位都为 0,表示没有访问权限,说明该页表项指向另一个页表,而非物理页。
模仿freewalk,我们可以实现遍历整个页表输出PTE索引、PTE位以及从PTE中提取的物理地址,但还需要一个变量level
来标记当前的层数,以便输出正确..
缩进数量来展示美丽的结构。
在defs.h中添加声明
敲make qemu
,启动
任务三
思路
当我们访问一个虚拟页面时,xv6 会在此页表条目中设置 PTE_A(accessed)标志位,表示该页面已经被访问过。因此,我们需要检查每个虚拟页面的页表条目,以确定是否被访问过。为了实现这个目标,我们需要遍历三级页表树中的所有页表条目。
步骤
查看pgaccess_test()
函数首先是创建了32个页面大小的字节块并将其地址赋给了指针变量buf(每个字节块占用一个页面),然后测试了pgaccess函数内部是否出错,接着对buf进行三次操作,分别是将第1块、第2块和第30块的字节加1来模拟对页面访问的操作,然后再次使用pgaccess函数来测试是否与预期结果一致。
可以看到pgaccess函数
走系统调用,传入了3个参数。我们在实现sys_pgaccess
系统调用时需要使用提示中的argaddr()
和argint()
来获取用户态传入的参数。
具体来说,这个函数的作用是检测一段内存区域中的页面是否被访问,并将结果以二进制形式保存在 mask 指向的内存区域中。
参数解释:
- base:指向要检测的内存区域的起始地址。
- len:要检测的页面数。
- mask:保存结果的内存区域的指针,需要预先分配足够的空间来存储结果。
定义PTE_A标志位
声明walk()
将 walk() 函数的声明放在 defs.h 文件中,使其在整个项目中都可见和访问。
实现sys_pgaccess
sys_pgaccess()
的作用是读取用户态指定地址范围内各页面是否被访问(Accessed)过,并将访问位状态以二进制标记的形式返回给用户层程序。
下面是代码的具体流程:
- 获取进程对象和系统调用参数
这段代码首先获取当前进程对象,然后获取系统调用的三个参数:base
、len
和 mask
。其中,base
是指定的内存地址范围的起始地址,len
是该范围内页面数量,mask
是用户层程序提供的一个输出参数,用于接收各页面的访问位状态。
- 遍历指定地址范围的各个页面
代码使用变量 va
来追踪正在访问的虚拟地址,初始值为 base
。然后,使用一个循环遍历指定地址范围内的每个页面。在每个页面上操作时,代码通过调用 walk()
函数来获取相应的页表项(PTE),然后检查 PTE 中的 Accessed 标志位(PTE_A
)。如果该页面被访问过,则将abits
对应的二进制位设置为 1,然后清除PTE 中的 Accessed 标志位。最后,将虚拟地址加上一个页面大小(PGSIZE
),以便在下一次循环中操作下一页。
- 将访问位状态写入输出参数
代码调用 copyout()
函数将访问位状态abits
以二进制标记的形式写入用户层程序提供的 mask
参数中。该函数将访问位状态从内核空间复制到用户空间,并返回是否复制成功的状态码。
- 返回操作结果
如果 copyout()
函数执行成功,则该函数返回 0,表示操作成功。否则,返回 -1,表示操作失败。
结果
实验总结
通过本次实验,我们深入了解了操作系统中进程虚拟地址空间的管理机制,以及RISC-V架构下的三级页表结构和地址转换过程。同时,我们还复习了系统调用实验中有关用户态与内核态数据交互的知识。
首先,我们认识到每个进程都拥有独立的虚拟地址空间,通过页映射机制,将虚拟地址映射到物理地址,实现了进程间的地址隔离。在实验中,我们通过访问一个特定的虚拟地址USYSCALL,成功获取了当前进程的PID,从而验证了虚拟地址空间与物理地址空间的映射关系。
其次,我们学习了RISC-V架构下的三级页表结构,了解了页表项(PTE)及其标志位的作用。通过遍历进程的页表,我们深入了解了页表项中各个标志位的含义,如有效位、读写位、用户位等,以及它们对地址转换过程的影响。
在实验过程中,我们还复习了系统调用实验的知识,包括如何从用户态获取参数、如何将内核态的数据传递到用户态。这些知识对于我们理解操作系统内部机制以及实现用户态与内核态的数据交互具有重要意义。