0%

MIT6.S081 Lab Syscall

开始日期:22.3.11

操作系统:Ubuntu20.0.4

Link:Lab Syscall

my github repository: duilec/MITS6.081-fall2021/tree/syscall

Lab Syscall

写在前面

遇到的问题或bug

  • 如何在虚拟机中调试(debug),需要打开两个终端,过程中需要打开两个终端,一个用来启动qemu,一个用来正常debug。(课程建议debug时,启动qemu只用一个cpu)

    1
    2
    3
    4
    5
    one windows
    $ make CPUS=1 qemu-gdb
    another windows
    $ gdb-multiarch kernel/kernel
    $ target remote localhost:26000
  • 没有切换分支到syscall lab,导致trace相关无法运行(因为只有切换到syscall分支之后才会有相关文件)
    官网lab中其实已经提示了,在整个实验开始前必须完全分支切换

    1
    2
    3
    $ git fetch
    $ git checkout syscall
    $ make clean
  • Timeout! trace children超时!

    • 超时bug如下所示,原因是电脑性能不够强

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      == Test trace children == 
      $ make qemu-gdb
      Timeout! trace children: FAIL (30.2s)
      ...
      9: syscall fork -> 56
      6: syscall fork -> -1
      7: syscall fork -> -1
      8: syscall fork -> -1
      qemu-system-riscv64: terminating on signal 15 from pid 5581 (make)
      MISSING '^ALL TESTS PASSED'
      QEMU output saved to xv6.out.trace_children
    • 解决方案,在.py文件gradelib.py中改变超时判断时间30s为50s或更长时间(在428行)

      • timeout=30改为timeout=50或更长时间
      1
      2
      3
      428 def run_qemu_kw(target_base="qemu", make_args=[], timeout=50):
      429 return target_base, make_args, timeout
      430 target_base, make_args, timeout = run_qemu_kw(**kw)

学到的知识

  • spinlock vs mutex

  • 系统调用函数的添加流程一开始可能读不懂,可以先阅读后面的实验内容同时多看看材料

    • user/user.h,添加用户调用声明(prototype)

    • user/usys.pl,添加存根(stub)到脚本文件usys.pl

      • 可以看到其作用与压栈相关(打印了汇编代码)

        1
        2
        3
        4
        5
        6
        7
        8
        9
        /* usys.pl */
        sub entry {
        my $name = shift;
        print ".global $name\n";
        print "${name}:\n";
        print " li a7, SYS_${name}\n";
        print " ecall\n";
        print " ret\n";
        }
      • 以系统调用函数trace为例,事实上是要调用时把SYS_trace(trace的系统调用编号)压入到寄存器a7当中,然后调用ecall进入kernel

        1
        2
        3
        4
        /* usys.S */
        li a7, SYS_trace
        ecall
        ret
    • kernel/syscall.h,添加系统调用编号(syscall number)

    • kernel/syscall.c,添加系统调用编号对应的系统调用函数系统函数外部调用声明以及系统调用编号对应的函数名字

      • 第一个:系统调用编号对应的系统调用函数,听起来有点绕口,其实这条添加的内容是存放在函数指针表static uint64 (*syscalls[])(void)中的,该表的功能是:根据系统调用编号,找到并调用对应的函数
      • 第二个:为了能让static uint64 (*syscalls[])(void)根据系统调用编号,找到并调用对应的函数,因为这些函数存放的位置都不统一,只能用外部调用的方式来声明
      • 第三个:系统调用编号对应的函数名字,就是调用函数的名字,用来给syscall.c跟踪打印

实验内容

