课程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.html
Lab 地址:https://pdos.csail.mit.edu/6.S081/2020/labs/syscall.html
我的代码地址:https://github.com/Amroning/MIT6.S081/tree/syscall
相关翻译:http://xv6.dgs.zone/labs/requirements/lab2.html
参考博客:https://blog.miigon.net/posts/s081-lab2-system-calls/
Lab2: system calls
添加几个新的系统调用,帮助加深对 xv6 内核的理解
System call tracing(moderate)
在本作业中,您将添加一个系统调用跟踪功能,该功能可能会在以后调试实验时对您有所帮助。您将创建一个新的trace
系统调用来控制跟踪。它应该有一个参数,这个参数是一个整数“掩码”(mask),它的比特位指定要跟踪的系统调用。例如,要跟踪fork
系统调用,程序调用trace(1 << SYS_fork)
,其中SYS_fork
是*kernel/syscall.h*中的系统调用编号。如果在掩码中设置了系统调用的编号,则必须修改xv6内核,以便在每个系统调用即将返回时打印出一行。该行应该包含进程id、系统调用的名称和返回值;您不需要打印系统调用参数。trace
系统调用应启用对调用它的进程及其随后派生的任何子进程的跟踪,但不应影响其他进程。
我们提供了一个用户级程序版本的trace
,它运行另一个启用了跟踪的程序(参见*user/trace.c*)。完成后,您应该看到如下输出:
$ trace 32 grep hello README
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0
$
$ trace 2147483647 grep hello README
4: syscall trace -> 0
4: syscall exec -> 3
4: syscall open -> 3
4: syscall read -> 1023
4: syscall read -> 966
4: syscall read -> 70
4: syscall read -> 0
4: syscall close -> 0
$
$ grep hello README
$
$ trace 2 usertests forkforkfork
usertests starting
test forkforkfork: 407: syscall fork -> 408
408: syscall fork -> 409
409: syscall fork -> 410
410: syscall fork -> 411
409: syscall fork -> 412
410: syscall fork -> 413
409: syscall fork -> 414
411: syscall fork -> 415
...
$
添加一个新的trace系统调用,用来跟踪其他的系统调用。需要为其他每一个系统调用设定一个位mask,用mask设定的位来指定跟踪哪一个系统调用,并输出所要求的调试信息
先在syscall.h中添加trace的mask:
//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系统调用号
在syscall.c中全局声明trace系统调用处理函数,并且把系统调用号与处理函数关联:
//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); //全局声明trace系统调用处理函数
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, //系统调用号与处理函数关联
};
在 proc.h 中修改 进程类proc 结构的定义,添加 syscall_trace,用 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 syscall_trace; //存储进程的系统调用跟踪掩码,用于控制哪些系统调用需要被跟踪
};
在 proc.c 中,创建新进程的时候,为syscall_trace 设置默认值 0(否则会是随机数据):
static struct proc*
allocproc(void)
{
......
// Set up new context to start executing at forkret,
// which returns to user space.
memset(&p->context, 0, sizeof(p->context));
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
p->syscall_trace = 0; //创建新进程的时候,syscall_trace 设置为默认值0
return p;
}
在sysproc.c 中,实现跟踪的具体代码,也就是设置当前进程的 syscall_trace:
//当前进程的系统调用跟踪掩码
uint64
sys_trace(void)
{
int mask;
if(argint(0, &mask) < 0) // 通过读取进程的trapframe,获得 mask 参数
return -1;
myproc()->syscall_trace = mask; // 设置调用进程的syscall_trace掩码mask
return 0;
}
修改 fork 函数,使得子进程可以继承父进程的 syscall_trace mask:
int
fork(void)
{
......
// increment reference counts on open file descriptors.
for(i = 0; i < NOFILE; i++)
if(p->ofile[i])
np->ofile[i] = filedup(p->ofile[i]);
np->cwd = idup(p->cwd);
safestrcpy(np->name, p->name, sizeof(p->name));
np->syscall_trace = p->syscall_trace; //子进程继承父进程的syscall_trace
pid = np->pid;
np->state = RUNNABLE;
release(&np->lock);
return pid;
}
所有的系统调用到达内核态后,都会进入到 syscall() 这个函数进行处理,因此在syscall函数中做出修改:
//kernel/syscall.c
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 ((p->syscall_trace >> num) & 1) { // 如果当前进程设置了对该编号系统调用的 trace
printf("%d: syscall %s -> %d\n",p->pid, syscall_names[num], p->trapframe->a0); // syscall_names[num]: 从 syscall 编号到 syscall 名的映射表
}
}
else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
打印上述信息需要知道系统调用对应的名称,可以定义一个字符串数组映射:
//kernel/syscall.c
//定义系统调用名称的字符串数组
const char* syscall_names[] = {
[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",
};
内核部分配置完毕,接下来是用户态部分。
在 usys.pl 中,加入用户态到内核态的跳板函数:
//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"); //用户态下的程序通过调用trace函数来使用跟踪系统调用功能
该脚本运行后生成汇编文件,其中定义了每一个用户态下每一个系统调用的跳板函数
在用户态的头文件user.h加入函数声明,这用用户态可以找到这个跳板入口函数:
// 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); //用户态程序可以找到trace系统调用的跳板入口函数
到此可以执行./grade-lab-syscall trace
验证该实验是否正确
系统调用的全流程:
user/user.h: 用户态程序调用跳板函数 trace()
user/usys.S: 跳板函数 trace() 使用 CPU 提供的 ecall 指令,调用到内核态
kernel/syscall.c 到达内核态统一系统调用处理函数 syscall(),所有系统调用都会跳到这里来处理。
kernel/syscall.c syscall() 根据跳板传进来的系统调用编号,查询 syscalls[] 表,找到对应的内核函数并调用。
kernel/sysproc.c 到达 sys_trace() 函数,执行具体内核操作
这么繁琐的调用流程的主要目的是实现用户态和内核态的良好隔离。
并且由于内核与用户进程的页表不同,寄存器也不互通,所以参数无法直接通过 C 语言参数的形式传过来,而是需要使用 argaddr、argint、argstr 等系列函数,从进程的 trapframe 中读取用户进程寄存器中的参数。
同时由于页表不同,指针也不能直接互通访问(也就是内核不能直接对用户态传进来的指针进行解引用),而是需要使用 copyin、copyout 方法结合进程的页表,才能顺利找到用户态指针(逻辑地址)对应的物理内存地址。
Sysinfo (moderate)
在这个作业中,您将添加一个系统调用sysinfo
,它收集有关正在运行的系统的信息。系统调用采用一个参数:一个指向struct sysinfo
的指针(参见kernel/sysinfo.h*)。内核应该填写这个结构的字段:freemem
字段应该设置为空闲内存的字节数,nproc
字段应该设置为state
字段不为UNUSED
的进程数。我们提供了一个测试程序sysinfotest
;如果输出“sysinfotest: OK*”则通过。
添加一个新的系统调用,获取 空闲内存量
和 已经创建的进程数量
并返回。
先是获取空闲内存量。在内存相关的kalloc.c中添加计算空闲内存的函数:
// kernel/kalloc.c
//获取空闲内存
void freebytes(uint64* dst) {
*dst = 0;
struct run* p = kmem.freelist;
acquire(&kmem.lock); //添加锁,防止竞态
while (p) {
*dst += PGSIZE; //统计空闲字节数
p = p->next;
}
release(&kmem.lock);
}
然后在内核头文件中defs.h声明这个函数:
// kernel/defs.h
// kalloc.c
void* kalloc(void);
void kfree(void *);
void kinit(void);
void freebytes(uint64* dst); //获取空闲内存
xv6 中,空闲内存页的记录方式是,将空虚内存页本身直接用作链表节点,形成一个空闲页链表,每次需要分配,就把链表根部对应的页分配出去。每次需要回收,就把这个页作为新的根节点,把原来的 freelist 链表接到后面。注意这里是直接使用空闲页本身作为链表节点,所以不需要使用额外空间来存储空闲页链表。
然后是获取已经创建的进程数量。在进程相关的proc.c中添加计算进程数量的函数:
// kernel/proc.c
//统计处于活动状态的进程
void
procnum(uint64* dst) {
*dst = 0;
struct proc* p;
for (p = proc;p < &proc[NPROC];p++) {
if (p->state != UNUSED)
(*dst)++;
}
}
也需要在内核头文件中声明函数:
// kernel/defs.h
// proc.c
......
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);
void procnum(uint64* dst); //统计处于活动状态的进程
然后就可以实现sysinfo了:
//收集系统信息
uint64
sys_sysinfo(void) {
struct sysinfo info;
freebytes(&info.freemem);
procnum(&info.nproc);
//获取虚拟地址
uint64 dstaddr;
argaddr(0, &dstaddr);
//从内核空间拷贝数据到用户空间
if (copyout(myproc()->pagetable, dstaddr, (char*)&info, sizeof info) < 0)
return -1;
return 0;
}
和上一个一样,添加对应的系统调用映射:
extern uint64 sys_sysinfo(void); //全局声明sysinfo系统调用处理函数
......
[SYS_sysinfo] sys_sysinfo,
和上一个一样,需要在用户态user.h和usys.pl添加跳板函数:
//user.h
struct sysinfo; //预先声明sysinfo结构体
int sysinfo(struct sysinfo*); //用户态程序可以找到sysinfo系统调用的跳板入口函数
//usys.pl
entry("sysinfo");
到此可以执行./grade-lab-syscall sysinfotest
验证该实验是否正确