System call tracing (moderate)

要求

创建一个控制跟踪的系统调用trace。需要传入一个整数掩码作为参数,该掩码指定跟踪哪些哪些系统调用。例如,可通过trace (1<<SYS_fork)来跟踪fork系统调用,SYS_forkkernel/syscall.h中定义。你需要修改内核以实现:当系统调用将要返回时,若它符合设置的掩码,则打印出一行信息,包括进程ID、系统调用名字以及返回值。trace系统调用应当跟踪一个进程及其所有子进程,但不能影响它们。

提示:

  • 运行make qemu,您将看到编译器无法编译 user/trace.c,因为系统调用的用户空间存根尚不存在:将系统调用的原型添加到user/user.h,向 user/usys.pl添加一个存根,向kernel/syscall.h添加一个系统调用号。Makefile调用perl脚本user/usys.pl,生成user/usys.S,实际的系统调用存根,它使用RISC-V的ecall指令过渡到内核。修复编译问题后,运行trace 32 grep hello README;它将失败,因为您尚未在内核中实现系统调用。
  • kernel/sysproc.c 中添加一个 sys_trace() 函数,该函数通过在 proc 结构中的新变量中记住其参数来实现新的系统调用(请参阅 kernel/proc.h)。从用户空间检索系统调用参数的函数位于 kernel/syscall.c 中,您可以在 kernel/sysproc.c 中看到它们的使用示例。
  • 修改fork()以将掩码从父进程复制到子进程
  • 修改syscall()以实现打印跟踪信息。你需要添加一个保存系统调用名称的数组

实现

步骤:

  1. 在Makefile的UPROGS中添加$U/_trace
  2. user/user.h中添加trace函数声明;

    int trace(int mask);
  3. user/usys.pl添加系统调用存根;

    entry("trace");
  4. kernel/syscall.h中定义系统调用序号。这样就能完成编译,但还没有在内核中实现该函数。

    #define SYS_trace  22
  5. kernel/proc.h定义的struct proc中添加字段mask来表示跟踪掩码:

    struct proc {
       ... // 其他字段不变
       int mask;  // 新加入的掩码字段
    };
  6. kernel/sysproc.c中添加函数sys_trace()

    uint64 sys_trace(void) {
      argint(0, &myproc()->mask);
      return 0;
    }
  7. 修改fork()函数,使其能将跟踪掩码从父进程复制给子进程:

    int fork(void) {
     ...
     // copy mask
     np->mask = p->mask;
     return 0;
    }
  8. kernel/syscall.c添加syscall名称数组,修改syscall()以打印跟踪信息:

    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", "sysinfo"};
    
    void syscall(void) {
     int num;
     struct proc* p = myproc();
    
     num = p->trapframe->a7;
     // num = * (int*)0;
     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->mask) {
             printf("%d: syscall %s -> %d\n", p->pid,
                    syscall_names[num], p->trapframe->a0);
         }
     } else {
         printf("%d %s: unknown sys call %d\n", p->pid, p->name, num);
         p->trapframe->a0 = -1;
     }
    }

结果

运行结果:
image.png
测试结果:
image.png

Sysinfo (moderate)

要求

添加一个收集正在运行的系统的信息的系统调用sysinfo。该系统调用需要一个struct sysinfo的指针作为参数。内核需要填写该结构体的两个字段:(1)freemem表示空闲内存字节数,(2)nproc表示状态非UNUSED的进程数。

提示:

  • 按照上边的步骤使得程序能够被编译。(user.h定义、usys.pl存根、syscall.h定义等)
  • 该系统调用需要将struct sysinfo拷贝会用户空间,因此需要用到copyout函数(使用方法可参考sys_fstat()filestat()
  • 计算空闲内存需要在kernel/kalloc.c中添加一个新函数
  • 计算进程数量需要在kernel/proc.c中添加新函数

实现

步骤:

  1. 在Makefile的UPROGS中添加$U/_sysinfotest
  2. user/user.h中添加trace函数声明,但需要在开头引入struct sysinfo的声明;

    struct sysinfo;
    ...
    int sysinfo(struct sysinfo*);
  3. user/usys.pl添加系统调用存根;

    entry("sysinfo");
  4. kernel/syscall.h中定义系统调用序号。这样就能完成编译,但还没有在内核中实现该函数。

    #define SYS_sysinfo 23
  5. kernel/sysproc.c中添加函数sys_sysinfo()

    uint64 sys_sysinfo(void) {
      uint64 p;
      argaddr(0, &p);
      return sysinfo(p);
    }
  6. 找个地方定义内核的sysinfo()函数,我这里选择新建一个文件kernel/sysinfo.c:(新建文件需要导入头文件、在Makefile特定位置添加文件名、在defs.h中添加声明)

    int sysinfo(uint64 addr) {
      struct sysinfo info;
      struct proc* p = myproc();
      info.freemem = getFreeMem();
      info.nproc = getProcNum();
      return copyout(p->pagetable, addr, (char*)&info, sizeof(info));
    }
  7. kalloc.c中实现获取空闲内存函数:

    uint64 getFreeMem() {
      struct run* r;
      uint64 n = 0;
      acquire(&kmem.lock);
      r = kmem.freelist;
      while (r) {
      ++n;
      r = r->next;
      }
      release(&kmem.lock);
      printf("freemem = %d\n", n);
      return n * PGSIZE;
    }
  8. proc.c中实现获取进程数量函数:

    uint64 getProcNum() {
      uint64 n = 0;
      struct proc* p;
      acquire(&pid_lock);
      for (p = proc; p < &proc[NPROC]; ++p) {
       if (p->state != UNUSED) {
           ++n;
       }
      }
      release(&pid_lock);
      return n;
    }

结果

运行结果
image.png
测试结果
image.png

总结

通过实践终于明白了所谓的syscall到底与普通的函数有什么区别:

  • 最根本的区别是普通函数运行在用户态,而系统调用运行在内核态
  • 用户态的函数可以直接传递参数,而系统调用不能直接传递参数,必须通过某种方式(argint argaddr)
  • 用户态函数可以直接返回,而系统调用必须通过某种方式拷贝到指定地址(copyout
  • 上述两点总的来说就算用户态和内核态的内存布局不同,并不相通

syscall的基本原理:

  • 用户态的某个程序调用syscall,然后由此进入内核态的syscall()函数
  • 该函数根据某个寄存器中储存的系统调用序号来调用指定的syscall函数
  • 系统调用函数第一件事就是要获取用户态传入的参数(如果有的话),然后再实现其功能逻辑
  • 最后,如果系统调用需要返回数值或地址,则需要copyout函数,还是因为用户态和内核态的内存布局不同
  • 具体有什么不同,应该在下一个实验Page tables中体会更深刻

Longfar
1 声望3 粉丝