System call tracing (moderate)

  • 任务:实现系统调用跟踪

  • 功能:跟踪一个或多个系统调用进程,打印该进程的pid,名字和该进程返回值return value

    The line should contain the process id, the name of the system call and the return value;

    eg. 3: syscall read -> 1023

    推出:<pid>: syscall <syscall_name> -> <return_value>

  • 注意不同进程的返回值不同,需要注意的是fork()的返回值可以恰好是pid

  • 按照提示(hints)一步步走即可。

    • Add $U/_trace to UPROGS in Makefile

    • 系统调用函数的添加流程,共四次添加,但提示只给到了三次添加,事实上,要结合最后一步才能更好的理解为什么要第四次添加,可以先不添加,看到最后一步再添加

      • trace.c已给出,它需要一个int参数

      • /* syscall.c */缺了系统调用编号对应的函数名字的添加,最后一步会提到

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        /* user/user.h */
        // syscall.h
        int fork(void);
        ...
        int trace(int); /* ADD */

        /* user/usys.pl */
        entry("fork");
        ...
        entry("trace"); /* ADD */

        /* kernel/syscall.h */
        // System call numbers
        #define SYS_fork 1
        ...
        #define SYS_trace 22 /* ADD */

        /* kernel/syscall.c */
        extern uint64 sys_chdir(void);
        ...
        extern uint64 sys_trace(void); /* ADD */

        static uint64 (*syscalls[])(void) = {
        [SYS_fork] sys_fork,
        ...
        [SYS_trace] sys_trace, /* ADD */
    • kernel/sysproc.c中编写sys_trace(),它的功能是获得该程序的mask,编写时主要参考kernel/proc.h,kernel/syscall.c,kernel/sysproc.c中的部分内容

      • 我们需要使用mask,mask是用来检查当前系统函数和用户所要跟踪的系统函数mask是否对应,如果是,才打印跟踪内容。eg. trace 32 grep hello README,其中321 << SYS_read1 << 5,需要跟踪read

      • 如何使用mask来判断是最后一步(修改syscall())的内容,这里先不提

      • 我们先要解决如何获得mask的问题

        • 首先,每个进程都有一个其相关信息的结构体,我们在这个结构体当中添加多一条mask

          1
          2
          3
          4
          5
          6
          7
          8
          /* kernel/proc.h */
          // Per-process state
          struct proc {
          struct spinlock lock;

          // these are private to the process, so p->lock need not be held.
          ...
          int mask; // mask for check and trace
        • 然后,编写sys_trace()获得该程序的mask,这个mask其实是我们一开始给的参数(在trace.c中放入),它存放在寄存器a0当中,所以我们要调用0模式argint(0, &n)),具体内容要看4.3和4.4

        • eg. trace 32 grep hello README,其中321 << SYS_read1 << 5,它就存放在a0当中。

        • 当使用myproc()->mask = n时,是对当前进程的mask赋值

          • eg. trace 32 grep hello README,该语句就是一个进程,包含了多个系统函数,但我们只跟踪read()

            1
            2
            3
            4
            5
            $ trace 32 grep hello README
            3: syscall read -> 1023
            3: syscall read -> 966
            3: syscall read -> 70
            3: syscall read -> 0
          • eg. trace 2147483647 grep hello README,该语句就是一个进程,该进程包含了多个系统函数,同时,全部调用到的系统函数我们都跟踪

            1
            2
            3
            4
            5
            6
            7
            8
            9
            $ 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
        • 参考代码:

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          uint64
          sys_trace(void)
          {
          int n;
          /* get mask by trap (mask in a0 of trapframe)*/
          if(argint(0, &n) < 0)
          return -1;
          myproc()->mask = n;
          return 0;
          }
        • 需要理解的argint()argraw()以及user.c/trace.c

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          27
          28
          29
          static uint64
          argraw(int n)
          {
          struct proc *p = myproc();
          switch (n) {
          case 0:
          return p->trapframe->a0;
          case 1:
          return p->trapframe->a1;
          case 2:
          return p->trapframe->a2;
          case 3:
          return p->trapframe->a3;
          case 4:
          return p->trapframe->a4;
          case 5:
          return p->trapframe->a5;
          }
          panic("argraw");
          return -1;
          }

          // Fetch the nth 32-bit system call argument.
          int
          argint(int n, int *ip)
          {
          *ip = argraw(n);
          return 0;
          }
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          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);
          exit(0);
          }
    • 为了通过trace children,即子程序也能被跟踪,需要把父程序的mask复制到子程序当中,很简单,在fork()中添加语句即可。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      /* kernel/proc.c */
      int
      fork(void)
      {
      ...
      // copy saved user registers.
      *(np->trapframe) = *(p->trapframe);

      // copy saved mask for trace.
      np->mask = p->mask;
      ...
    • 最后一步,我们修改syscall()满足功能:打印被跟踪进程的pid,名字和该进程返回值return value

      • 首先,需要在syscall.c中多添加trace的系统外部调用声明以及trace的系统调用编号对应的系统函数调用,为了能够跳转并执行sys_trace(),获得它的返回值

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        /* kernel/syscall.c */
        extern uint64 sys_chdir(void);
        ...
        extern uint64 sys_trace(void);

        static uint64 (*syscalls[])(void) = {
        [SYS_fork] sys_fork,
        ...
        [SYS_trace] sys_trace,
        };
      • 其次,获得调用系统函数名字,我们需要一个指针数组,以便syscall()使用

        You will need to add an array of syscall names to index into.

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        // an array of syscall names to index into
        static char *syscall_name[] = {
        [SYS_fork] "fork",
        [SYS_exit] "exit",
        [SYS_wait] "wait",
        [SYS_pipe] "pipe",
        [SYS_read] "read",
        [SYS_kill] "kill",
        [SYS_exec] "exec",
        [SYS_fstat] "stat",
        [SYS_chdir] "chdir",
        [SYS_dup] "dup",
        [SYS_getpid] "getpid",
        [SYS_sbrk] "sbrk",
        [SYS_sleep] "sleep",
        [SYS_uptime] "uptime",
        [SYS_open] "open",
        [SYS_write] "write",
        [SYS_mknod] "mknod",
        [SYS_unlink] "unlink",
        [SYS_link] "link",
        [SYS_mkdir] "mkdir",
        [SYS_close] "close",
        [SYS_trace] "trace",
        };
      • 最后,修改syscall()

        • 寄存器a0本身被约定用来存放返回值(c-style),打印出来即可;寄存器a7是用来存放SYS_<syscall_name>的数值的
          位移处理后,我们用来和当前进程的mask比较,从而检查当前系统函数和用户所要跟踪的系统函数mask是否对应
          &操作,我们可以检查一个或多个系统函数调用。
        • eg. trace 32 grep hello README,其中321 << SYS_read1 << 5,需要跟踪是系统函数read
        • 注意,要先判断是不是系统函数,再判断是不是需要跟踪的系统函数
        • 我们实际上是用syscall()来打印跟踪内容,trace()只是用来传递mask的

        参考代码:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        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]();
        // check mask, if OK print
        if(p->mask & (1 << num))
        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;
        }
        }
  • 参考链接

