MIT 6.S081 2021: Lab system call

System call tracing

这个实验里我们要实现一个trace系统调用,要求是传入一个待追踪的系统调用的掩码,输出所有系统调用的名称和返回值。

首先我们回顾一下,当程序使用系统调用的时候操作系统会怎么做。执行系统调用的时候,操作系统要执行一个trap指令,也就是所谓trap to the kernel,把特权级别提升到kernel mode,这样才能执行特权级指令。系统调用返回的时候,也要执行一个从trap返回的指令(return-from-trap)指令,返回到发起调用的程序中,同时回到user mode。每个用户进程都有user stack和kernel stack。一般的用户代码都是在user stack里执行的,但是系统调用需要跳转到kernel stack里执行。下面这张表详细列出了trap to kernel的整个过程:

MIT 6.S081 2021: Lab system call

xv6操作手册里4.2节介绍了xv6这个操作系统trap to the kernel的全过程,感兴趣可以看一下。
了解了系统调用之后,来看实验给我们的提示。

提示1

提示1是要我们在makefile里加入trace这一项,这没什么好说的。

提示2

提示2说,在user/user.h里添加这个trace函数的原型,这个原型我们设计成

int trace(int trace_mask)

然后在kernel/syscall.h里加入trace的编号,然后在user/usys.pl里面加入这个函数的存根,这个usys.pl在make的时候会生成一个usys.S文件,这个文件就能用ecall执行trap to the kernel。我们来看一下这个usys.S文件,发现每个系统调用名字下面的汇编代码都是一样的。根据xv6手册第4.3节,用户代码是把系统调用编号存到寄存器a7里面然后执行ecall指令。那么显然只要仿照其他系统调用的格式加入trace就可以了:

.global trace
trace:
 li a7, SYS_trace
 ecall
 ret

4.3节里说,ecall执行trap之后,会有一个syscall从a7里取出系统调用编号。看一下这个syscall:

MIT 6.S081 2021: Lab system call


函数从一个结构体里面取出了这个a7并存在num中,然后从uint64 (*)(void)类型的函数指针数组syscalls里取出第num个函数指针并调用它,将它的返回值存在寄存器a0里。也就是说,用户通过一段汇编代码向操作系统内核传系统调用的编号,内核根据编号来在kernel stack里执行对应的sys函数。
那么这个p是什么呢?看一下proc.h里面struct proc的定义就知道了。根据2.5节,struct proc是进程的状态表。也就是说,这个myproc()可以返回一个指向当前进程状态表的struct proc*类型的指针,通过这个指针可以读写当前进程的状态表。

提示3

提示3说,在sysproc.c里加入sys_trace()函数,在proc里新建一个变量,把传给trace()系统调用的参数存到这个变量里。syscall里有很多名为arg.*的函数,根据注释可以判断它们正式用来取出传给系统调用的参数的。我们选择argint(),这个函数可以"Fetch the nth 32-bit system call argument"。所以sys_trace()的思路就很明确了,就是在结构体里新加入一个trace_mask变量,用sys_trace()把用户传进来的参数存进去:

uint64 sys_trace(void)
{
  //取出trace的参数
  int trace_arg;
  if(argint(0,&trace_arg)<0)
  {
    return -1;
  }
  //存到进程proc里面
  struct proc *p = myproc();
  //printf("trace_arg:%d\n",trace_arg);
  p->trace_mask=trace_arg;
  //printf("trace_mask:%d\n",p->trace_mask);
  return 0;
}

为了执行函数,还要记得在syscall.c里把sys_trace加入函数指针数组syscalls,并添加它的extern外部链接。

提示4

提示4说,修改fork()函数,把trace_mask变量传给子进程。看一下proc.c里fork()的代码,新定义了一个struct* proc类型的指针np,还有一个指向当前进程表的指针p。发现这个函数主要就是把p里面的值想方设法赋给np,于是可以确定np代表的是子进程的状态表。因此只需要在进程中加入一行代码:

np->trace_mask=p->trace_mask;

提示5

现在可以修改syscall()函数来打印进程信息了,为了打印进程名,我把所有系统调用的名称按照syscall.h里面的顺序存进一个char* 类型的数组syscall_name中。然后将trace_mask这个掩码和1<<num做或运算(这是掩码的基本操作),不为0就说明当前这个系统调用是trace_mask里指定过的,需要打印。

void
syscall(void)
{
  int num;
  struct proc *p = myproc();
  static int flag=0;
  num = p->trapframe->a7;
  //printf("%d ",num);
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) 
  {
    p->trapframe->a0 = syscalls[num]();
    if(num==SYS_trace)
    {
      //printf("%d",num);
      flag=1;
    }
    if(flag)
    {
      if((1<<num)&(p->trace_mask))
      {
        //printf("num:%d p->trace_mask:%d &:%d ",num,p->trace_mask,num&(p->trace_mask));
        printf("%d: syscall %s -> %d\n",p->pid,syscall_name[num],p->trapframe->a0);
      }
    }  
  } 
  else 
  {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

由于现在还不知道proc这个结构是怎么初始化的,在没有调用trace()之前trace_mask是未知的,严谨起见,可以增设一个静态变量flag=0,只在第一次调用trace()时初始化一次flag=1,当flag为1时才允许比对num和trace_mask。

Sysinfo

这个要求输出进程信息。用户把一个指向struct sysinfo的指针info传给sysinfo(),sysinfo()负责把它指向的内存空间填满。和上一题一样,也是按照提示步骤进行。

提示1,提示2和上一题一样,还是在user.h等文件中加入函数声明、系统调用号的宏等等。从提示3开始:

提示3

思路很明显,就是创建一个struct sysinfo,写入其中的两个参数,然后用copyout写入info。稍后我们来讨论怎么获取参数。注意要在文件开头#include “sysinfo.h”。

uint64 sys_sysinfo(void)
{
  struct sysinfo info_tmp;
  
  //准备存数据
  //存进程的统计数据
  info_tmp.nproc=collect_process_unused();
  //存内存数据
  info_tmp.freemem=free_mem_count();

  uint64 user_info_ptr; // user pointer to struct sysinfo

  if(argaddr(0, &user_info_ptr) < 0)//先取得用户传过来的指针
    return -1;
  
  //得到的结果写回
  struct proc *p = myproc();
  if(copyout(p->pagetable, user_info_ptr, (char *)&info_tmp, sizeof(info_tmp)) < 0)
    return -1;
  return 0;
}

根据提示观察sysfile.c里面的sys_fstat()函数和file.c里面的fstat()函数:sys_fstat抓取了传入的文件描述符fd和传入的struct stat类型指针st,并调用fstat();fstat()又声明了一个struct stat st,明显是用一个stati(f->ip, &st)把一些东西存到st里,然后用copyout做写操作。copyout参数1是页表,参数2是待写入指针,参数3是指向待写数据的指针(明显是要逐字节写入),参数4是写入大小。在trace()里模仿一下就行了。

提示4

现在实现上面代码里的free_mem_count()。根据手册,xv6分配从内核末尾到PHYSTOP的内存,一次分配一整页(4096B),使用一个linked list来管理空闲内存。下图是linked list的示例:

MIT 6.S081 2021: Lab system call

根据提示,我们来看kalloc.c。

MIT 6.S081 2021: Lab system call


这是一个很明显的链表结构,匿名结构kmem中的freelist指向linked list的开头,next指向下一个空闲页面。(这个struct run结构是直接存在空页面里面的)。因此遍历这个链表即可得到总空闲字节数:

uint64 free_mem_count(void)
{
  struct run *r;
  acquire(&kmem.lock);
  r = kmem.freelist;
  uint64 total_free_mem=0;
  while(r)
  {
    total_free_mem+=PGSIZE;
    r=r->next;

  }
  release(&kmem.lock);

  return total_free_mem;
  
}

注意文档里提到了free list是被一个spin lock保护的,因此要仿照上面的kalloc()做一个PV操作,把遍历链表放在PV之间。另外要注意把free_mem_count函数的声明加入到defs.h中。

提示5

现在实现collect_process_unused()。struct proc里有一个enum类型的变量指示了进程状态。观察到proc.h里面的struct proc proc[NPROC],这明显是存储所有进程的数组。直接遍历:

uint64 collect_process_unused(void)
{
  struct proc *p;
  uint64 total=0;
  for(p = proc; p < &proc[NPROC]; p++)
  {
    if(p->state != UNUSED)
    {
      total++;
    }
  }
  return total;
}

 

上一篇:学习分布式一致性协议:自己实现一个Raft算法


下一篇:死磕Tomcat系列(3)——Tomcat如何做到一键式启停的