MIT 6.S081 Fall 2020 Lab 2

System calls

系列 -

学!

自己写一个系统调用, 能够追踪打印系统调用. 看到的时候我是蒙的, 这咋追踪. 后来看提示, 原来是修改 syscall() 函数. kernel/syscall.c 中的 syscall 函数长这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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]();
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

num 赋值为寄存器 a7 的值, 也就是系统调用号. 然后判断是否合法, 最后调用 syscallsnum. syscalls 是函数指针数组.

在这句话结束后, 打印信息就可以了.

首先加入一个系统调用:

  • kernel/syscall.h 中加入 #define SYS_trace 22
  • kernel/syscall.c 中加入系统调用 sys_trace 的声明 extern uint64 sys_trace(void);; 在 syscalls 数组中加入 [SYS_trace] sys_trace 的声明
  • user/user.h 中加入函数声明 int trace(int);
  • user/usys.pl 中加入入口声明 entry("trace");

一句话, 就是出现一堆系统调用的地方都对着抄加一个 trace 即可.

根据提示, 我们要把 mask 写到 proc 结构体中, 这样 fork 的时候就可以进行传递了. 于是在 kernel/proc.hstruct proc 最后加一个变量 int sys_trace_mask; 找到 kernel/proc.c 中的 fork 函数, 将子进程的 proc 结构体中的 sys_trace_mask 赋值为父进程的.

由于需要打印系统调用的名称, 所以我们还要创建一个字符串数组. 在 kernel/syscall.c 中写入如下内容:

1
2
3
4
5
6
7
8
const char *names[] = { "",
  "fork", "exit", "wait", "pipe",
  "read", "kill", "exec", "fstat",
  "chdir", "dup", "getpid", "sbrk",
  "sleep", "uptime", "open", "write",
  "mknod", "unlink", "link", "mkdir",
  "close", "trace"
};

这里用的就是系统调用号作下标.

把 syscall 函数改成这样:

 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]();
    if (p->sys_trace_mask & (1 << num))
      printf("%d: syscall %s -> %d\n",
              p->pid, names[num], p->trapframe->a0);
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

就可以实现打印 mask 所指示的系统调用过程了. 由于 proc 是全局变量, 所以初始的 mask 是 0. 这个打印就不会执行.

最后差一个修改 mask, 也就是 sys_trace 的功能. 在 kernel/sysproc.c 中写入如下函数即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
uint64
sys_trace(int)
{
  int mask;

  if(argint(0, &mask) < 0)
    return -1;
  myproc()->sys_trace_mask = mask;
  return 0;
}

写一个系统调用, 返回系统信息. kernel/sysinfo.h 如下:

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

先按照上一个任务的方法加入一个 sysinfo 系统调用. 这里在 kernel 文件夹下再写一个 sysinfo.c 来实现这个系统调用. 在 Makefile 的 OBJS 加入 $K/sysinfo.o \.

sysinfo.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
#include "types.h"
#include "param.h"
#include "riscv.h"
#include "spinlock.h"
#include "proc.h"
#include "defs.h"
#include "sysinfo.h"

extern int freemem(void);
extern int proc_number(void);

uint64
sys_sysinfo(void)
{
  struct proc *p = myproc();
  struct sysinfo info;
  uint64 addr;    // user pointer to struct sysinfo

  if (argaddr(0, &addr) < 0)
    return -1;

  info.freemem = freemem();
  info.nproc = proc_number();

  if (copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0)
    return -1;
  return 0;
}

需要注意头文件的顺序… 这还是第一次了解这玩意. 展开宏的时候是一个一个展开的, 所以如果有一个头文件中的 type 没有在之前的头文件中定义, 那么就会报错.

然后说一下 copyout. 由于系统调用已经陷入了内核态, 而传入的地址是某个程序的虚拟地址, 所以需要程序的页表来计算得到物理地址, 再进行写入 (这里是 copy 的方式). 千万不能认为传入的地址就是用户态的某一个变量的地址, 然后直接通过指针操作改内容.

你问我怎么知道要这样写? 答案是看 sys_fstat().

系统调用的问题解决了, 可以先随便赋值, 测试一下, 没问题再继续.

然后是写 freemem 和 proc_number.

kernel/kalloc.c 中加入如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int
freemem(void)
{
  struct run *r;
  int mem = 0;

  for (r = kmem.freelist; r; r = r->next)
    mem += PGSIZE;
  return mem;
}

通过翻阅 kalloc.c 之前的代码可以知道, kmem 这个结构里维护了一个空闲内存链表, 单位是页大小. 所以遍历累加即可.

kernel/proc.c 中加入如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int
proc_number(void)
{
  struct proc *p;
  int n = 0;

  for (p = proc; p < &proc[NPROC]; p++)
    n += p->state != UNUSED;
  return n;
}

通过翻阅之前的代码可以知道, proc 数组维护了所有的进程结构, 那么只需要遍历, 统计 state 不为 UNUSED 的数量就行. 当然更优秀的做法是直接用一个变量维护进程数.

额外记录 syscall 的参数个数, 然后去翻寄存器就行. 不写了.

TODO. 连负载是啥我都不知道… 再说, 先学之后的, 以后回来补.