核心问题:xv6是如何执行系统调用的?系统调用时用户进入内核的一种手段(一般是受限直接执行(Limited Direct Execution, LDE)机制)。如下图所示:
有三种事件会导致CPU搁置普通指令的执行,强制将控制权转移给处理该事件的特殊代码。
- 系统调用:当用户程序执行ecall指令要求内核为其做某事时。
- 异常:一条指令(用户或内核)做了一些非法的事情,如除以零或使用无效的虚拟地址。
- 中断:当一个设备发出需要注意的信号时,例如当磁盘硬件完成一个读写请求时。
在xv6中这三种情况统称为trap。
xv6 trap 处理分为四个阶段:
- RISC-V CPU采取的硬件行为
- 为内核C代码准备的汇编入口
- 处理trap的C 处理程序
- 系统调用或设备驱动服务。
trap机制-硬件
每个RISC-V CPU都有一组控制寄存器,内核写入这些寄存器来告诉CPU如何处理trap,内核可以通过读取这些寄存器来发现已经发生的trap。监督者模式下的控制寄存器如下:
以下是关于trap机制的寄存器的概述:
stvec
:内核在这里写下trap处理程序的地址;RISC-V跳转到这里来处理trap。sepc
:当trap发生时,RISC-V会将程序计数器保存在这里(因为PC
会被stvec
覆盖)。sret
(从trap中返回)指令将sepc
复制到pc
中。内核可以写sepc
来控制sret
的返回到哪里。scause
:RISC -V在这里放了一个数字,描述了trap的原因。sscratch
:内核在这里放置了一个值,在trap处理程序开始时可以方便地使用。sstatus
:sstatus
中的SIE位控制设备中断是否被启用,如果内核清除SIE,RISC-V将推迟设备中断,直到内核设置SIE。SPP位表示trap是来自用户模式还是supervisor模式,并控制sret
返回到什么模式。
多核芯片上的每个CPU都有自己的一组这些寄存器,而且在任何时候都可能有多个CPU在处理一个trap。
重点关注stvec
、 sepc
、scause
三个寄存器即可。
当需要执行trap时,RISC-V硬件对所有的trap类型(除定时器中断外)进行以下操作:
- 如果该trap是设备中断,且
sstatus
SIE位为0,则不执行以下任何操作。 - 通过清除 SIE 来禁用中断。
- 复制
pc
到sepc
。 - 将当前模式(用户态或特权态)保存在
sstatus
的 SPP 位。 - 在
scause
设置该次trap的原因。 - 将模式转换为特权态。
- 将
stvec
复制到pc
。 - 从新的
pc
开始执行。
从用户态陷入内核
系统调用的入口
要想理清楚xv6系统调用过程,就必须从用户态程序开始入手,下面以lab1下用户态程序 sleep.c
为例,其c程序如下(调用了sleep系统调用):
|
|
在shell中执行编译命令 make qemu
后,可以看到 sleep.c
生成了对应的 sleep.asm
文件,查看之,有如下代码片段:
|
|
其中 ECALL
是RISC-V中的一个专门的指令,可以让应用程序将控制权转移给内核(Entering Kernel)。
ECALL
接收一个数字参数(a7),当一个用户程序想要将程序执行的控制权转移到内核,它只需要执行 ECALL
指令,并传入一个数字。这里的数字参数代表了应用程序想要调用的System Call。
ps: 对应的,x86中就是int 0x80
执行 ecall
指令时发生了什么呢?ecall
使程序跳转到哪里呢?
Risc-v中有一个STVEC
寄存器,这是一个只能在supervisor mode下读写的特权寄存器。在从内核空间进入到用户空间之前,内核会设置好STVEC寄存器指向内核希望trap代码运行的位置。
Xv6在内核页表和每个用户页表中的同一个虚拟地址上映射了 trampoline page
。STVEC
寄存器保存的地址是 trampoline page
的起始位置,主要执行一些保护用户态寄存器的操作。trampoline page
是 uservec
函数的起始位置。
为什么
STVEC
会跳转到uservec
函数?STVEC
寄存器的值是什么时候写入的?在下文的
usertrapret
函数中写入,如下,这个函数在操作系统将内核切换到用户进程的时候就会调用。
1
2// send syscalls, interrupts, and exceptions to trampoline.S
w_stvec(TRAMPOLINE + (uservec - trampoline));
安全吗?
trampoline page
是在用户地址空间的user page table完成的映射,用户态代码不能写它,因为这些page对应的PTE并没有设置PTE_u标志位。
总结一下,ecall
调用时发生了什么 :
- 关中断。
ecall
将代码从user mode改到supervisor mode。ecall
将程序计数器的值保存在了SEPC
寄存器。ecall
会跳转到STVEC
寄存器指向的指令。
uservec
函数
trampoline page
中的 uservec
函数主要执行以下几个功能:
保存用户寄存器的内容至
trapframe
(xv6在每个user page table映射了trapframe page
,每个进程都有自己的trapframe page
,定义可见kernel/proc.h:44
) ,此时需要使用sscratch
以一定技巧腾出a0
寄存器。恢复内核栈、内核页表,刷新TLB,设置下一步trap处理函数
usertrap
的地址。跳转至
usertrap
函数执行。
uservec
函数代码如下:
|
|
usertrap
函数
usertrap
的作用是确定trap的原因,处理它,然后返回(kernel/ trap.c:37)。若是系统调用,其 scause
寄存器值为8,usertrap
函数调用 syscall
函数进行处理。
|
|
为什么要改变
stvec
?因为当前已经在内核中了,在内核中发生的trap将由
kernelvec
处理。所以要写入stvec
寄存器的值为kernelvec
:
1
2
3// send interrupts and exceptions to kerneltrap(),
// since we're now in the kernel.
w_stvec((uint64)kernelvec);
为什么要保存
sepc
?此时,
sepc
中的数据为用户pc。因为xv6在执行ecall
的时候,程序计数器的值被保存在了SEPC
寄存器中。当程序还在内核中执行时,可能需要切换到另一个进程,并进入到那个程序的用户空间,然后那个进程可能再调用一个系统调用进而导致SEPC寄存器的内容被覆盖。因此需要保存一下用户pc,代码如下:
1
2// save user program counter.
p->trapframe->epc = r_sepc();
syscall
函数
经过种种艰辛,终于来到了系统调用的处理函数。
- 用户态代码在调用系统调用时,将系统调用函数的参数放在寄存器
a0
、a1
等寄存器中中,并将系统调用号放在a7
中。 syscall
(kernel/syscall.c:133)从trapframe
中的a7
中得到系统调用号,并其作为索引在syscalls
查找相应函数。对于系统调用sleep
,其a7
寄存器值为13(SYS_sleep
),这会让syscall
函数调用sleep
系统调用的实现函数sys_sleep
。- 当系统调用函数返回时,
syscall
将其返回值记录在p->trapframe->a0
中。
其代码如下:
|
|
从内核中返回用户态
usertrapret
函数
usertrap
函数的最后调用了 usertrapret
函数,设置RISC-V控制寄存器,为以后用户空间trap做准备。
- 改变
stvec
来引用uservec
- 准备
uservec
所依赖的trapframe
字段 - 将
sepc
设置为先前保存的用户程序计数器。 usertrapret
在用户页表和内核页表中映射的trampoline页上调用userret
(userret
中的汇编代码会切换页表)。
|
|
userret
函数
usertrapret
对 userret
的调用传递了参数a0
,a1
, 其中a0
指向TRAPFRAME
,a1
指向用户进程页表。
userret将
satp
切换到进程的用户页表。在用户页表和内核页表中,trampoline
页被映射在相同的虚拟地址上,这也是允许userret
(以及uservec
)在改变satp
后继续执行的原因。userret
将trapframe
中保存的用户a0
复制到sscratch
中,为以后与TRAPFRAME
交换做准备。(此时,userret
能使用的数据只有寄存器内容和trapframe
的内容。)- 此时
sscratch
: 用户的a0
,从trapframe
中读取的;a0
:trapframe
地址,函数参数传入。
- 此时
userret
从trapframe
中恢复保存的用户寄存器,对
a0
和sscratch
做最后的交换,恢复用户a0
并保存TRAPFRAME
,为下一次trap做准备。- 此时
sscratch
:trapframe
地址,下次trap使用;a0
: 用户的a0
。
- 此时
使用
sret
返回用户空间。
|
|
总结一下,sret
调用时发生了什么 :
程序会切换回user mode
SEPC
寄存器的数值会被拷贝到PC
寄存器重新打开中断
总体流程
从用户态陷入(trap)内核的流程如下: