第二章 实验:实现trace系统调用

第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可能不同)。
提示

  • MakefileUPROGS中添加 $U/_trace
  • 运行make qemu,您将看到编译器无法编译user/trace.c,因为系统调用的用户空间存根还不存在:将系统调用的原型添加到user/user.h,存根添加到user/usys.pl,以及将系统调用编号添加到kernel/syscall.hMakefile调用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的具体功能,而是看看程序执行流程。大致流程如下图所示
Lab2_trace

  1. 当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系统调用的调用情况
  2. 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
  1. 先不要纠结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文件,文件内容如下:
#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;
}
  • MakefileUPROGS中添加$U/_trace ,在user/usys.pl 中添加entry("trace")Makefile会调用perl脚本usys.pl生成系统调用存根(内核里的sys_trace与用户态的trace就是在这里联系起来的)。这一步我们完成的是一些编译准备。
  • 在内核侧:我们需要修改kernel/sysproc.ckernel/syscall.hkernel/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.ckernel/sysproc.ckernel/syscall.c这四个文件。

  1. /kernel/proc.h 中的proc结构体中添加一个成员变量int trace_mask用于保存trace系统调用的参数,trace_mask会被进程中的系统调用共享,因为proc结构体是进程控制块;
  2. kernel/sysproc.csys_trace中将系统调用的参数保存到进程的trace_mask
uint64
sys_trace(void)
{
  // 获取系统调用的参数并保存到trace_mask中
  argint(0, &(myproc()->trace_mask));
  //printf("sys_trace:Hi\n");
  return 0;
}

3.在kernel/syscall.csycall函数中获取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;
  }
}
  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 cleanmake qemu,结果与实验要求一致,就不再赘述了。完整代码参考Lab2_trace这个分支

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值