xv6-k210/doc/构建调试-系统调用.md

170 lines
7.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Syscall在xv6-k210中的实现与执行
Syscall是如何实现与执行的呢以下从用户程序出发分析在xv6-k210中系统调用的过程。
## 一、Syscall的封装与使用
使用C语言编写用户程序时可以直接编写内联汇编的形式执行`ecall`指令,但这不够美观便捷。
xv6-k210将系统调用的汇编指令段进行了封装提供了一个C函数的接口在用户程序中可以直接像使用普通函数一样调用
包括传递参数和接收返回值。
在[xv6-user/user.h](/xv6-user/user.h)中声明了各个Syscall封装后的函数接口。在用户程序中包含该头文件即可使用其中的函数。
```C
// xv6-user/user.h
int fork(void);
int exit(int) __attribute__((noreturn));
int wait(int*);
int pipe(int*);
int write(int, const void*, int);
int read(int, void*, int);
......
```
这些函数又是怎么实现的呢xv6-k210使用了perl语言脚本xv6-user/usys.pl重定向输出生成一个汇编源文件这些函数就是在其中实现的。
不需要perl语言的知识也能大致理解该文件中代码的含义。文件中部分代码如下
```perl
print "#include \"kernel/include/syscall.h\"\n";
sub entry {
my $name = shift;
print ".global $name\n";
print "${name}:\n";
print " li a7, SYS_${name}\n";
print " ecall\n";
print " ret\n";
}
entry("fork");
entry("exit");
entry("wait");
......
```
其中`sub`就是perl语言中的函数定义。这里不深究perl语言的语法含义只需知道在xv6-k210编译时
通过将***print***函数的输出重定向至一个汇编源文件具体可查看Makefile该文件中将会生成若干类似下述的汇编片段
```
#include "kernel/include/syscall.h"
.global fork
fork:
li a7, SYS_fork
ecall
ret
.global exit
exit:
li a7, SYS_exit
ecall
ret
......
```
这就实现了xv6-user/user.h中声明的函数。此处包含了kernel/include/syscall.h文件因为该文件中宏定义了各个系统调用的调用号。
```C
// System call numbers
#define SYS_fork 1
#define SYS_exit 2
#define SYS_wait 3
......
```
综上所述我们知道xv6-k210为用户程序提供了`ecall`指令的封装,使得用户程序可以很方便地请求系统调用。
这里可能会有疑问系统调用需要的参数该怎么办呢RISC-V体系提供了数量不少的寄存器函数调用所需的参数通常通过寄存器传递。
后续内容会提到用户进程在trap后会在进程控制块中保存寄存器现场系统调用的参数也就存放在里头可以被访问获取。
## 二、特权态的转换的处理
RISC-V的处理器在发生trap后会自动完成若干行为详见[此处](/doc/内核原理-系统调用.md)
但硬件并没有完成我们所需的所有操作,例如寄存器现场保护,还需进一步通过软件方式实现。
根据xv6-k210的[页表与内存映射](/doc/内核原理-内存管理.md)机制,我们知道,
在用户程序虚拟地址空间的最高地址中,映射了`TRAMPOLINE`和`TRAPFRAME`两个特殊的页面,这两个页面与特权态的转换相关。
`TRAMPOLINE`对应的物理页中,存放着一些代码(见[kernel/trampoline.S](/kernel/trampoline.S)),包括`uservec`和`userret`两个函数。
其中,`uservec`函数的入口地址存放在`stvec`寄存器中也就是S模式trap处理函数的入口地址寄存器。
当执行`ecall`指令后,`pc`会被设置为`stvec`中的值,系统就会进入`uservec`函数。该函数主要有以下工作:
1. 在用户进程的`TRAPFRAME`中,保存用户进程的工作空间(即各个寄存器的内容);
2. 加载`TRAPFRAME`中保存的内核所需寄存器内容,如内核栈栈顶、内核态页表等。
随后,该函数通过`jr`指令跳转到`usertrap`函数(定义在[kernel/trap.c](/kernel/trap.c)中。Trampoline为“蹦床”之意由此看来还挺形象的。
## 三、Syscall的执行
在`usertrap`函数中进行一些后续操作后,可看到以下代码片段:
```C
......
struct proc *p = myproc();
p->trapframe->epc = r_sepc();
if (r_scause() == 8) {
if (p->killed)
exit(-1);
p->trapframe->epc += 4;
intr_on();
syscall();
}
......
```
这里首先获取该用户进程的进程控制块,该结构中也有对其`TRAPFRAME`的引用。发生trap时寄存器`pc`的值已经保存在`sepc`中,
这里再将其保存在进程的`TRAPFRAME`中以防再次发生trap。随后读取S模式CSRs中的`scause`寄存器其值为trap产生的原因。
当该值为8时对应的就是syscall。由于syscall返回时是回到`ecall`指令的下一条指令,因此这里将`epc`的值加4。
至此,已经明确不是外设中断,可以通过`intr_on`将中断打开。
接下来进入系统调用的处理函数`syscall`(在[kernel/syscall.c](/kernel/syscall.c)中定义),函数体如下:
```C
void syscall(void)
{
int num;
struct proc *p = myproc();
num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num]();
} else {
printf("%d %s: unknown sys call %d\n", p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
```
该函数首先获取当前进程的进程控制块,其`TRAPFRAME`中保存的`a7`寄存器的值就是请求的系统调用号。
检测该值的合法性后,调用相应的函数,函数返回值直接写入`TRAPFRAME`的`a0`寄存器。`a0`寄存器是RISC-V中约定的返回值寄存器
当返回用户态时,通过该寄存器就可以获得系统调用的返回结果,就如同普通的函数调用一样。
注意到上述代码中有一个`syscalls[]`数组,这就是所谓的系统调用表,实际上就是一个函数指针数组,指向各个系统调用的功能函数。
该数组和其中的内容也是在[kernel/syscall.c](/kernel/syscall.c)中声明,
而系统调用号在[kernel/include/syscall.h](/kernel/include/syscall.h)中声明,部分内容如下:
```C
extern uint64 sys_fork(void);
extern uint64 sys_exit(void);
extern uint64 sys_wait(void);
......
static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
[SYS_wait] sys_wait,
......
};
```
我们注意到,这里的函数都是无参数的,而有的系统调用是需要参数的,如何解决这一问题呢?
实际上,参数的获取可以由各功能函数自行完成,[kernel/syscall.c](/kernel/syscall.c)中也提供了相关的辅助函数(如`argint`)。
在需要参数的系统调用功能函数中,通过`myproc`函数获取进程的控制块,其`TRAPFRAME`中保存中断时各通用寄存器的值,
对通过寄存器传递的参数,可直接在其中找到;若参数是指针值,还需要其指向的实际内容,则先通过进程控制块中保存的页表起始地址,
找到进程的页表,再由页表将该指针值(虚拟地址)转换成物理地址,即可找到其实际指向的内容,完成系统调用的具体功能。
## 四、返回用户模式
系统功能调用完成后,从`syscall`函数返回到`usertrap`函数,该函数进行其他操作后,调用`usertrapret`也定义在kernel/trap.c中
这一函数则为返回U模式做准备包括设置S模式部分CSRs寄存器、在进程`TRAPFRAME`中保存进程在S模式下所需寄存器的值、重设`epc`寄存器等,
最后需要调用kernel/trampoline.S中的另一个函数——`userret`,其需要两个参数,一个是`TRAPFRAME`在用户空间的虚拟地址(由宏定义),
另一个是用户进程的页表在内核中的地址(已经格式化为正确形式)。
在跳转到`userret`函数后,首先将用户页表还原至页表寄存器`satp`并刷新TLB。之后就可以使用虚拟地址
将`TRAPFRAME`中保存的各个寄存器还原至对应寄存器,最后使用`sret`指令返回到用户模式一个Syscall到这里就完成了。
<br>
<br>