System calls
Contents
这是 6.s081 的第二个实验,使用系统调用来写一些工具,从而帮助你更好的了解系统调用是如何工作的。
RISC-V 调用规约 (calling convention)
为了更好的理解系统调用的过程,这里需要了解一下 risc-v 的调用规约,即 risc-v 在进行函数调用时,调用者和被调用者需要遵循的一种约定,首先来看一下调用过程中寄存器的使用。
寄存器名 | ABI 名(编程用名) | 用途约定 | 谁负责在函数调用过程中维护这些寄存器 |
---|---|---|---|
x0 | zero | 读取时总为 0,写入时无效 | N/A |
x1 | ra | 存放函数返回的地址(return address) | Caller |
x2 | sp | 存放栈指针(stack pointer) | Callee |
x5-x7,x28-x31 | t0-t2,t3-t6 | 临时(temporaries) 寄存器,Callee 可能会使用这些寄存器,所以 Callee 不保证这些寄存器中的值在函数调用过程中保存不变,这意味着对于 Caller 来说,如果需要的话,Caller 需要自己在调用 Callee 之前保存临时寄存器中的值。 | Caller |
x8,x9, x18-x27 | s0,s1,s2-s11 | 保存(saved)寄存器,Callee 需要保证这些寄存器的值在函数返回后仍然维持函数调用之前的原值,所以一旦 Callee 在自己的函数中会用到这些寄存器则需要在栈中备份并在退出函数时进行恢复。 | Callee |
x10, x11 | a0,a1 | 参数(argument)寄存器,用于函数调用过程中保存第一个和第二个参数,以及在函数返回是传递返回值。 | Caller |
x12-x17 | a2-a7 | 参数 (argument)寄存器,如果函数调用时需要传递更多的参数,则可以用这些寄存器,但注意用于传递参数的寄存器最多只有 8 个 (a0-a7),如果还有更多的参数则要利用栈。 | Caller |
接下来看一下调用过程常用的一些指令。
伪指令 | 等价指令 | 描述 | 例子 |
---|---|---|---|
jal offset |
jal x1, offset |
跳转到 offset 指定位置,返回地址保存在 x1(ra) | jal foo |
jalr rs |
jalr x1, 0(rs) |
跳转到 rs 中值指定的位置,返回地址保存在 x1(ra) | jalr s1 |
j offset |
jal x0, offset |
跳转到 offset 指定位置,不保存返回地址 | j loop |
jr rs |
jalr x0, 0(rs) |
跳转到 rs 值指定位置,不保存返回地址 | jr s1 |
call offset |
auipc x1,offset[31:12] + offset[11];jalr x1 offset[11:0](x1) |
长跳转调用函数 | call foo |
tail offset |
auipc x6,offset[31:12] + offset[11];jalr x0 offset[11:0](x6) |
长跳转尾调用 | tail foo |
ret |
jalr x0, 0(x1) |
从 Callee 返回 | ret |
函数调用时的栈帧。
看一个具体函数调用的例子。
|
|
这个调用例子里 ra 即 return address 并没有压进栈里,猜测是编译器认为在这个函数里不会调用其他函数,所以 ra 寄存器的值不会变,可以直接读取 ra 里的值作为返回地址。
下面这个例子里 ra 是压进栈里的。
|
|
系统调用流程
从启动一个进程调用 exec 系统调用开始来了解一个整个系统调用的流程。首先 exec 是在 (user/initcode.S:7) 开始的。
|
|
a0, a1 存放了系统的调用的参数,a7 存放了系统调用号,对应 (kernel/syscall.c:108) 中的定义。
|
|
ecall
指令陷入内核执行 uservec
, usertrap
,然后执行 syscall
。(kernel/trapoline.S:16)
|
|
(kernel/trap.c:37)
|
|
(kernel/syscall.c:133)
|
|
系统调用的返回值会写到 p->trapframe->a0
,当 exec()
返回时通过 a0 读到系统调用的返回值,系统调用通常用负数返回值来表示错误,0 表示成功。
上面简单梳理了系统调用的流程,梳理清楚这些就可以做这个 lab2 了,当然这里还有些细节,比如参数如何从用户态传递到内核态,内核怎么通过用户态传入的地址寻址等等这些要等后面虚拟内存和中断两个实验做完才知道。
System call tracing
这里要实现一个新的系统调用 trace 用来跟踪调用了哪个系统调用,trace 系统调用传入一个参数 mask 来指定需要跟踪哪个系统调用,trace 系统调用要打印出系统调用的名称和返回值以及进程号。
根据实验提示首先在 Makefile
里把 $U/_trace
加到 UPROGS
。
然后增加 trace 系统调用,_trace 进程会调用 sys_trace
系统调用。
|
|
为了保存 mask 参数需要在进程结构体中加入一个变量来记录。
|
|
同时不要忘了在 fork 子进程是将参数复制到子进程
|
|
最后在系统调用的路口判断,然后打印需要的信息就可以了
|
|
Sysinfo
这里要实现一个系统调用 sysinfo,参数是一个 sysinfo 结构体,用来收集系统信息,包括有多少空闲内存,正在运行的进程数。
根据实验提示首先在 Makefile
里把 $U/_sysinfotest
加到 UPROGS
。
声明 sysinfo 系统调用
|
|
sysinfo 需要将 struct sysinfo 拷贝回用户空间,这里可以参考 sys_fstat()
|
|
分别实现 get_free_memory()
和 get_free_memory()
。
|
|
|
|
总结
通过这个实验对系统调用是如何工作的有了更深的了解,同时也是学习了下 rsic-v,对 rsic-v 有了些了解。