• MIT 6.s081操作系统实验 Lab2: system calls



    实验链接

    1 System call tracing

    写一个系统调用追踪器,作用是对于指定的系统调用号,对进程号,系统调用名称,返回值进行打印。

    1.1 主要思路

    1. 每个系统调用都需要经过syscall函数,此函数从进程的trapframe取得将要进行调用的系统调用号num,因此思路就是在进行系统调用后,若num是否在进程指定的调用号中,则将以上信息进行打印。
    2. 完成trace系统调用:在1.中需要知道进程要追踪的指定系统调用号是什么,因此trace系统调用就是来指定系统调用号的,因此对于每个进程,为进程结构体中新增属性mask指定系统调用号集合,并且还需要在fork出子进程时,让子进程继承此属性。
      //kernel/proc.h
      struct proc {
        int pid;                     // Process ID
        ....
        // mask that expresses which syscalls will be traced.
        int mask;
      };
      //kernel/proc.c
      int
      fork(void){
        .....
        np->mask = p->mask;
      }
      // kernel/sysproc.c
      uint64
      sys_trace(void){
        int mask;
        if(argint(0, &mask) < 0)
          return -1;
        myproc()->mask = mask;
        return 0;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22

    1.2 系统调用流程

    根据Lab的介绍,系统调用的流程如下:

    • 系统调用函数需要被用户程序所感知,因此在/user/user.h中添加系统调用的方法int trace(int);,以供给用户调用
    • user/usys.pl脚本类似其他系统调用添加entry("trace");
      //user/usys.pl文件
      #!/usr/bin/perl -w
      print "# generated by usys.pl - do not edit\n";
      print "#include \"kernel/syscall.h\"\n";
      sub entry {
          my $name = shift;
          print ".global $name\n";
          print "${name}:\n";
          print " li a7, SYS_${name}\n";
          print " ecall\n";
          print " ret\n";
      }
      entry("fork");
      ...
      entry("trace");
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
    • Makefile会执行此脚本,生成user/usys.S
      //Makefile文件
      $U/usys.S : $U/usys.pl
      	perl $U/usys.pl > $U/usys.S
      
      • 1
      • 2
      • 3
    • user/usys.S汇编代码中,用户调用trace,实际上会通过ecall跳转到真正的系统调用,具体的实现很简单,就是为1个a7寄存器赋值一个在头文件define好的系统调用号。
      # generated by usys.pl - do not edit
      #include "kernel/syscall.h"
      .....
      .global trace
      trace:
       li a7, SYS_trace
       ecall
       ret
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
    • 看到user/usys.S中,include了"kernel/syscall.h"的头文件,此头文件只有一些define值,因此以上汇编句,其实等价于li a7, 22,即a7 = 22,进行了一下寄存器赋值
      // System call numbers
      ...
      #define SYS_close  21
      #define SYS_trace  22
      
      • 1
      • 2
      • 3
      • 4
    • 22实际上是在kernel/syscall.c中的一个系统调用函数数组的下标,在系统调用时,进程取到寄存器a7的值作为系统调用函数数组的下标,至此终于真的调用到了这个函数,当然,最后我们还需要在kernel/sysproc.c中实现对应的函数。
      //kernel/syscall.c
      ........
      extern uint64 sys_trace(void);
      extern uint64 sys_sysinfo(void);
      static uint64 (*syscalls[])(void) = {
      .........
      [SYS_trace]   sys_trace,
      [SYS_sysinfo] sys_sysinfo,
      };
      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](); //※
        } ......
        if((p->mask >> num) & 1)
          printf("%d: syscall %s -> %d\n", p->pid, sys_call_name[num], p->trapframe->a0);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22

    2 Sysinfo

    新增一个系统调用Sysinfo,传入的参数为以下结构体的指针,内核会将结构体填满:freemem表示空闲内存的字节数,nproc表示状态为UNUSED的进程数。

    struct sysinfo {
      uint64 freemem;   // amount of free memory (bytes)
      uint64 nproc;     // number of process
    };
    
    • 1
    • 2
    • 3
    • 4

    提示1:sysinfo需要将数据复制到用户态,需要使用copy_out函数,可参考sys_fstat() (kernel/sysfile.c) filestat() (kernel/file.c)函数。
    提示2:在kernel/kalloc.c中新增统计空闲内存的函数,在kernel/proc.c中新增统计空闲进程的函数。

    2.1 kernel/kalloc.c 此文件用于实现分配物理空间的函数

    主要是定义了一个空闲链表用来管空闲内存,包括初始化,内存回收,内存分配,以下是具体函数功能介绍,非常非常简单。

    2.1.1 结构体定义

    首先是定义了1个run的极简链表,用来串起空闲内存,其次是kmem结构体,用来放1个锁,还有空闲内存链表的表头。

    struct run {
      struct run *next;
    };
    
    struct {
      struct spinlock lock;
      struct run *freelist;
    } kmem;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.1.2 空闲链表初始化

    • kinit 此函数用于空闲链表初始化
      先使用initlock初始化锁,然后使用freerange指定初始化的内存范围是从endPHYSTOP

      extern char end[]; // first address after kernel.
                         // defined by kernel.ld.
      extern char end[]; // first address after kernel.
                         // defined by kernel.ld.
      void
      kinit()
      {
        initlock(&kmem.lock, "kmem");
        freerange(end, (void*)PHYSTOP);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
    • freerange 此函数用于将连续长度的内存空间释放掉
      释放的方法是使用kfree逐页(一页PGSIZE为4096B)将首地址插入到空闲链表中

      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);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

    2.1.3 内存的分配和释放

    • kfree 此函数将1个物理页的首地址存放到空闲链表中
      实现上:
      • 先判断地址是否越界
      • 不越界则先将该页填满数据垃圾,避免数据泄露
      • 进行隐式链接:直接在该地址创建1个空闲链表节点,采用头插的方法将该节点作为新的空闲链表节点。这种链接方法为隐式链接,即直接在空闲的地址创建节点,这样的好处是空闲节点不用额外占据空间,可节约空间;坏处是隐式链接的稳定性较差,若某个链接断开或错漏,则整个链表失效。注意在链接前需要获得锁,在链接后释放锁,因为空闲链表是临界资源,同一时间只能由1个进程访问。
      // 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);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
    • kalloc 此函数将空闲链表中的一页分配出去
      // 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;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18

    2.1.4 获取空闲内存字节数

    • 自实现get_freemem,用于返回当前内存空闲字节数
      根据以上分析,显然内存空闲字节数就是空闲链表的节点数量乘上每个物理页的大小,注意在访问空闲链表的时候需要加锁,访问结束后释放锁,因为可能出现以下场景:
      • 进程A调用sysinfo,并获取了kmem.freelist
      • 进程B调用malloc,并触发了kalloc,若未加锁,则kalloc将链表头部的节点kmem.freelist分配给进程B
      • 进程A尝试通过run链表的next指针尝试去获取下一个节点,但kmem.freelist已经给进程B在使用,此地址不再存放之前的next地址,而是进程B的私有数据,不应该再在此时访问,这样会导致数据泄露,破坏内存的安全性,并且next在此语义下仍是一个地址,可能会出现访问越界等情况。
      uint64 get_freemem(){
        uint64 freemem = 0;
        acquire(&kmem.lock);
        struct run *p= kmem.freelist;
        while(p){
          freemem += PGSIZE;
          p = p->next;
        }
        release(&kmem.lock);
        return freemem;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

    2.2 kernel/proc.c 此文件用于进程管理

    因为这个文件的体量较为庞大,后续还有和进程管理更强相关的如Lab thread: Multithreading实现多线程,Lab lock: Parallelism/locking实现并行和进程锁,因此暂不做详细解析。
    目前任务是统计空闲进程的数量。
    采用的是最简单的遍历思路,在文件中定义了进程结构体数组struct proc proc[NPROC];直接遍历此进程数组,获取进程状态stateUNUSED的数量。参考已有代码中遍历进程数组的某个函数procdump:利用for(p = proc; p < &proc[NPROC]; p++)遍历进程数组,利用p->state判断进程状态。

    //Get the num of processes that are not in UNUSED state
    int 
    not_UNUSED_process_num(void){
      int count = 0;
      for(int i = 0; i < NPROC; i ++)
        count += (proc[i].state != UNUSED);
      return count;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2.3 sys_Sysinfo

    提示1:sysinfo需要将数据复制到用户态,因此需要使用copy_out函数,可参考sys_fstat() (kernel/sysfile.c) filestat() (kernel/file.c)函数。sys_fstat() (kernel/sysfile.c) 函数如下:

    // kernel/sysfile.c
    uint64
    sys_fstat(void)
    {
      struct file *f;
      uint64 st; // user pointer to struct stat
    
      if(argfd(0, 0, &f) < 0 || argaddr(1, &st) < 0)
        return -1;
      return filestat(f, st);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    主要完成的是2个工作:

    2.3.1读取用户传递的参数

    此系统调用有2个参数,参数1为struct file *类型,参数2为uint64类型,但参数不是直接传递的,而是需要通过argxx去读取trapframe寄存器,因为在用户调用参数前,会将参数按顺序存放到trapframea0a6寄存器。

    例如static int argfd(int n, int *pfd, struct file **pf)用于读取用户传进来的fd,并从进程的打开文件列表ofile中,根据fd索引到其指向的文件结构体file

    // Fetch the nth word-sized system call argument as a file descriptor
    // and return both the descriptor and the corresponding struct file.
    static int
    argfd(int n, int *pfd, struct file **pf)
    {
      int fd;
      struct file *f;
    
      if(argint(n, &fd) < 0)
        return -1;
      if(fd < 0 || fd >= NOFILE || (f=myproc()->ofile[fd]) == 0)
        return -1;
      if(pfd)
        *pfd = fd;
      if(pf)
        *pf = f;
      return 0;
    }
    
    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;
    }
    
    • 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

    内核获得用户态函数参数调用流程

    2.3.2 数据复制到用户态

    参考filestat() (kernel/file.c),此处使用copyout函数将数据从内核态复制到用户态。copyout函数原型如下,它从src开始,复制len字节到虚拟地址dstva中。
    int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)

    // Get metadata about file f.
    // addr is a user virtual address, pointing to a struct stat.
    int
    filestat(struct file *f, uint64 addr)
    {
      struct proc *p = myproc();
      struct stat st;
      
      if(f->type == FD_INODE || f->type == FD_DEVICE){
        ilock(f->ip);
        stati(f->ip, &st);
        iunlock(f->ip);
        if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
          return -1;
        return 0;
      }
      return -1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    sys_sysinfo具体实现

    1. 读取用户传递参数:用户传递进来的是1个sysinfo结构体的指针,因此先使用argaddr获取此指针
    2. 获取需要的系统信息
    3. 通过copyout将获取的信息传递到用户态。
    uint64
    sys_sysinfo(void)
    {
      uint64 p;
    
      if(argaddr(0, &p) < 0)
        return -1;
      struct sysinfo sp;
      
      sp.nproc = not_UNUSED_process_num();
      sp.freemem = get_freemem();
      if(copyout(myproc()->pagetable, p, (char *)(&sp), sizeof(sp)) < 0)
          return -1;
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    3 总结

    通过这个实验,我了解了:

    • 系统调用的流程,但还不了解具体是如何从用户态陷入内核态(TODO)
    • xv6使用隐式链表数据结构来进行空闲内存的分配和回收,同时加锁防止多个进程使用此临界资源
    • 进程管理是是1个进程结构体数组维护,可通过遍历获取每个进程的状态
    • 内核态要将数据返回给用户态,因为用户态有自己的进程空间,使用的虚拟地址无法直接在内核态解引用,所以在内核态需要实现copyout函数,利用用户态进程的页表来进行虚拟地址到物理地址的寻址,再直接将数据拷贝到用户态进程的虚拟地址映射的物理地址上。
  • 相关阅读:
    学习笔记之——视觉三维重建(colmap)
    Go语言结构体
    精通Linux,没用过scp?
    Html5API(自定义属性、媒体元素、canvas画布)(一)
    基于SpringBoot+Redis实现查找附近用户的功能
    【面试题】http协议
    echarts 双向柱状图 共用y轴
    11、设计模式之享元模式(Flyweight)
    C编程基础四十分笔记
    【牛客 - 剑指offer】JZ10 斐波那契数列(入门难度)三种方案 Java实现
  • 原文地址:https://blog.csdn.net/m0_51531927/article/details/133963042