该lab是对系统内核函数的设计
代码模块的话,这篇博客写的很详细了,
https://blog.miigon.net/posts/s081-lab2-system-calls/

trace:在proc中添加一个内核函数掩码,然后再调用syscall时,判断是否是要追踪的内核函数,是就进入

系统内核函数的调用逻辑大致为下面

  1. user/user.h: 用户态程序调用跳板函数 trace() user/usys.S:
  2. 跳板函数 trace() 使用 CPU 提供的 ecall 指令,调用到内核态 kernel/syscall.c
  3. 到达内核态统一系统调用处理函数 syscall(),所有系统调用都会跳到这里来处理。
  4. kernel/syscall.c syscall() 根据跳板传进来的系统调用编号,查询 syscalls[] 表,找到对应的内核函数并调用。 kernel/sysproc.c
  5. 到达 sys_trace() 函数,执行具体内核操作

这么繁琐的调用流程的主要目的是实现用户态和内核态的良好隔离。
并且用于内核与用户进程的页表不同,寄存器也不互通,所以参数无法直接通过C语言参数的形式传过来,而是需要使用argaddr、argint、argstr等系列函数,从进程的trapframe中读取用户进程寄存器的参数。
同时用于页表不同,指针也不能直接互通访问(也就是内核不能直接对用户态传进来的指针进行解引用),而是需要使用copyin、copyout方法结合进程的页表,才能顺利找到用户态指针(逻辑地址)对应的物理内存地址。

trace

  1. // user/trace.c
  2. int
  3. main(int argc, char *argv[])
  4. {
  5. int i;
  6. char *nargv[MAXARG];
  7. if(argc < 3 || (argv[1][0] < '0' || argv[1][0] > '9')){
  8. fprintf(2, "Usage: %s mask command\n", argv[0]);
  9. exit(1);
  10. }
  11. if (trace(atoi(argv[1])) < 0) {
  12. fprintf(2, "%s: trace failed\n", argv[0]);
  13. exit(1);
  14. }
  15. for(i = 2; i < argc && i < MAXARG; i++){
  16. nargv[i-2] = argv[i];
  17. }
  18. exec(nargv[0], nargv);
  19. exit(0);
  20. }

在这个方法中,是调用到了内核的trace函数,实际上是调用到了进程的trace方法,我们可以理解为,用户态在调用内核态方法前,已经是作为一个进程,在生命进程时,就会把传进来的内核函数掩码装进我们新加的一个trace_mask中,方便后续调用。

  1. // sysproc.c
  2. // add a sys_trace() function in kernel/sysproc.c
  3. uint64
  4. sys_trace(void)
  5. {
  6. int mask;
  7. if (argint(0, &mask) < 0) { // 取 a0 寄存器中的值返回给 mask
  8. return -1;
  9. }
  10. struct proc *p = myproc();
  11. p->trace_mask = mask; // 把掩码放进进程控制块,方便后续调用
  12. return 0;
  13. }

而所有的内核函数调用,最后都是调用到一个方法,在这个方法中,就去判断当前调用的方法是不是要追踪的函数掩码,是的话,就打印一下语句。

  1. // syscall.c
  2. void
  3. syscall(void)
  4. {
  5. int num;
  6. struct proc *p = myproc();
  7. num = p->trapframe->a7;
  8. if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
  9. p->trapframe->a0 = syscalls[num]();// 通过系统调用编号,获取系统调用处理函数的指针,调用并将返回值存到用户进程的 a0 寄存器中
  10. // 如果当前进程设置了对该编号系统调用的 trace,则打出 pid、系统调用名称和返回值。
  11. int trace_mask = p->trace_mask;
  12. if ((trace_mask >> num) & 1) {
  13. printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num - 1], p->trapframe->a0);
  14. }
  15. } else {
  16. printf("%d %s: unknown sys call %d\n",
  17. p->pid, p->name, num);
  18. p->trapframe->a0 = -1;
  19. }
  20. }

sysinfo

计算忙碌进程数及空闲内存字节

xv6 中,空闲内存页的记录方式是,将空虚内存页本身直接用作链表节点,形成一个空闲页链表,每次需要分配,就把链表根部对应的页分配出去。每次需要回收,就把这个页作为新的根节点,把原来的 freelist 链表接到后面。注意这里是直接使用空闲页本身作为链表节点,所以不需要使用额外空间来存储空闲页链表,在 kalloc() 里也可以看到,分配内存的最后一个阶段,是直接将 freelist 的根节点地址(物理地址)返回出去了: 常见的记录空闲页的方法有:空闲表法、空闲链表法、位示图法(位图法)、成组链接法。这里 xv6 采用的是空闲链表法。