MIT6.S081 Lab2:System Calls

287 阅读8分钟

进行此Lab之前,首先需要阅读Chapter2 和 Chapter4 的 4.3 和 4.4 节,包括部分源文件:

  • 用户空间代码(关于系统调用) user/user.h and user/usys.pl.
  • 内核空间代码kernel/syscall.h, kernel/syscall.c
  • 进程相关代码kernel/proc.h and kernel/proc.c

开始此Lab之前,还需要切换分支

 git fetch
 git checkout syscall
 make clean

Lab2:System Calls

System call tracing (moderate)

要求:

 添加一个系统调用跟踪特性,在调试以后的实验时可能会有所帮助。
 创建一个 trace 系统调用,它接收一个参数,参数是一个整数掩码(mask),掩码的位指定要跟踪哪个系统调用
 比如:要跟踪fork系统调用,程序调用 trace(1 << SYS_fork),其中SYS_fork是来自 kernel/syscall.h 的一个系统调用编号
 如果掩码中设置了系统调用编号,则必须修改xv6内核,以便 在每个系统调用即将返回时打印一行 ,该行应该包含进程id,系统调用的名称和返回值,不需要打印系统调用参数
 trace系统调用应该支持对调用它的进程和它随后派生的任何子进程的跟踪,但不应该影响其他进程。
 ​
 在命令行进行调用时:trace [tracing_mask] [command]
 比如:trace 32 grep hello README

提示:

  • kernel/sysproc.c中添加函数sys_trace(),通过在proc结构中定义一个新变量来记住该函数的参数(即mask),进行实现。从用户空间检索系统调用参数的函数是int argint(int, int*)(见kernel/syscall.c),可以在kernel/sysproc.c中看到它们的使用示例。 可以通过argint(int, int*)来获取系统调用参数,即 tracing mask。
  • 修改 fork() (见kernel/proc.c),从父进程拷贝 trace mask 到子进程
  • 修改syscall()(见kernel/syscall.c)打印 trace 输出。你需要添加一个用于索引的系统调用名称数组(syscalls)。

创建一个新的系统调用

  1. 在内核中合适的位置,实现内核系统调用(取决于要实现的功能属于什么模块,理论上随便放都可以,只是主要起归类作用),这里由于trace系统调用会对进程进行操作,所以放在 sysproc.c 中比较合适:
 // kernel/sysproc.c
 uint64 sys_trace(void) {
   int mask;
 ​
   if (argint(0, &mask) < 0)  // 通过argint获得参数
     return -1;
 ​
     // 设置当前调用进程的trace_mask为mask
     // 通过myproc获得指向进程的地址,trace_mask 是加在进程结构体中的变量用来保存mask
   myproc()->trace_mask = mask; 
   return 0;
 }
  1. kernel/syscall.h中,添加新的系统调用编号:
 // System call numbers
 #define SYS_fork    1
 #define SYS_exit    2
 #define SYS_wait    3
 #define SYS_pipe    4
 #define SYS_read    5
 #define SYS_kill    6
 #define SYS_exec    7
 #define SYS_fstat   8
 #define SYS_chdir   9
 #define SYS_dup    10
 #define SYS_getpid 11
 #define SYS_sbrk   12
 #define SYS_sleep  13
 #define SYS_uptime 14
 #define SYS_open   15
 #define SYS_write  16
 #define SYS_mknod  17
 #define SYS_unlink 18
 #define SYS_link   19
 #define SYS_mkdir  20
 #define SYS_close  21
 #define SYS_trace  22  // 添加的trace系统调用编号
  1. extern全局声明新的内核调用函数,并且在 syscalls映射表中,加入从前面定义的编号到系统调用函数指针的映射:
 // kernel/syscall.c
 extern uint64 sys_chdir(void);
 extern uint64 sys_close(void);
 extern uint64 sys_dup(void);
 extern uint64 sys_exec(void);
 extern uint64 sys_exit(void);
 extern uint64 sys_fork(void);
 extern uint64 sys_fstat(void);
 extern uint64 sys_getpid(void);
 extern uint64 sys_kill(void);
 extern uint64 sys_link(void);
 extern uint64 sys_mkdir(void);
 extern uint64 sys_mknod(void);
 extern uint64 sys_open(void);
 extern uint64 sys_pipe(void);
 extern uint64 sys_read(void);
 extern uint64 sys_sbrk(void);
 extern uint64 sys_sleep(void);
 extern uint64 sys_unlink(void);
 extern uint64 sys_wait(void);
 extern uint64 sys_write(void);
 extern uint64 sys_uptime(void);
 extern uint64 sys_trace(void);  // 添加函数声明
 ​
 // 函数指针数组
 static uint64 (*syscalls[])(void) = {
 [SYS_fork]    sys_fork,
 [SYS_exit]    sys_exit,
 [SYS_wait]    sys_wait,
 [SYS_pipe]    sys_pipe,
 [SYS_read]    sys_read,
 [SYS_kill]    sys_kill,
 [SYS_exec]    sys_exec,
 [SYS_fstat]   sys_fstat,
 [SYS_chdir]   sys_chdir,
 [SYS_dup]     sys_dup,
 [SYS_getpid]  sys_getpid,
 [SYS_sbrk]    sys_sbrk,
 [SYS_sleep]   sys_sleep,
 [SYS_uptime]  sys_uptime,
 [SYS_open]    sys_open,
 [SYS_write]   sys_write,
 [SYS_mknod]   sys_mknod,
 [SYS_unlink]  sys_unlink,
 [SYS_link]    sys_link,
 [SYS_mkdir]   sys_mkdir,
 [SYS_close]   sys_close,
 [SYS_trace]   sys_trace,  // 添加函数编号到函数的映射关系
 };

