在上一个实验中,我们使用操作系统提供的各类系统调用完成了复杂的任务。在 Lab System calls 的实验中,我们会深入操作系统的内核,为 xv6 的系统调用添加一些功能,乃至添加新的系统调用。

在开始之前,将代码仓库切换到 syscall 分支

2.1 追踪系统调用

为了方便对系统调用进行 debug ,我们引入一个新的系统调用 trace ,用以追踪(打印)用户程序使用系统调用的情况。该系统调用接受一个参数,被称为 mask(掩码),用以设置被追踪的系统调用:例如,若我们想要追踪 fork 系统调用,则需要调用 trace(1 << SYS_fork) ,其中SYS_fork 是 fork 系统调用的编号。该进程调用过 trace(1 << SYS_fork) 后,如果该进程后续调用了 fork 系统调用,调用 fork 时内核则会打印形如 <pid>: syscall fork -> <ret_value>的信息(但无需打印传入系统调用的参数)。

<aside> 💡 "1 << SYS_fork" 是什么意思?

"1 << SYS_fork" 实际上是将数字 1 左移 SYS_fork 位,得到一个二进制数,其中只有第 SYS_fork 位是 1,其余位都是 0。这个二进制数可以用作一个位掩码(bitmask),用于标记需要跟踪的系统调用。

</aside>

该实验提供了一个用户态程序用来方便我们测试 trace 功能,该用户态程序的源码在 user/trace.c 。如果我们正确实现了 trace 系统调用,则使用 trace 用户程序的情况如下面的例子所示:

Untitled

<aside> 💡 上面这串命令是什么意思?

这个实验需要我们着手修改 xv6 的内核,但在修改内核之前,我们需要先将 $U/_trace 加入到 Makefile 中的 UPROGS 环境变量中(已经存在已经写好的 user/trace.c ,可以去看一下)。此时如果直接运行 make qemu ,则会无法通过编译,原因时我们还没有将 trace 系统调用加到 xv6 的用户态库和内核中。按照下面的步骤将 trace 系统调用加入到户态库和内核中(具体的原理将在下文的小结中讨论):

  1. 在 user/user.h 中的系统调用部分加入一行 int trace(int); 作为 trace 系统调用在用户态的入口。其中 user/trace.c 已经帮我们写好trace的用户态程序,其中调用了系统调用trace
  2. 在 user/usys.pl 中的系统调用部分加入一行 entry("trace"); 该代码会生成用于调用 trace(int) 系统调用的汇编代码(也称为 stubs),以便在用户态下执行该系统调用。
  3. 在 kernel/syscall.h 中 #define SYS_trace 22 为 trace 分配一个系统调用的编号。同时定义 #define SYS_CALL_NUM 22 记录总数,之后会用到

完成上述的工作后,应当能够通过 make qemu 的编译,并且进入系统。由于我们还没有真正实现 trace 系统调用,故而如果我们在 xv6 的终端中执行 trace 32 grep hello README,进程会因为系统报 unknown sys call 22 而被终止。

接下来我们可以开始真正实现 trace 系统调用了。首先,为了保存每个进程的 trace 的参 数,我们需要在进程控制块中加入一个新的变量。查看 kernel/proc.h ,找到进程的数据结 构 struct proc,在其中加入一行 int tracemask;

为了将 trace(int) 的参数保存到 struct proc 中,我们需要在 kernel/sysproc.c 中实现 sys_trace(void) 函数,在 xv6 操作系统中,sysproc.c 是用于实现系统调用的文件之一。它包含了一些常用的系统调用,例如 fork、exec、wait 等等。当用户程序调用这些系统调用时,内核会根据系统调用号来调用相应的系统调用实现函数。代码如下:

uint64
sys_trace(void)
{
  int mask;
  argint(0, &mask); // 从用户空间获取 trace 参数(用户态的trace函数帮我们处理好了)
  myproc()->tracemask = mask; // 将 trace 参数保存到当前进程的进程控制块中
  return 0; // 返回 0 表示系统调用执行成功
}

该函数使用了 argint 函数从用户空间获取 trace 参数。argint 函数是一个辅助函数,用于从用户空间获取一个整数参数,并将其存储到指定的变量中。在这里,它用于从用户空间获取 trace 参数,并将其存储到 mask 变量中。

接下来,该函数使用 myproc 函数获取当前进程的进程控制块,并将 trace 参数保存到该进程控制块的 tracemask 变量中。这样,我们就可以在需要时使用 tracemask 变量来判断当前进程是否需要启用 trace 功能。

然后我们需要修改 kernel/syscall.c 中的 syscall(void) ,使其能够根据 tracemask 打印所需要的信息。首先定义一个字符串常量数组,保存各系统调用的名称,以下是示例,按照这样的把所有系统调用都写一遍就好啦: