xv6是MIT为其操作系统课程开发的一个教学目的操作系统,其系统调用的过程是如何进行的?本文将以xv6的RISC-V版本进行说明。
1 系统调用
用户运行的应用程序处于用户态,处于用户态的进程受到诸多限制,某些工作必须由内核代为完成,因此用户进程需要使用系统调用。
内核态下可以获得的特权:
- 读写控制寄存器,包括
satp、stvec、sepc等 - 可以使用页表中用户不可用的表项
2 xv6系统调用的过程
下图是整个过程的流程图,先从整体上感受其过程,由于某些命名比较相似,请注意不要混淆其功能,比如uservec、usertrap、userret

2.1 系统调用函数原型及汇编代码生成
xv6使用名为usys.pl的perl脚本在用户空间中生成一个包括所有系统调用的汇编代码
1 | sub entry { |
在perl脚本中调用entry("exit");就能在usys.S中生成如下汇编代码
1 | .global exit |
而fork函数的函数原型声明在user.h头文件中,当用户程序调用exit(0);时,就会将参数0存入a0参数寄存器,然后使用jalr指令跳转到上述汇编代码中,然后将系统调用号(宏定义SYS_exit)放入a7寄存器中,再由ecall指令发起系统调用,此时会将当前程序计数器pc寄存器的值保存至sepc控制寄存器中。
2.2 用户空间和内核空间的切换
stvec控制寄存器中保存着uservec函数的地址,当用户使用ecall指令后,会跳转到uservec函数,这个函数位于trampoline.S的汇编代码(由于用户态跳转内核态、内核态跳转用户态都需要经过这里面的代码,故命名为蹦床trampoline)中,uservec作用如下:
- 保存32个通用寄存器的值到一片指定的内存区域,这个区域称为
trapframe,而sscratch寄存器中保存着trapframe的地址 - 将
sp栈寄存器的值设为内核栈的地址,开始使用内核的栈;将tp寄存器的值设为该进程所处cpu的id - 从
trapframe中取得内核页表的地址,并设置satp寄存器指向内核页表(内核页表的地址是在用户进程被创建时放入trapframe的),开始使用内核的页表 - 从
trapframe中取得usertrap函数的地址,跳转到usertrap中
每个用户进程都在特定的内存地址保存trampoline的代码和trapframe,用户进程页表如下:

2.3 内核完成系统调用并返回
跳转进入usertrap函数后,usertrap执行流程如下:
- 读取
sepc寄存器的值并保存到trapframe中,返回到用户空间时程序会从该值指向的指令处重新开始执行,目前sepc寄存器的值指向之前的ecall指令 - 读取
scause寄存器的值,scause寄存器的值表明了用户陷入(trap)内核的原因,如果值为8则代表要进行系统调用 - 将
trapframe中保存的sepc值+4,表明返回用户空间后从ecall的下一条指令开始执行 - 调用
syscall函数
syscall函数执行流程如下:
- 从
trapframe中取出a7寄存器的值,这个值是系统调用号,表明了要进行哪一个系统调用(是在usys.S中发起系统调用时放入a7寄存器的) - 执行相应的系统调用函数,该函数会从
trapframe中取出函数所需的参数并执行功能(所有参数寄存器都被保存在trapframe中) - 将返回值保存到
trapframe中,返回到用户空间时即可取得该返回值
相应的系统调用完成后,将会执行usertrapret函数,其执行流程如下:
- 在
trapframe中保存内核页表的地址、内核栈的地址、cpu的id - 将
trapframe中保存的sepc值写入sepc寄存器中,由于在内核中也可能发生trap,会导致sepc寄存器的值被修改,所以现在要再次写一遍sepc寄存器的值 - 跳转到
trampoline中的userret
每个用户进程在内核中被首次创建出来后都会调用一遍usertrapret函数返回用户空间,从而其trapframe中会保存有内核页表地址等信息,使其可以完成系统调用。
trampoline中的userret执行流程如下:
- 将
trapframe中保存的通用寄存器值恢复到32个寄存器中,因为trapframe中a0寄存器的值已经在syscall中被修改成了系统调用函数的返回值,因此恢复过程完毕后a0寄存器中就保存着返回值供用户程序使用 - 将
satp寄存器设置为指向用户的页表(用户页表地址是userret函数的参数之一) - 使用
sret指令返回用户空间继续执行指令,会将pc寄存器的值设置为sepc寄存器的值,而sepc寄存器的值在usertrap中被保存,在usertrapret中被恢复
3 总结
再次放出流程图

3.1 相关的控制寄存器
sepc:发生trap时将pc寄存器的值保存;sret指令会跳转到sepc寄存器所指的指令stvec:执行ecall指令后会跳转到的位置;在用户空间中指向trampoline中的uservecsccratch:保存着trapframe的内存地址scause:其值说明了发生trap的原因,8代表系统调用
3.2 trapframe和trampoline
trapframe:用于保存通用寄存器的值和系统调用的返回值,还保存了内核页表、内核栈的地址trampoline:有用于从用户态跳转到内核态、从内核态跳转到用户态的两个函数uservec和userret
3.3 相关的函数
uservec:保存通用寄存器的值、切换页表和栈usertrap:改写trapframe中的sepc值,调用syscallsyscall:执行相应的系统调用函数,将返回值放入trapframeusertrapret:恢复sepc寄存器的值,将内核页表、内核栈的地址写入trapframeuserret:恢复通用寄存器的值,切换页表和栈