xv6系统调用过程
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
中的uservec
sccratch
:保存着trapframe
的内存地址scause
:其值说明了发生trap的原因,8代表系统调用
3.2 trapframe和trampoline
trapframe
:用于保存通用寄存器的值和系统调用的返回值,还保存了内核页表、内核栈的地址trampoline
:有用于从用户态跳转到内核态、从内核态跳转到用户态的两个函数uservec
和userret
3.3 相关的函数
uservec
:保存通用寄存器的值、切换页表和栈usertrap
:改写trapframe
中的sepc
值,调用syscall
syscall
:执行相应的系统调用函数,将返回值放入trapframe
usertrapret
:恢复sepc
寄存器的值,将内核页表、内核栈的地址写入trapframe
userret
:恢复通用寄存器的值,切换页表和栈