Sysinfo (moderate)

  • 任务:实现一个系统函数sysinfo()

  • 功能:收集正在运行的系统信息,包括freemem(空闲内存的大小)和nproc(运行程序的数量:即程序状态不是 UNUSED

  • 按照提示写

    • Add $U/_sysinfotest to UPROGS in Makefile,注意添加的是**$U/_sysinfotest** , 不是$U/_sysinfo

    • 系统函数添加流程

      • 这里要格外添加一个结构体sysinfo,它就是系统信息,包含了freemem和nproc

        1
        2
        3
        4
        5
        /* kernel/sysinfo.h */
        struct sysinfo {
        uint64 freemem; // amount of free memory (bytes)
        uint64 nproc; // number of process
        };
      • 添加

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        /* user/user.h */
        struct stat;
        ...
        struct sysinfo;
        // syscall.h
        int fork(void);
        ...
        int sysinfo(struct sysinfo *); /* ADD */

        /* user/usys.pl */
        entry("fork");
        ...
        entry("sysinfo"); /* ADD */

        /* kernel/syscall.h */
        // System call numbers
        #define SYS_fork 1
        ...
        #define SYS_sysinfo 23 /* ADD */

        /* kernel/syscall.c */
        extern uint64 sys_chdir(void);
        ...
        extern uint64 sys_sysinfo(void); /* ADD */

        static uint64 (*syscalls[])(void) = {
        [SYS_fork] sys_fork,
        ...
        [SYS_sysinfo] sys_sysinfo, /* ADD */

        // an array of syscall names to index into
        static char *syscall_name[] = {
        [SYS_fork] "fork",
        ...
        [SYS_sysinfo] "sys_sysinfo",
        };
    • 参考filestat() (kernel/file.c),sys_fstat() (kernel/sysfile.c) 以及copyout(),编写sys_sysinfo(),从内核中获取freemem和nproc,输出给用户

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      uint64
      sys_sysinfo(void)
      {
      struct proc *p = myproc();
      struct sysinfo info;
      uint64 addr;

      /* get VA(virtual address)*/
      if(argaddr(0, &addr) < 0)
      return -1;

      /* get info*/
      info.freemem = get_amount_freemem();
      info.nproc = get_nproc();

      /* Copy len bytes from src(info) to virtual address dstva(addr) in a given page table. */
      if(copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0)
      return -1;
      return 0;
      }
    • 接下来,就是要编写 get_amount_freemem()get_nproc()从而获得freememnproc

      • 注意写完之后要在defs.h中声明

        1
        2
        3
        4
        5
        6
        7
        8
        /* kernel/defs.h */
        // kalloc.c
        ...
        uint64 get_amount_freemem(void);

        // proc.c
        ...
        uint64 get_nproc(void);
    • get_amount_freemem()

      • 主要参考kalloc.c,我们可以看到内核的内存是如何初始化,如何分配,从kfree()中可以看到kmem.freelist一直是指向空闲内存,它是一个空闲链表,而且是倒着组装的,我们只要遍历即可获得空闲内存的总量(一个链表的结点就是一个页表,一个页表的字节数为PGSIZE4096),但是它没有初始化指向

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        struct run {
        struct run *next;
        };

        struct {
        struct spinlock lock;
        struct run *freelist;
        } kmem;

        void
        kinit()
        {
        initlock(&kmem.lock, "kmem");
        freerange(end, (void*)PHYSTOP);
        }

        void
        freerange(void *pa_start, void *pa_end)
        {
        char *p;
        p = (char*)PGROUNDUP((uint64)pa_start);
        for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
        kfree(p);
        }

        // Free the page of physical memory pointed at by v,
        // which normally should have been returned by a
        // call to kalloc(). (The exception is when
        // initializing the allocator; see kinit above.)
        void
        kfree(void *pa)
        {
        struct run *r;

        if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
        panic("kfree");

        // Fill with junk to catch dangling refs.
        memset(pa, 1, PGSIZE);

        r = (struct run*)pa;

        acquire(&kmem.lock);
        r->next = kmem.freelist;
        kmem.freelist = r;
        release(&kmem.lock);
        }

        // Allocate one 4096-byte page of physical memory.
        // Returns a pointer that the kernel can use.
        // Returns 0 if the memory cannot be allocated.
        void *
        kalloc(void)
        {
        struct run *r;

        acquire(&kmem.lock);
        r = kmem.freelist;
        if(r)
        kmem.freelist = r->next;
        release(&kmem.lock);

        if(r)
        memset((char*)r, 5, PGSIZE); // fill with junk
        return (void*)r;
        }
      • 参考kalloc(void)来写,这里没有使用自旋锁,因为没有更改kmem.freelist的指向,只是简单的查看。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        uint64
        get_amount_freemem()
        {
        uint64 num_page = 0;
        struct run *page = kmem.freelist;
        while(page){
        /* count for number of page */
        num_page++;
        /* perare next page */
        page = page->next;
        }
        return PGSIZE * num_page;
        }
    • get_nproc()

      • 这个编写很简单,多线程的state访问,需要用自旋锁保护

      • 参考proc.c的内容

        1
        2
        3
        4
        5
        6
        7
        8
        9
        enum procstate { UNUSED, USED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };

        // Per-process state
        struct proc {
        struct spinlock lock;

        // p->lock must be held when using these:
        enum procstate state; // Process state
        ...
      • 统计状态不是UNUSED的线程即可

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        uint64
        get_nproc(void)
        {
        uint64 nproc = 0;
        struct proc *p;
        /* use spinlock to avoid race(accessing 'nproc') between different threads */
        for(p = &proc[0]; p < &proc[NPROC]; p++){
        acquire(&p->lock);
        if(p->state != UNUSED)
        nproc++;
        release(&p->lock);
        }
        return nproc;
        }
  • 参考链接

总结

  • 完成日期22.3.21
  • sysinfo笔者一开始误把 p < proc[NPROC]写成了p < p[NPROC] ,找了好几个小时这个bug,是抄procdump()的时候抄错了 = ^ =
  • 笔者一开始把系统函数和进程弄混了,误认为一个系统函数就是一个进程,事实上,一个进程一般都会调用多个系统函数
  • 倒着组装空闲链表,需要不断地改变头指针(即freelist)的指向
  • 艾尔登法环真好玩!
  • 修改了一些错误22.8.06