第1节实验描述很长,因为是翻译的,也有点拗口,可以先直接跳到第2小节,然后再回头看,最后再看第3节具体的实现。
推荐阅读顺序:2->1->3
1.实验描述
在本作业中,您将添加一个系统调用跟踪功能,该功能可能会在以后调试实验时对您有所帮助。您将创建一个新的trace系统调用来控制跟踪。它应该有一个参数,这个参数是一个整数“掩码”(mask),它的比特位指定要跟踪的系统调用。例如,要跟踪fork系统调用,程序调用trace(1 << SYS_fork),其中SYS_fork是kernel/syscall.h中的系统调用编号。如果在掩码中设置了系统调用的编号,则必须修改xv6内核,以便在每个系统调用即将返回时打印出一行。该行应该包含进程id、系统调用的名称和返回值;您不需要打印系统调用参数。trace系统调用应启用对调用它的进程及其随后派生的任何子进程的跟踪,但不应影响其他进程。
我们提供了一个用户级程序版本的trace,它运行另一个启用了跟踪的程序(参见user/trace.c)。完成后,您应该看到如下输出:
$ 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
...
$
在上面的第一个例子中,trace调用grep,仅跟踪了read系统调用。32是1<<SYS_read。在第二个示例中,trace在运行grep时跟踪所有系统调用;2147483647将所有31个低位置为1。在第三个示例中,程序没有被跟踪,因此没有打印跟踪输出。在第四个示例中,在usertests中测试的forkforkfork中所有子孙进程的fork系统调用都被追踪。如果程序的行为如上所示,则解决方案是正确的(尽管进程ID可能不同)。
提示
- 在Makefile的UPROGS中添加 $U/_trace
- 运行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指令转换到内核。一旦修复了编译问题(注:如果编译还未通过,尝试先make clean,再执行make qemu),就运行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() 函数以打印跟踪输出。您将需要添加一个系统调用名称数组以建立索引。
2.trace系统调用执行流程
我们先不关心trace的具体功能,而是看看程序执行流程。大致流程如下图所示
- 当shell启动后,我们通过 <命令> [参数]的形式执行一个trace用户程序,如
trace 32 echo hello
,shell就会fork一个子进程并调用exec转去执行trace,32作为参数表示trace要跟踪的系统调用为read(系统调用read在xv-6中的编号为5,1<<5与32相等),echo hello 则是要跟踪的用户程序及它的参数。总的来说,trace要跟踪echo程序执行过程中read系统调用的调用情况。 - trace.c中的工作如图所示,主要代码为:
if (trace(atoi(argv[1])) < 0) {
//trace调用失败,这里的trace只声明在了user/user.h文件中,
//没有具体的实现,怎么对应内核中的sys_trace还不清楚,
//学了后面几章后,对xv-6有了更深的了解,应该就知道了
fprintf(2, "%s: trace failed\n", argv[0]); //2,标准错位的文件描述符
exit(1);
}
for(i = 2; i < argc && i < MAXARG; i++){
nargv[i-2] = argv[i]; //将trace 32去掉后的字符串都存到nargv中
//也就是nargv中现在是echo hello
}
exec(nargv[0], nargv); //转去执行echo hello
- 先不要纠结void trace(int)这个函数了,来看看内核中的系统调用是怎样的吧,系统调用有一个统一的接口
void syscall(void)
,定义在kernel/syscall.c,其主要代码如下:
int num;
struct proc *p = myproc(); //获取保存进程信息的结构体指针
num = p->trapframe->a7; //用num保存获取到的系统调用编号
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) { //系统调用编号合法
// Use num to lookup the system call function for num, call it,
// and store its return value in p->trapframe->a0
p->trapframe->a0 = syscalls[num](); //syscalls是一个存储函数指针的数组,其下标对应系统调用编号,值为系统调用的函数指针
//在这里就执行了相应的系统调用,系统调用定义在/kernel/syspro.c中
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
自此,大致的流程就是这样了,我们做一个小demo来验证一下,实现一个trace系统调用,但它的功能先不实现,只在系统调用内,打印“sys_trace:Hi”
当然也可以通过gdb一步一步的看,就是比较费时间。
- 在用户侧这边:我们需要做这么两件事
- 在user文件夹下,1.首先在user.h中添加一个声明
int trace(int);
,2.然后新建一个trace.c文件,文件内容如下:
- 在user文件夹下,1.首先在user.h中添加一个声明
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc, char * argv[]){
int i;
char *nargv[MAXARG];
if(argc<3 || (argv[1][0] < '0' || argv[1][0] > '9')){
fprintf(2, "Usage: %s mask command\n", argv[0]);
exit(1);
}
if (trace(atoi(argv[1])) < 0) {
fprintf(2, "%s: trace failed\n", argv[0]);
exit(1);
}
for(i = 2; i < argc && i < MAXARG; i++){
nargv[i-2] = argv[i];
}
exec(nargv[0], nargv);
return 0;
}
- 在 Makefile 的
UPROGS
中添加$U/_trace
,在user/usys.pl 中添加entry("trace")
,Makefile会调用perl脚本usys.pl生成系统调用存根(内核里的sys_trace与用户态的trace就是在这里联系起来的)。这一步我们完成的是一些编译准备。 - 在内核侧:我们需要修改kernel/sysproc.c,kernel/syscall.h和kernel/syscall.c三个个文件。
1.在 kernel/sysproc.c 文件中实现sys_trace
,内容如下:
uint64
sys_trace(void)
{
printf("sys_trace:Hi\n");
return 0;
}
2. 在kernel/syscall.h中定义一个SYS_trace宏,作为sys_trace的编号
#define SYS_trace 22
3. 在kernel/syscall.c中要添加系统调用原型
extern uint64 sys_trace(void);
在syscalls数组中添加
[SYS_trace] sys_trace,
至此,我们的代码就修改完成了,运行make qemu
启动系统后,在shell中键入trace 32 echo
执行,得到
sys_trace:Hi
3.实现trace的完整功能
用户侧代码和Makefile都已经在第2节的demo中修改好了,我们只需要在内核侧完成trace的功能。需要修改 /kernel/proc.h,/kernel/proc.c, kernel/sysproc.c和kernel/syscall.c这四个文件。
- 在 /kernel/proc.h 中的
proc结构体
中添加一个成员变量int trace_mask
用于保存trace系统调用的参数,trace_mask
会被进程中的系统调用共享,因为proc结构体是进程控制块; - 在 kernel/sysproc.c的
sys_trace
中将系统调用的参数保存到进程的trace_mask
中
uint64
sys_trace(void)
{
// 获取系统调用的参数并保存到trace_mask中
argint(0, &(myproc()->trace_mask));
//printf("sys_trace:Hi\n");
return 0;
}
3.在kernel/syscall.c的sycall
函数中获取trace_mask
,还记得在上一步中trace_mask
保存了trace的参数吗?然后再通过trace_mask
与系统调用编号进行&操作来判断是不是要跟踪的系统调用,是的话就按要求输出当前进程id,系统调用名称和返回值。
static char * syscall_names[] = {
"fork","exit", "wait", "pipe", "read", "kill", "exec", "fstat", "chdir","dup","getpid",
"sbrk","sleep", "uptime", "open", "write","mknod", "unlink","link","mkdir","close","trace"
};
void
syscall(void)
{
int num;
struct proc *p = myproc();
num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
// Use num to lookup the system call function for num, call it,
// and store its return value in p->trapframe->a0
p->trapframe->a0 = syscalls[num]();
// 系统调用是否匹配
if ((1 << num) & p->trace_mask) //获取mask_trace并判断是不是要跟踪的系统调用
printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num-1], p->trapframe->a0);
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
- 此时make qemu后使用trace能达到实验的主要要求,但是有个bug,不使用trace时,还是会打印,这是因为进程退出的时候没有清空进程中的
trace_mask
;另外fork
的子进程也不会使用trace
,因为trace_mask
还没有复制给子进程。因此我们还需要修改 /kernel/proc.c 中的freeproc
函数和fork
函数(sys_fork
调用这里的fork
),如下:
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
p->pagetable = 0;
p->sz = 0;
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->chan = 0;
p->killed = 0;
p->xstate = 0;
p->state = UNUSED;
//进程退出时,将trace_mask置为0
p->trace_mask = 0;
}
int
fork(void)
{
int i, pid;
struct proc *np;
struct proc *p = myproc();
// Allocate process.
if((np = allocproc()) == 0){
return -1;
}
// Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
np->sz = p->sz;
// copy saved user registers.
*(np->trapframe) = *(p->trapframe);
// Cause fork to return 0 in the child.
np->trapframe->a0 = 0;
//拷贝trace_mask到子进程
np->trace_mask = p->trace_mask;
// increment reference counts on open file descriptors.
for(i = 0; i < NOFILE; i++)
if(p->ofile[i])
np->ofile[i] = filedup(p->ofile[i]);
np->cwd = idup(p->cwd);
safestrcpy(np->name, p->name, sizeof(p->name));
pid = np->pid;
release(&np->lock);
acquire(&wait_lock);
np->parent = p;
release(&wait_lock);
acquire(&np->lock);
np->state = RUNNABLE;
release(&np->lock);
return pid;
}
修改完这四个文件后再make clean
和make qemu
,结果与实验要求一致,就不再赘述了。完整代码参考Lab2_trace这个分支。