实验1: 加速系统调用速度
代码实现 & 思路
其中,代码添加部分在注释// add start from here
和// add end
之间
- 在
kernel/proc.h proc
结构体中添加一项指针来保存页表地址
// kernel/proc.h
struct proc {
...
struct usyscall *usyscallpage; // 添加usyscallpage指针
}
- 在
kernel/proc.c
的allocproc()
中为其分配空间(kalloc)。并初始化其保存当前进程的pid
// kernel/proc.c
static struct proc*
allocproc(void)
{
...
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// add start from here
// 分配内存空间,返回指针赋值。分配失败时,指针为0。
if ((p->usyscallpage = (struct usyscall *)kalloc()) == 0) {
freeproc(p); // 释放进程
release(&p->lock); // 释放进程锁
return 0;
}
p->usyscallpage->pid = p->pid; // 初始化pid,记录当前进程的pid。
// add end
// An empty user page table.
p->pagetable = proc_pagetable(p);
...
}
- 在
kernel/proc.c
的proc_pagetable()
中将这个映射(PTE)写入pagetable
中,权限是用户态可读
// kernel/proc.c
pagetable_t
proc_pagetable(struct proc *p)
{
pagetable_t pagetable;
// An empty page table.
pagetable = uvmcreate();
if(pagetable == 0)
return 0;
// add start from here
// 映射失败,即返回值小于0
if(mappages(pagetable, USYSCALL, PGSIZE,
(uint64)(p->usyscallpage), PTE_R | PTE_U) < 0) {
uvmfree(pagetable, 0); // 释放页表
return 0;
}
// add end
// map the trampoline code (for system call return)
// at the highest user virtual address.
// only the supervisor uses it, on the way
// to/from user space, so not PTE_U.
if(mappages(pagetable, TRAMPOLINE, PGSIZE,
(uint64)trampoline, PTE_R | PTE_X) < 0){
uvmfree(pagetable, 0);
return 0;
}
...
}
- 在
kernel/proc.c
的freeproc()
中确保释放进程的时候,能够释放该共享页,将页表插入空闲页链表中
// kernel/proc.c
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
// add start from here
// 判断是否为非空指针,即是否分配共享页。是,则释放内存空间,并将其设置为0.
if(p->usyscallpage)
kfree((void *)p->usyscallpage);
p->usyscallpage = 0;
// add end
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
...
}
- 在
kernel/proc.c
的proc_freepagetable()
中解除映射关系
// kernel/proc.c
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, USYSCALL, 1, 0); // 解除映射关系
...
}
运行结果
系统调用流程
- 用户程序通过系统调用指令(如syscall指令)触发系统调用。
- CPU从用户态切换到内核态。
- CPU执行中断处理程序,将控制权转移到内核中的系统调用处理函数。
- 系统调用处理函数根据系统调用号,执行相应的操作。
- 在执行系统调用之前,系统调用处理函数可以访问用户空间和内核空间之间的只读区域,其中包括了共享的
usyscallpage
页表。 - 系统调用处理函数根据需要,读取或修改
usyscallpage
中的数据。 - 系统调用处理函数执行完毕后,将结果返回给用户程序。
- CPU从内核态切换回用户态,用户程序继续执行。
实验2: 打印页表
代码实现
- 在
defs.h
中定义一个vmprint()
// kernel/defs.h
...
// vm.c
...
void vmprint(pagetable_t);
...
- 在
vm.c
中实现vmprint()
的函数
// kernel/vm.c
...
static int ptLevel = 0; // ptLevel:表示当前打印页表层级
void
vmprint(pagetable_t pagetable) {
// 页表层级为0时,打印页表地址
if (ptLevel == 0)
printf("page table %p\n", (uint64)pagetable);
// 遍历页表
for (int i = 0; i < 512; i++) {
pte_t pte = pagetable[i];
// 检查PTE_V标志位是否有效
if (pte & PTE_V) {
// 根据页表层级打印对应数量的缩进
for (int j = 0; j <= ptLevel; j++) {
printf("..");
}
// 打印页表条目的索引、值、地址
printf("%d: pte %p pa %p\n", i, (uint64)pte, (uint64)PTE2PA(pte));
}
// 对于有效的页表条目,检查读、写、执行权限的标志位,若都为0,进行操作
if((pte & PTE_V) && (pte & (PTE_R | PTE_W | PTE_X)) == 0) {
ptLevel++; // 满足条件,层级加1
uint64 childPT = PTE2PA(pte); // childPT:将页表条目转换为物理地址
vmprint((pagetable_t)childPT); // 作为vmprint起始地址的页表,打印更低层级的页表,完成递归。
ptLevel--; // 恢复原先层级
}
}
}
- 在
exec.c
中return argc;
之前插入
// kernel/exec.c
int
exec(char *path, char **argv)
{
...
// 当前进程pid为1时,开始执行
if(p->pid == 1) {
vmprint(p->pagetable);
}
return argc; // this ends up in a0, the first argument to main(argc, argv)
...
}
运行结果
系统调用流程
- 调用exec()函数。
- 在
exec()
函数中,如果当前进程的pid为1,则调用vmprint(p->pagetable)
打印页表内容。 vmprint()
函数会递归地遍历页表,并打印每个页表条目的信息。- 打印完成后,回到
main()
。
实验3: 检测哪些页被访问
代码实现
- 在
kernel/sysproc.c
中实现sys_pgaccess()
// kernel/sysproc.c
...
#ifdef LAB_PGTBL
int
sys_pgaccess(void)
{
// lab pgtbl: your code here.
uint64 virtual_addr; // virtual_addr: 第一个用户页的起始虚拟地址
argaddr(0, &virtual_addr);
int num_pages; // num_pages: 被检查的页面数
argint(1, &num_pages);
uint64 abitsaddr; // abitsaddr: 缓冲区地址
argaddr(2, &abitsaddr);
uint64 maskbits = 0; // maskbits: 储存页访问情况的掩码
// 获取当前进程的指针,循环遍历要检查页面
struct proc *proc = myproc();
for (int i = 0; i < num_pages; i++) {
// 获取页面对应的页表条目指针
pte_t *pte = walk(proc->pagetable, virtual_addr + i * PGSIZE, 0);
// 条目不存在,报错
if(pte == 0)
panic("Page does not exist!");
// 被访问过,将掩码设置为1
if(PTE_FLAGS(*pte) & PTE_A) {
maskbits |= (1L << i);
}
// 记录、重置访问位
*pte = ((*pte & PTE_A) ^ *pte) ^ 0;
}
// 通过copyout()将掩码存在缓冲区,然后复制到用户态
if (copyout(proc->pagetable, abitsaddr, (char *)&maskbits, sizeof(maskbits)) < 0)
panic("sys_pgacess copyout error!");
return 0;
}
#endif
...
- 需要在
kernel/riscv.h
中定义PTE_A
(访问位)
// kernel/riscv.h
...
// 定义访问位
#define PTE_A (1L << 6)
...
运行结果
系统调用流程
- 通过系统调用指令触发系统调用。
- 转内核态,内核会根据系统调用号调用相应的处理函数,即
sys_pgaccess()
。 - 处理函数会获取用户传递的参数,并进行页访问检测的操作。
- 将结果返回给用户程序。