在上一个实验中,我们初次修改 xv6 的内核,为其添加了两个系统调用。本次实验的重点则是在于操作系统的另一个机制:页表。在本次实验中,我们会初探页表的一些性质,并利用页表机制完成一些任务。

在开始之前,将代码仓库切换到 pgtbl 分支

3.1 使用页表机制加速系统调用

在现实的操作系统中,很多系统调用只是读取内核态的一些数据结构,而并不进行写入操作,这类系统调用的一个典型例子就是 getpid 系统调用。对于这类系统调用,诸如 Linux 等 操作系统不会将内核的数据结构复制到用户空间,而是直接将存放该数据结构的页通过页表的机制映射到用户空间的对应位置,从而免去了复制的开销,提高了系统的性能。

本实验中,需要在进程创建时将一个内存页面以只读权限映射到 USYSCALL 位置(参见 memlayout.h)中的定义。该映射的页面开头存储有内核数据结构 struct usyscall ,该数据结构在进程创建时被初始化,并且存储有该进程的 pid 。在这个实验中,我们使用纯用户空间的函数 ugetpid() 来替代需要进行内核态到用户态拷贝的 getpid 系统调用。用户空间的函数 ugetpid() 已经在用户空间中被实现了,我们需要做的是将映射页面的工作完成。

首先在 kernel/proc.h proc 结构体中添加一项指针来保存这个共享页面的地址。

struct proc {
...
  struct usyscall *usyscall;  // share page whithin kernel and user
...
}

由于需要在创建进程时完成页面的映射,故而我们考虑修改 kernel/proc.c 中的 proc_pagetable(struct proc *p) ,即用于为新创建的进程分配页面的函数。在proc_pagetable() 完成分配新的页面后,即可以使用 mappages 函数分配页面。

<aside> 💡 proc_pagetable() 分配的是一个新的页表,该页表包含多个页表项。而 mappages() 分配的是一个物理页面,并将其映射到一个进程的虚拟地址空间中的一个虚拟地址上。

</aside>

页面映射时,需要设定其权限为只读,故权限位为 PTE_R | PTE_U ,此后在 allocproc() 中添加初始化该页面,向其中写入数据结构的代码:

<aside> 💡 PTE_R 和 PTE_U 是 xv6 操作系统中页表项的权限位,它们分别代表“读取”和“用户模式”权限。

PTE_R 表示该页表项具有读取权限。当该权限位被设置时,进程可以从该页表项所映射的物理页面中读取数据。如果该权限位未被设置,则进程无法从该页表项所映射的物理页面中读取数据。

PTE_U 表示该页表项可以在用户模式下访问。当该权限位被设置时,进程可以在用户模式下使用该页表项所映射的虚拟地址。如果该权限位未被设置,则该页表项只能在内核模式下使用,进程无法在用户模式下使用该页表项所映射的虚拟地址。

</aside>

// Allocate a usyscall page.
if((p->usyscall = (struct usyscall *)kalloc()) == 0){
	freeproc(p);
	release(&p->lock);
	return 0;
}
p->usyscall->pid = p->pid

这段代码是在为一个进程分配一个 usyscall 页面。在 xv6 操作系统中,usyscall 页面是用于实现用户态系统调用的页面,它包含了用户态系统调用的代码和数据。当用户进程发起系统调用时,操作系统会将进程的控制权从用户态切换到内核态,并使用 usyscall 页面中的代码处理系统调用请求。

具体来说,这段代码的作用如下:

  1. 使用 kalloc() 函数分配一个物理页面,该页面用于存储 usyscall 结构体。
  2. 检查是否分配成功,如果分配失败,则释放进程所占用的资源,并返回 0。
  3. 将 usyscall 结构体的 pid 成员设置为该进程的进程 ID。
  4. 将 usyscall 结构体的指针保存在进程控制块 proc 结构体中的 usyscall 成员中。

需要注意的是,usyscall 页面是在每个进程创建时分配的,因此该代码段应该在进程创建时被调用。