注意:[SYS_trace] sys_trace是C语言数组的一个语法,方括号内的数作为元素下标。比如int arr[] = {[3] 2333, [6] 6666}代表 arr 的下标为 3 的元素为 2333,下标为 6 的元素为 6666,其他元素填充为0。(该语法在C++中已不可用)

  1. user/usys.pl中注册一个内核态trace的入口。即在user/usys.pl中,加入用户态到内核态的跳板函数
 // user/usys.pl
 entry("fork");
 entry("exit");
 entry("wait");
 entry("pipe");
 entry("read");
 entry("write");
 entry("close");
 entry("kill");
 entry("exec");
 entry("open");
 entry("mknod");
 entry("unlink");
 entry("fstat");
 entry("link");
 entry("mkdir");
 entry("chdir");
 entry("dup");
 entry("getpid");
 entry("sbrk");
 entry("sleep");
 entry("uptime");
 entry("trace");  // 加入用户态到内核态的跳板函数

Makefile调用 perl 脚本 user/usys.pl,该脚本生成user/usys.S(实际的系统调用stub),它使用RISC-V的ecall指令进入内核。

  trace:     # 定义用户态跳板函数
  li a7, SYS_trace   # 将系统调用 id 存入 a7 寄存器
  ecall      # ecall,调用 system call ,跳到内核态的统一系统调用处理函数 syscall()  (syscall.c)
  ret
  1. user/user.h中加入一个trace的函数原型:int trace(int)
 // user/user.h
 // system calls
 int fork(void);
 int exit(int) __attribute__((noreturn));
 int wait(int*);
 int pipe(int*);
 int write(int, const void*, int);
 int read(int, void*, int);
 int close(int);
 int kill(int);
 int exec(char*, char**);
 int open(const char*, int);
 int mknod(const char*, short, short);
 int unlink(const char*);
 int fstat(int fd, struct stat*);
 int link(const char*, const char*);
 int mkdir(const char*);
 int chdir(const char*);
 int dup(int);
 int getpid(void);
 char* sbrk(int);
 int sleep(int);
 int uptime(void);
 int trace(int);

系统调用的具体流程

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

通过以上过程,可以实现用户态和内核态的良好隔离。

由于用户态的参数无法直接通过C语言参数的形式传递到内核,所以需要使用argaddrargintargstr等系列函数,从进程的trapframe读取用户进程寄存器中的参数

