为了完成 Syscall 作业,需要阅读:
XV6-book, Chapter 2, Sections 4.3 and 4.4
files:
user/user.h
,kernel/proc.c kernel/proc.h
,kernel/syscall.c kernel/syscall.h
system call tracing
system call tracing 需要我们补充 kernel 中的一些程序,将某程序中指定的 system call 打印出来。当然,这需要我们新增一个 system_trace
系统调用函数。题中,给定的 tracing 程序以 trace [system-call-number] [cmd]
的方式运行。我们首先去看 user/trace.c
中的内容,看看 system call number
是怎么传入系统调用的。
user/trace.c
的程序如下所示:
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 系统调用来实现。所以接下来的任务是进行 trace 系统调用的实现。再次回顾 Lab 1 中的内容,在 Lab 1 中我们分析系统调用是通过在 usys.S 的汇编程序中进入的,然后 ecall 到 kernel/syscall.c 中映射到的函数,最终执行。那么 trace 系统调用也是这个逻辑。
在汇编中,是这样实现的:
li a7, SYS_trace ecall ret
这个 lab 使用的是 risc-v 汇编。
li a7, SYS_trace
表示把 32 位的数据SYS_trace
加载到指定的寄存器a7
中。
ecall
将异常的类型写在a7
寄存器中,参数写在a0-a5
寄存器中。syscall 场景下,使用ecall
会把处理器的特权级别由 User-Mode 转到 Supervisor-Mode。那ecall
跳转的地址在哪里呢?在操作系统启动的时候,会把异常表地址绑定到stevc
寄存器中。
为了能够让每个进程知道需要打印出什么样的 syscall 调用,我们需要将 trace
调用传入的值传给进程?那如何将一个值传给进程,进程又该如何保存这个值呢?
显然,我们需要修改进程的定义,在 kernel/proc.h
中修改进程的数据结构,加上一个 mask 项来存储需要跟踪的系统调用的数值。
struct proc {
struct spinlock lock;
...
char name[16]; // Process name (debugging)
int mask; // -- Modified -- Lab2, system call.
};
接下来,我们需要考虑在哪个地方插入打印指令来打印出 trace 程序追踪的系统调用?是在 process 中加入吗?显然不能在进程中做到这一点。我们只能在 syscall
这个通用的系统调用函数里面加入处理代码。在文件 kernel/syscall.c
的函数 syscall
中加入打印代码:
void
syscall(void)
{
...
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
...
// -- modified --
if ((1 << num) & p->mask) {
printf("%d: syscall %s -> %d\n", p->pid, sysname[num], p->trapframe->a0);
}
} else {
...
}
}
在上述代码中,syscall 指令会将调用号(a7 寄存器)读取到 num 中,然后查找系统调用表并执行。我在这里定义了一个新的数组 sysname
来存放每个系统调用的名称。
对于系统调用,
a0
寄存器中保存的是返回值
为了便利的增加新的系统调用,xv6 lab 实现了用 perl 脚本来生成 asm 的一段小程序。我们需要在 user/usys.pl
中加入:
entry("trace");
在 make qemu 的过程中,它会自动生成对应的陷入系统调用的汇编如下:
.global trace
trace:
li a7, SYS_trace
ecall
ret
接下来,我们还需要给 sys_trace 函数在内核中新加一段声明,修改 kernel/syscall.c
文件,加入
extern uint64 sys_trace();
然后在 kernel/sysproc.c
中实现系统调用:
uint64
sys_trace() {
int mask;
argint(0, &mask);
if(mask < 0) return -1;
myproc()->mask = mask;
return 0;
}
这里的 argint
函数会去调用 argraw
函数如下:
static uint64
argraw(int n)
{
struct proc *p = myproc();
switch (n) {
case 0:
return p->trapframe->a0;
case 1:
return p->trapframe->a1;
case 2:
return p->trapframe->a2;
case 3:
return p->trapframe->a3;
case 4:
return p->trapframe->a4;
case 5:
return p->trapframe->a5;
}
panic("argraw");
return -1;
}
因为我们需要获得 mask 值,而 mask 值在 trace()
函数中是第一个,所以我们直接方位 a0
寄存器来得到 mask 的值。
总结:
在 xv6 中实现系统调用有如下的几步:
- user-mode 调用
trace()
,trace()
的实现在usys.S
的汇编中 usys.S
将 user-mode 中调用的系统调用代号填充到a7
寄存器中ecall
调用了kernel/syscall.c
中的syscall()
函数来执行系统调用,并切换到 supervisor-modesyscall()
函数本身没有定义输入,其从a7
寄存器中找到系统调用函数的代号,然后再在函数表中查找。
Notes:
为了过 fork()
测试样例,需要修改 fork()
函数,加入 np->mask = p->mask;
来继承 mask。
sysinfo
有了上面的例子,实现 sysinfo()
就非常的简单了。sysinfo 要求我们统计空闲进程的数量和空闲内存的数量。
对于进程,xv6 使用了一个数组来存储了所有的进程,每个进程的状态都保存在一个结构体(PCB)中。如果要查询每个进程的状态,我们只需要遍历所有的进程,然后 check 是否是 UNUSED 状态就行了。
每个进程的状态只能是下面的几种之一:
enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
我们需要在 kernel/proc.c
中加入代码:
// -- modified --
// To get the number of free processes.
uint64
free_process_num() {
uint64 res = 0;
struct proc* tmp;
for (tmp = proc; tmp < &proc[NPROC]; tmp++) {
acquire(&tmp->lock);
if (tmp->state != UNUSED) res++;
release(&tmp->lock);
}
return res;
}
需要注意的是: 在统计状态的时候,必须对当前的 PCB 加锁,防止出现冲突(free_process_num)运行过程中变成了 UNUSED 或者其他
而可用空间判断则是在 kernel/kalloc.c
文件中定义了一个链表,每个链表都指向上一个可用空间,这个kmem就是一个保存最后链表的变量。
struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
kmem.freelist
永远指向最后一个可用页,那我们只要顺着这个链表往前走,直到 NULL 为止。
在 kernel/kalloc.c
中加入代码:
uint64
free_mem_num() {
uint64 res = 0;
struct run *page = kmem.freelist;
while(page != 0) {
res ++;
page = page->next;
}
return res * PGSIZE;
}
对于统计空闲内存和进程的代码,kernel/def.h
中声明。 之后,我们在 kernel/sysproc.c
中实现 sysinfo()
函数如下:
uint64
sys_sysinfo() {
uint64 address;
struct sysinfo info;
struct proc *p = myproc();
argaddr(0, &address);
if (address < 0) return -1;
info.freemem = free_mem_num();
info.nproc = free_process_num();
if (copyout(p->pagetable, address, (char*)&info, sizeof(info)) < 0) return -1;
return 0;
}
这里 copyout 是吧页表中的内容 copy 到 user-mode 的进程信息中去。我们通过 argaddr(0, &address);
拿到 info
的地址,把内核态的内存 copy 到用户态去。