发布于 

xv6系统调用过程

xv6是MIT为其操作系统课程开发的一个教学目的操作系统,其系统调用的过程是如何进行的?本文将以xv6的RISC-V版本进行说明。

1 系统调用

用户运行的应用程序处于用户态,处于用户态的进程受到诸多限制,某些工作必须由内核代为完成,因此用户进程需要使用系统调用。

内核态下可以获得的特权:

  • 读写控制寄存器,包括satpstvecsepc
  • 可以使用页表中用户不可用的表项

2 xv6系统调用的过程

下图是整个过程的流程图,先从整体上感受其过程,由于某些命名比较相似,请注意不要混淆其功能,比如uservecusertrapuserret

过程

2.1 系统调用函数原型及汇编代码生成

xv6使用名为usys.pl的perl脚本在用户空间中生成一个包括所有系统调用的汇编代码

1
2
3
4
5
6
7
8
sub entry {
my $name = shift;
print ".global $name\n";
print "${name}:\n";
print " li a7, SYS_${name}\n";
print " ecall\n";
print " ret\n";
}

在perl脚本中调用entry("exit");就能在usys.S中生成如下汇编代码

1
2
3
4
5
.global exit
exit:
li a7, SYS_exit
ecall
ret

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执行流程如下:

  1. 读取sepc寄存器的值并保存到trapframe中,返回到用户空间时程序会从该值指向的指令处重新开始执行,目前sepc寄存器的值指向之前的ecall指令
  2. 读取scause寄存器的值,scause寄存器的值表明了用户陷入(trap)内核的原因,如果值为8则代表要进行系统调用
  3. trapframe中保存的sepc值+4,表明返回用户空间后从ecall的下一条指令开始执行
  4. 调用syscall函数

syscall函数执行流程如下:

  1. trapframe中取出a7寄存器的值,这个值是系统调用号,表明了要进行哪一个系统调用(是在usys.S中发起系统调用时放入a7寄存器的)
  2. 执行相应的系统调用函数,该函数会从trapframe中取出函数所需的参数并执行功能(所有参数寄存器都被保存在trapframe中)
  3. 将返回值保存到trapframe中,返回到用户空间时即可取得该返回值

相应的系统调用完成后,将会执行usertrapret函数,其执行流程如下:

  1. trapframe中保存内核页表的地址、内核栈的地址、cpu的id
  2. trapframe中保存的sepc值写入sepc寄存器中,由于在内核中也可能发生trap,会导致sepc寄存器的值被修改,所以现在要再次写一遍sepc寄存器的值
  3. 跳转到trampoline中的userret
trapframe的初始化

每个用户进程在内核中被首次创建出来后都会调用一遍usertrapret函数返回用户空间,从而其trapframe中会保存有内核页表地址等信息,使其可以完成系统调用。

trampoline中的userret执行流程如下:

  1. trapframe中保存的通用寄存器值恢复到32个寄存器中,因为trapframea0寄存器的值已经在syscall中被修改成了系统调用函数的返回值,因此恢复过程完毕后a0寄存器中就保存着返回值供用户程序使用
  2. satp寄存器设置为指向用户的页表(用户页表地址是userret函数的参数之一)
  3. 使用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:有用于从用户态跳转到内核态、从内核态跳转到用户态的两个函数uservecuserret

3.3 相关的函数

  • uservec:保存通用寄存器的值、切换页表和栈
  • usertrap:改写trapframe中的sepc值,调用syscall
  • syscall:执行相应的系统调用函数,将返回值放入trapframe
  • usertrapret:恢复sepc寄存器的值,将内核页表、内核栈的地址写入trapframe
  • userret:恢复通用寄存器的值,切换页表和栈

本站由 @Eumendies 使用 Stellar 主题创建。 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。