同时,内核对于用户态传进来的指针不能直接解引用,这是因为内核不能信任任何用户空间的指针。必须对用户空间的指针指向的数据进行验证,因为这个指针指向的地址只在这个进程空间是有效的,跨进程是无效的。并且需要通过验证,避免被传进一个内核的地址

所以需要使用copyincopyout方法结合进程的页表,才能顺利找到用户态指针(逻辑地址)对应的物理地址:

 struct proc *p = myproc(); // 获取调用该 system call 的进程的 proc 结构
 copyout(p->pagetable, addr, (char *)&data, sizeof(data)); // 将内核态的 data 变量(常为struct),结合进程的页表,写到进程内存空间内的 addr 地址处。

代码: 在kernel/proc.h中定义一个新变量,用来保存mask:

 // kernel/proc.h
 // Per-process state
 struct proc {
   struct spinlock lock;
 ​
   // p->lock must be held when using these:
   enum procstate state;        // Process state
   struct proc *parent;         // Parent process
   void *chan;                  // If non-zero, sleeping on chan
   int killed;                  // If non-zero, have been killed
   int xstate;                  // Exit status to be returned to parent's wait
   int pid;                     // Process ID
 ​
   // these are private to the process, so p->lock need not be held.
   uint64 kstack;               // Virtual address of kernel stack
   uint64 sz;                   // Size of process memory (bytes)
   pagetable_t pagetable;       // User page table
   struct trapframe *trapframe; // data page for trampoline.S
   struct context context;      // swtch() here to run process
   struct file *ofile[NOFILE];  // Open files
   struct inode *cwd;           // Current directory
   char name[16];               // Process name (debugging)
   uint64 trace_mask;           // Trace Mask
 };

kernel/proc.c中,当创建新进程时,为mask赋一个默认值0:

 // kernel/proc.c
 static struct proc*
 allocproc(void)
 {
     ...
   memset(&p->context, 0, sizeof(p->context));
   p->context.ra = (uint64)forkret;
   p->context.sp = p->kstack + PGSIZE;
 ​
   p->trace_mask = 0;  // 将trace_mask的默认值设置为0
 }

kernel/sysproc.c中,实现sys_trace()函数,它是trace系统调用的具体代码:

 // kernel/sysproc.c
 uint64 sys_trace(void) {
   int mask;
 ​
   if (argint(0, &mask) < 0)
     return -1;
 ​
   myproc()->trace_mask = mask;  // 设置当前调用进程的trace_mask为mask
   return 0;
 }

kernel/proc.c中,修改fork()函数,使得子进程可以继承父进程的 trace_mask

 // kernel/proc.c
 int
 fork(void)
 {
     ...
       safestrcpy(np->name, p->name, sizeof(p->name));
 ​
   pid = np->pid;
 ​
   np->state = RUNNABLE;
 ​
   np->trace_mask = p->trace_mask;  // 从父进程拷贝trace_mask到子进程
 ​
   release(&np->lock);
 ​
   return pid;
 }

由于所有的系统调用到达内核态后,都会进入syscall()这个函数,所以为了可以跟踪所有的系统调用,只需要修改syscall()函数,在该函数中打印输出即可(为了能输出系统调用的名称,还需要建立一个系统调用编号到系统调用名称的映射):

// 系统调用名称数组(用于trace系统调用打印信息)
const char* syscalls_name[] = {
[SYS_fork]    "fork",
[SYS_exit]    "exit",
[SYS_wait]    "wait",
[SYS_pipe]    "pipe",
[SYS_read]    "read",
[SYS_kill]    "kill",
[SYS_exec]    "exec",
[SYS_fstat]   "fstat",
[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",
};

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]();  // 通过系统调用编号,获取对应的系统调用函数指针,调用该函数并 将返回值存到用户进程的a0寄存器中 
    if ((1 << num) & (p->trace_mask)) {  // 如果mask中设置了该系统调用编号,则打印信息
      printf("%d: syscall %s -> %d\n", p->pid, syscalls_name[num], p->trapframe->a0);
    }
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

