MIT6.S081 Lab 2 Syscall
Lab 2 Syscall
System call tracing (moderate)
在本次作业中,你将添加一项系统调用跟踪功能,这可能对以后实验调试有所帮助。 创建一个新的trace系统调用来控制跟踪。 trace接受一个参数:一个整数掩码mask,mask的位指定要跟踪的系统调用。 例如,要跟踪 fork 系统调用,mask的值为1<<SYS_fork,其中 SYS_fork 是 kernel/syscall.h 中的系统调用编号。你必须修改 xv6 内核,以便在每个被追踪的系统调用在即将返回时打印出一行。 该行应包含进程 ID、系统调用名称和返回值;无需打印系统调用参数。 trace除了追踪调用trace的进程外还要追踪其fork的子进程。
提示:
-
在 Makefile 的 UPROGS 中添加 $U/_trace
-
运行 make qemu,你会发现编译器无法编译 user/trace.c,因为系统调用的用户空间存根还不存在:在 user/user.h 中添加系统调用的原型,在 user/usys.pl 中添加存根,在 kernel/syscall.h 中添加系统调用号。Makefile 会调用 perl 脚本 user/usys.pl,生成 user/usys.S,即实际的系统调用存根,它使用 RISC-V ecall 指令过渡到内核。 解决编译问题后,运行 trace 32 grep hello README;它将会失败,因为你还没有在内核中实现系统调用。
-
在 kernel/sysproc.c 中添加一个 sys_trace() 函数,通过在 proc 结构(参见 kernel/proc.h)中的一个新变量中记住参数来实现新的系统调用。
从用户空间获取系统调用参数的函数在 kernel/syscall.c 中,你可以在 kernel/sysproc.c 中看到它们的使用示例。
-
修改 fork()(参见 kernel/proc.c),将父进程的跟踪掩码复制到子进程。
-
修改 kernel/syscall.c 中的 syscall() 函数,打印跟踪输出。 您将需要添加一个系统调用名称数组,以便对其进行索引。
源码阅读
重点关注argint 、argaddr 的使用方式
系统调用不通过函数的形参获取参数,usertrap时系统会将参数保存在寄存器中,系统调用通过argint 、argaddr 获取寄存器中参数
完成实验
思路
设计trace.c用来追踪,设计sys_trace系统调用,trace的结果由syscall返回时负责打印
user/trace.c :负责处理shell输入的命令,调用sys_trace,并exec程序
sys_trace() in kernel/sysproc.c :负责写入mask
syscall() in kernel/syscall.c :负责校验mask,打印
添加函数声明
在 user/user.h 中添加系统调用的原型:int trace(int);
在 user/usys.pl 中添加一个存根:entry("trace");
在 kernel/syscall.h 中添加系统调用编号:#define SYS_trace 22
在 kernel/call.c 中将函数添加至调用列表:
extern uint64 sys_trace(void);
[SYS_trace] sys_trace,
sys_trace
trace调用通过argint获取需要追踪的系统调用参数,将其记录到进程结构体的mask中
uint64
sys_trace(void)
{
int n;
if(argint(0, &n) < 0)
return -1;
myproc()->mask = n;
return 0;
}
打印trace结果
//fork()
// copy mask from parent to child
np->mask = p->mask;
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->mask > 0){ // trace
if(p->mask & (1 << num)) // trace all syscall
printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num], p->trapframe->a0);
}
}
else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
实验结果:
补充
一开始做实验没注意掩码的使用,syscall中使用了等于进行了判断系统调用编号if(1 << num == p->mask)。虽然似乎也能通过测试,但这样写有巨大的问题:如果需要trace多个调用,如:0011,使用== 无论是 0001 还 0010都无法正确识别,而是用&运算则可以识别
Sysinfo (moderate)
在本次作业中,你将添加一个系统调用 sysinfo,用于收集运行系统的信息。 系统调用需要一个参数:指向 struct sysinfo 的指针(参见 kernel/sysinfo.h)。
内核应填写该结构体的字段:freemem 字段应设置为可用内存的字节数,nproc 字段应设置为进程state不是 UNUSED 的进程总数
提供了一个测试程序 sysinfotest;如果打印出 "sysinfotest: OK”即为成功
提示:
-
在 Makefile 的 UPROGS 中添加 $U/_sysinfotest
-
运行 make qemu;user/sysinfotest.c 将无法编译。 添加系统调用 sysinfo,步骤与前面的作业相同。 要在 user/user.h 中声明 sysinfo() 的原型,需要预先声明 struct sysinfo 的存在:
struct sysinfo; int sysinfo(struct sysinfo *);解决了编译问题后,运行 sysinfotest;它会失败,因为你还没有在内核中实现系统调用。
-
sysinfo 需要将 struct sysinfo 复制到用户空间;有关如何使用 copyout() 进行复制的示例,请参阅 sys_fstat() (kernel/sysfile.c) 和 filestat() (kernel/file.c)。
-
要收集可用内存的数量,请在 kernel/kalloc.c 中添加一个函数
-
要收集进程的数量,请在 kernel/proc.c 中添加一个函数
大致思路:sysinfo调用使用copyout取出内核才能访问的关键信息(内存、进程数)
源码阅读
int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
功能:将src地址起的len字节数据传入虚拟地址dstva在页表中对应的物理地址
sys_fstat() (kernel/sysfile.c) , filestat() (kernel/file.c)
用户调用函数:user/user.h:int fstat(int fd, struct stat*);
触发系统调用:uint64 sys_fstat(void)
系统调用 sys_fstat 获取并检查参数、调用处理函数 int filestat(struct file *f, uint64 addr)
filestat 中使用copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0 将st传入p的页表中
完成实验
函数声明
在user中添加sysinfo函数和结构体声明
在 user/usys.pl 中添加一个存根:entry("sysinfo");
在 kernel/syscall.h 中添加系统调用编号,``#define SYS_sysinfo 23`
在kernel/syscall.c中添加数组
extern uint64 sys_sysinfo(void);
[SYS_sysinfo] sys_sysinfo,
sysinfo系统调用
uint64
sys_sysinfo(void)
{
uint64 addr; // user space's struct sysinfo adress
struct sysinfo info;
struct proc *p = myproc();
if(argaddr(0, &addr) < 0)
return -1;
info.freemem = count_free_memory();
info.nproc = count_proc_not_UNUSED();
if(copyout(p->pagetable, addr, (char*)&info, sizeof(info)) < 0)
return -1;
return 0;
}
辅助函数
uint64 count_free_memory(void){
uint64 count = 0;
struct run *r;
r = kmem.freelist;
while(r){
count++;
r = r->next;
}
return count * PGSIZE;
}
// count the number of proc's state is not UNUSED
uint64 count_proc_not_UNUSED(void){
uint64 count = 0;
struct proc *p;
for(p = proc; p < &proc[NPROC]; p++){
if(p->state != UNUSED)
count++;
}
return count;
}
实验结果:
实验总结
不同于第一次实验的单纯使用系统调用,第二次实验开始深入内核,开始手写系统调用和相关的辅助函数。第一小题操作都在内核中完成,在熟悉了系统调用后,第二小题在第一题的基础上要求使用copyout将内核数据传入用户空间,所以这样来看实验的难度安排还是十分科学的。做完后对用户进行系统调用的细节有了比较清晰的认知。
总体来看这两个实验都不算很难,但依旧比较费事。花了点时间了解系统调用函数的声明方式,也花了很多时间在理解系统调用的参数获取上,理解了这点收获也是比较大的,在之后的实验中也可以快速使用。
一个比较简单的过程:
用户函数在用户空间调用系统调用函数,引发陷阱,进入陷阱时系统会将寄存器、函数参数等重要信息保存在陷阱帧(trapframe)中,随后进入内核,在内核中,系统调用函数访问内核数据,如果需要则通过copyout将内核数据传入用户空间

浙公网安备 33010602011771号