MIT 6.S081 Lab2: System Calls

1. System call tracing

1.1 描述

In this assignment you will add a system call tracing feature that may help you when debugging later labs. You’ll create a new trace system call that will control tracing. It should take one argument, an integer “mask”, whose bits specify which system calls to trace. For example, to trace the fork system call, a program calls trace(1 << SYS_fork), where SYS_fork is a syscall number from kernel/syscall.h. You have to modify the xv6 kernel to print out a line when each system call is about to return, if the system call’s number is set in the mask. The line should contain the process id, the name of the system call and the return value; you don’t need to print the system call arguments. The trace system call should enable tracing for the process that calls it and any children that it subsequently forks, but should not affect other processes.

  • 将相应的文件添加到user/user.huser/usys.pl中,向kernel/syscall.h文件中添加本实验所需要添加的系统调用号。
  • kernel/sysproc.c中添加系统调用函数sys_trace(这是内核系统调用的实现),以及需要在进程的结构体中,新建一个变量Mask(掩码)由低位开始偏移系统调用号个bit的值(通过观察syscall.h文件以及题目中给的case可以发现系统调用号对应二进制的bit)。
  • 还需要再kernel/proc.c/fork函数中使得子进程继承父进程的Mask属性。
  • 参照kernel/syscall.c中别的系统调用,通过阅读user/trace.c代码,使用argint函数来提取trace函数中的第一个参数(a0寄存器)作为掩码赋给进程的属性Mask(这个赋值不会影响其他系统调用的掩码),还需要定义系统调用号对应的系统调用名数组。
  • 需要注意的点,a0作为返回值且a7作为系统调用号,在trace调用后打印信息时需要用到
  • trace的参数只能是(1<<系统调用号)每次只能检测一种系统调用(就是这么设计的),通过掩码的方式
  • argint等用于提取用户地址空间的参数到内核(比如通过寄存器)

1.2 实现

这里贴出部分代码

kernel/syscall.c/syscall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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](); // reap return value.
if ((1 << num) & p->mask)
printf("%d: syscall %s -> %d\n", p->pid, syscall_name[num], p->trapframe->a0);
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}

kernel/sysproc.c/sys_trace

1
2
3
4
5
6
7
8
9
uint64
sys_trace(void)
{
int mask;
if (argint(0, &mask) < 0) // 将a0寄存器中的值(也就是trace的第一个参数)赋给mask,argint获取的类型为int
return -1;
myproc()->mask = mask; // 每个进程都要有一个mask属性
return 0;
}

trace.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
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int
main(int argc, char *argv[])
{
int i;
char *nargv[MAXARG];

if(argc < 3 || (argv[1][0] < '0' || argv[1][0] > '9')){
fprintf(2, "Usage: %s mask command\n", argv[0]);
exit(1);
}

if (trace(atoi(argv[1])) < 0) { // 命令行参数是字符串形式
fprintf(2, "%s: trace failed\n", argv[0]);
exit(1);
}

for(i = 2; i < argc && i < MAXARG; i++){
nargv[i-2] = argv[i];
}
exec(nargv[0], nargv); // 从当前进程的上下文中加载一个程序并运行
exit(0);
}
  • 调用样例
    $ trace 32 grep hello README
    3: syscall read -> 1023
    3: syscall read -> 966
    3: syscall read -> 70
    3: syscall read -> 0

  • kernel/proc.c/fork。每个进程都要有一个mask属性,保证在fork子进程时也能追踪到子进程的syscall

    1
    np->mask = p->mask;

2. Sysinfo

2.1 描述

In this assignment you will add a system call, sysinfo, that collects information about the running system. The system call takes one argument: a pointer to a struct sysinfo (see kernel/sysinfo.h). The kernel should fill out the fields of this struct: the freemem field should be set to the number of bytes of free memory, and the nproc field should be set to the number of processes whose state is not UNUSED. We provide a test program sysinfotest; you pass this assignment if it prints “sysinfotest: OK”.

  • 类似trace系统调用将函数添加到相应文件中。
  • .需要参考函数kernel/sysfile.c/sys_fstat以及kernel/file.c/filestat中copyout函数的实现,即将内核数据复制到用户的虚拟地址空间。
  • 需要在kernel/kalloc.c以及kernel/proc.c中添加函数,获取空闲内存的数量(字节为单位),并获取状态为UNUSED状态的进程数,将两个函数的返回值分别赋给定义在kernel/sysinfo.h中sysinfo结构体的两个属性
    1
    2
    3
    4
    struct sysinfo {
    uint64 freemem; // amount of free memory (bytes)
    uint64 nproc; // number of process
    };
  • 需要注意的是,在获取空闲内存的数量时,空闲链表的一个节点的大小为一个页的大小,遍历空闲链表即可获得结果。
  • 该lab出现了比较少见的数组初始化方式Designated Initializers,详见此处
  • sysinfo(struct sysinfo*); // 传出参数为sysinfo结构体,里面保存系统记录的信息

2.2 实现

kernel/sysproc.c/sys_sysinfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uint64
sys_sysinfo(void)
{
uint64 useraddr; // 用于保存用户sysinfo参数的虚拟地址
if(argaddr(0, &useraddr) < 0) // argaddr获取的类型为指针(64-bit),获取到内核
return -1;
struct sysinfo sys;
struct proc* p = myproc();
sys.nproc = procnum();
sys.freemem = freemem();
// 将sys结构体从内核空间拷贝到用户空间useraddr,即参数sysinfo中
if (copyout(p->pagetable, useraddr, (char*)&sys, sizeof(sys)) < 0) {
return -1;
}
return 0;
}

kernel/kalloc.c/freemem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
uint64
freemem() {
struct run *r;
uint64 n = 0;
// every node of the freelist represent a page.
for (r = kmem.freelist; r; r = r->next) { // 遍历空闲空间链表
n += PGSIZE; // 记录未使用的物理地址空间大小(字节)
}
return n; // 返回未使用的物理地址空间的大小
}

// 物理内存的结构
struct {
struct spinlock lock;
struct run* freelist; // 空闲空间的链表
} kmem;

struct run {
struct run *next;
};

kernel/proc/procnum

1
2
3
4
5
6
7
8
9
10
11
uint64 procnum(void) {
struct proc *p;
uint64 num = 0;
// 遍历PCB(进程控制块)
for (p = proc; p < &proc[NPROC]; p++) { // NPROC为最大进程数量,xv6中为64
// xv6中有6种状态:未使用,使用,阻塞,就绪,运行,僵尸
if (p->state != UNUSED)
num++; // 记录除了未使用状态(i.e.没有分配内存)进程的个数
}
return num;
}

总结

通过对lab2的学习,阅读了kernel/proc.c以及kernel/proc.h。通过熟悉进程的结构,从本质上理解了进程调度,进程上下文,并阅读了fork、exec等系统调用的源代码。lab2以动手实现系统调用形式,让我明白了其通过系统调用号进行索引,也了解了Designated Initializers的数组初始化形式。