测试: image-20220427143017641

Sysinfo(moderate)

要求:

 添加一个系统调用sysinfo,它收集关于运行系统的信息
 sysinfo系统调用有一个参数:一个指向结构体sysinfo的指针(见 kernel/sysinfo.h)
 内核应该填充这个结构体的字段:
 freemem 字段应该设置为空闲内存的字节数
 nproc 字段应该设置为状态 不是UNUSED 的进程的数量
 ​
 如果测试程序 sysinfotest 输出 "sysinfotest: OK" 说明通过了测试

提示:

  • sysinfo系统调用 需要拷贝一个 sysinfo结构体到用户空间(见 sys_fstat() (位于kernel/sysfile.c)和 filestat() (位于kernel/file.c) 查看如何使用copyout()
  • 为了收集空闲内存的数量,在kernel/kalloc.c中添加函数
  • 为了收集进程的数量,在kernel/proc.c中添加函数

获取空闲内存

在内核的头文件中(kernel/defs.h)对计算空闲内存的函数进行声明:

 // kernel/defs.h
 // kalloc.c
 void*           kalloc(void);
 void            kfree(void *);
 void            kinit(void);
 uint64          cal_free_mem(void);

kernel/kalloc.c中有如下两个结构体:

 // kernel/kalloc.c
 struct run {
   struct run *next;
 };
 ​
 struct {
   struct spinlock lock;
   struct run *freelist;
 } kmem;

xv6中,空闲内存页的记录方式是形成一个空闲页链表,需要分配空闲页时,就把链表头部的页分配出去,需要回收的时候,就将回收的页作为空闲页的新的头节点。并且链表节点就是空闲页本身

 // kernel/kalloc.c
 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;  // 返回空闲页链表的头节点,将其作为内存页使用
 }

注意:常见的记录空闲页的方法有:空闲表法、空闲链表法、位示图法、成组链接法。xv6使用的是空闲链表法。

代码:

 // kernel/kalloc.c
 // 计算空闲内存(计算空闲链表的长度,然后乘以每一页的大小)
 uint64 cal_free_mem(void) {
   struct run *r;
   uint64 mem_size = 0;
   acquire(&kmem.lock);  // 需要先上锁,防止竞态条件
   r = kmem.freelist;
   while (r) {
     mem_size += PGSIZE;
     r = r->next;
   }
   release(&kmem.lock);
   return mem_size;
 }

获取不是UNUSED的进程数量

同样需要先在kernel/defs.h,对函数进行声明:

 // kernel/defs.h
 // proc.c
 ...
 void            userinit(void);
 int             wait(uint64);
 void            wakeup(void*);
 void            yield(void);
 int             either_copyout(int user_dst, uint64 dst, void *src, uint64 len);
 int             either_copyin(void *dst, int user_src, uint64 src, uint64 len);
 void            procdump(void);
 uint64          cal_not_unused_proc(void);

代码:

 // kernel/proc.c
 struct proc proc[NPROC]; // 进程数组
 // 获取不是UNUSED的进程数量
 uint64 cal_not_unused_proc(void) {
   uint64 res = 0;
   for (int i = 0; i < NPROC; ++i) { // 遍历进程数组进行统计即可
     if (proc[i].state != UNUSED)
         ++res;
   }
   return res;
 }

实现sysinfo系统调用:

 // kernel/sysproc.c
 uint64 sys_sysinfo(void) {
   uint64 addr; // 从用户态读入一个地址,存放 sysinfo 结构
   if (argaddr(0, &addr) < 0)
     return -1;
   
   struct sysinfo info;
   info.freemem = cal_free_mem();
   info.nproc = cal_not_unused_proc();
   // 将sysinfo结构体从内核空间拷贝到用户空间
   if (copyout(myproc()->pagetable, addr, (char* )&info, sizeof(info)) < 0)
     return -1;
   return 0;
 }

测试:

image-20220427232759427

总体测试:

image-20220427233222389

参考:blog.miigon.net/posts/s081-…