- 添加一个系统调用跟踪功能,该功能可以在以后的实验中为你提供帮助。
- 你将创建一个新的
trace
系统调用来控制跟踪。 - 它应该有一个参数,一个整数“mask(掩码)”,其指定要跟踪的系统调用。例如,为了跟踪
fork
系统调用,程序调用 trace (1 << SYS_fork)
,其中 SYS_fork
是来自 kernel/syscall.h
的系统调用号。 - 如果掩码中设置了系统调用的编号,则必须修改 xv6 内核以在每个系统调用即将返回时打印出一行。
- 该行应包含 进程 ID 、 系统调用名称 和 返回值 ;您不需要打印系统调用参数。
trace
系统调用应该为调用它的进程和它随后派生的任何子进程启用跟踪,但不应影响其他进程。
- 将
$U/_trace
添加到 Makefile
的 UPROGS
中 - 运行
make qemu
, 你将看到编译器无法编译 user/trace.c
,因为系统调用的用户空间存根还不存在:将系统调用的原型添加到 user/user.h
,将存根添加到 user/usys.pl
,以及将系统调用号添加到 kernel/syscall.h
中。Makefile
调用 perl 脚本 user/usys.pl
,它生成 user/usys.S
,实际的系统调用存根,它使用 RISC-V ecall
指令转换到内核。修复编译问题后,运行 trace 32 grep hello README
;它会失败,因为你还没有在内核中实现系统调用。 - 在
kernel/sysproc.c
中添加一个 sys_trace()
函数,该函数通过在 proc
结构中的新变量中记住其参数来实现新系统调用(请参阅 kernel/proc.h
)。从用户空间检索系统调用参数的函数位于 kernel/syscall.c
中,你可以在 kernel/sysproc.c
中查看它们的使用示例。 - 修改
fork()
(参见 kernel/proc.c
)以将跟踪的掩码从父进程复制到子进程。 - 修改
kernel/syscall.c
中的 syscall()
函数以打印跟踪输出。你将需要添加要索引的系统调用名称数组。
- 首先,根据实验前须知阅读 xv6 文档的第 2 章和第 4 章的 4.3 节和 4.4 节以及相关源文件。其中第 2 章讲的是 xv6 系统的组织结构,第 4 章的 4.3 节讲的是调用 system call 的过程,第 4 章的 4.4 节讲的是调用 system call 的参数。与本实验直接相关,所以必须依照源码进行阅读。
- 这里补充一点,做这个实验需要对 xv6 启动过程以及调用系统调用过程有一些了解,具体可以观看上课视频的结尾部分,视频地址:https://www.bilibili.com/video/BV19k4y1C7kA?p=2
作为一个系统调用,我们先要定义一个系统调用的序号。系统调用序号的宏定义在 kernel/syscall.h 文件中。我们在 kernel/syscall.h 添加宏定义,模仿已经存在的系统调用序号的宏定义,我们定义 SYS_trace
如下:
#define SYS_trace 22
查看了一下 user 目录下的文件,发现官方已经给出了用户态的 trace
函数( user/trace.c ),所以我们直接在 user/user.h 文件中声明用户态可以调用 trace
系统调用就好了,但有一个问题,该系统调用的参数和返回值分别是什么类型呢?接下来我们还是得看一看 trace.c 文件,可以看到 trace(atoi(argv[1])) < 0
,即 trace
函数传入的是一个数字,并和 0
进行比较,结合实验提示,我们知道传入的参数类型是 int
,并且由此可以猜测到返回值类型应该是 int
。这样就可以把 trace
这个系统调用加入到内核中声明了:
// system calls
int trace(int);
接下来我们查看 user/usys.pl 文件,这里 perl 语言会自动生成汇编语言 usys.S ,是用户态系统调用接口。所以在 user/usys.pl 文件加入下面的语句:
entry("trace");
如果你编译后查看 usys.S 文件,就能可以看到存在把系统调用号放入 a7
寄存器的指令,然后就直接使用命令 ecall
进入系统内核。不信我们先查看上一次实验编译后的 usys.S 文件,可以看到如下的代码块:
.global fork
fork:
li a7, SYS_fork
ecall
ret
li a7, SYS_fork
指令就是把 SYS_fork
的系统调用号放入 a7
寄存器,使用 ecall
指令进入系统内核。
那么,执行 ecall
指令会跳转到哪里呢?答案是跳转到 kernel/syscall.c 中 syscall
那个函数处,执行此函数。下面是 syscall
函数的源码:
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;
}
}
其中,我们可以看到, num = p->trapframe->a7;
从寄存器 a7
中读取系统调用号,所以上面的 usys.S 文件就是系统调用用户态和内核态的切换接口。接下来是 p->trapframe->a0 = syscalls[num]();
语句,通过调用 syscalls[num]();
函数,把返回值保存在了 a0
寄存器中。我们看看 syscalls[num]();
函数,这个函数在当前文件中。该函数调用了系统调用命令。
static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
...
}
所以我们把新增的 trace
系统调用添加到函数指针数组 *syscalls[]
上:
static uint64 (*syscalls[])(void) = {
...
[SYS_trace] sys_trace,
};
接下来在文件开头给内核态的系统调用 trace
加上声明,在 kernel/syscall.c 加上:
extern uint64 sys_trace(void);
在实现这个函数之前,我们可以看到实验最后要输出每个系统调用函数的调用情况,依照实验说明给的示例,可以知道最后输出的格式如下:
<pid>: syscall <syscall_name> -> <return_value>
其中, <pid>
是进程序号, <syscall_name>
是函数名称, <return_value>
是该系统调用的返回值。注意:冒号和 syscall
中间有个空格,刚开始的时候自己就踩了一个坑。
根据提示,我们的 trace
系统调用应该有一个参数,一个整数“mask(掩码)”,其指定要跟踪的系统调用。所以,我们在 kernel/proc.h 文件的 proc
结构体中,新添加一个变量 mask
,使得每一个进程都有自己的 mask
,即要跟踪的系统调用。
struct proc {
...
int mask; // Mask
};
然后我们就可以在 kernel/sysproc.c 给出 sys_trace
函数的具体实现了,只要把传进来的参数给到现有进程的 mask
就好了:
uint64
sys_trace(void)
{
int mask;
// 取 a0 寄存器中的值返回给 mask
if(argint(0, &mask) < 0)
return -1;
// 把 mask 传给现有进程的 mask
myproc()->mask = mask;
return 0;
}
接下来我们就要把输出功能实现,因为 RISCV 的 C 规范是把返回值放在 a0
中,所以我们只要在调用系统调用时判断是不是 mask
规定的输出函数,如果是就输出。
因为 proc
结构体(见 kernel/proc.h )里的 name
是整个线程的名字,不是函数调用的函数名称,所以我们不能用 p->name
,而要自己定义一个数组,我这里直接在 kernel/syscall.c 中定义了,这里注意系统调用名字一定要按顺序,第一个为空,当然你也可以去掉第一个空字符串,但要记得取值的时候索引要减一,因为这里的系统调用号是从 1
开始的。
static char *syscall_names[23] = {
"", "fork", "exit", "wait", "pipe",
"read", "kill", "exec", "fstat", "chdir",
"dup", "getpid", "sbrk", "sleep", "uptime",
"open", "write", "mknod", "unlink", "link",
"mkdir", "close", "trace"};
然后我们就可以在 kernel/syscall.c 中的 syscall
函数中添加打印调用情况语句。mask
是按位判断的,所以判断使用的是按位运算。进程序号直接通过 p->pid
就可以取到,函数名称需要从我们刚刚定义的数组中获取,即 syscall_names[num]
,其中 num
是从寄存器 a7
中读取的系统调用号,系统调用的返回值就是寄存器 a0
的值了,直接通过 p->trapframe->a0
语句获取即可。注意上面说的那个空格。
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]();
// 下面是添加的部分
if((1 << num) & p->mask) {
printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num], p->trapframe->a0);
}
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
然后在 kernel/proc.c 中 fork
函数调用时,添加子进程复制父进程的 mask
的代码:
int
fork(void)
{
...
pid = np->pid;
np->state = RUNNABLE;
// 子进程复制父进程的 mask
np->mask = p->mask;
...
}
最后在 Makefile
的 UPROGS
中添加:
UPROGS=\
...
$U/_trace\
编译并运行 xv6 进行测试。
$ make qemu
...
init: starting sh
$ trace 32 grep hello README
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0
$
$ trace 2147483647 grep hello README
4: syscall trace -> 0
4: syscall exec -> 3
4: syscall open -> 3
4: syscall read -> 1023
4: syscall read -> 966
4: syscall read -> 70
4: syscall read -> 0
4: syscall close -> 0
$
$ grep hello README
$
$ trace 2 usertests forkforkfork
usertests starting
test forkforkfork: 407: syscall fork -> 408
408: syscall fork -> 409
409: syscall fork -> 410
410: syscall fork -> 411
409: syscall fork -> 412
410: syscall fork -> 413
409: syscall fork -> 414
411: syscall fork -> 415
...
$
退出 xv6 ,运行单元测试检查结果是否正确。
./grade-lab-syscall trace
通过测试样例。
== Test trace 32 grep == trace 32 grep: OK (2.6s)
== Test trace all grep == trace all grep: OK (1.0s)
== Test trace nothing == trace nothing: OK (0.5s)
== Test trace children == trace children: OK (10.8s)