MIT6.S081-Lab2 Syscall [2021Fall]


開始日期:22.3.11

操作系統:Ubuntu20.0.4

Link:Lab Syscall

個人博客:Memory Dot

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

Lab Syscall

寫在前面

遇到的問題或bug

  • 如何在虛擬機中調試(debug),需要打開兩個終端,過程中需要打開兩個終端,一個用來啟動qemu,一個用來正常debug。(課程建議debug時,啟動qemu只用一個cpu)

    one windows
    $ make CPUS=1 qemu-gdb
    another windows
    $ gdb-multiarch kernel/kernel
    $ target remote localhost:26000
    
  • 沒有切換分支到syscall lab,導致trace相關無法運行(因為只有切換到syscall分支之后才會有相關文件)
    官網lab中其實已經提示了,在整個實驗開始前必須完全分支切換

      $ git fetch
      $ git checkout syscall
      $ make clean
    
  • Timeout! trace children超時!

    • 超時bug如下所示,原因是電腦性能不夠強

      == 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或更長時間
      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

      • 可以看到其作用與壓棧相關(打印了匯編代碼)

        /* 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

         /* 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 */缺了系統調用編號對應的函數名字的添加,最后一步會提到

        /* 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

          /* 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()

            $ 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,該語句就是一個進程,該進程包含了多個系統函數,同時,全部調用到的系統函數我們都跟蹤

            $ 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
            
        • 參考代碼:

          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

          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;
          }
          
          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()中添加語句即可。

      /* 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(),獲得它的返回值

        /* 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.

        // 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的

        參考代碼:

        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

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

        /* 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,輸出給用戶

      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中聲明

        /* 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),但是它沒有初始化指向

        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的指向,只是簡單的查看。

        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的內容

        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的線程即可

        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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM