System call tracing (moderate)
要求
创建一个控制跟踪的系统调用trace
。需要传入一个整数掩码作为参数,该掩码指定跟踪哪些哪些系统调用。例如,可通过trace (1<<SYS_fork)
来跟踪fork系统调用,SYS_fork
在kernel/syscall.h
中定义。你需要修改内核以实现:当系统调用将要返回时,若它符合设置的掩码,则打印出一行信息,包括进程ID、系统调用名字以及返回值。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()
以将掩码从父进程复制到子进程 - 修改
syscall()
以实现打印跟踪信息。你需要添加一个保存系统调用名称的数组
实现
步骤:
- 在Makefile的
UPROGS
中添加$U/_trace
; 在
user/user.h
中添加trace
函数声明;int trace(int mask);
在
user/usys.pl
添加系统调用存根;entry("trace");
在
kernel/syscall.h
中定义系统调用序号。这样就能完成编译,但还没有在内核中实现该函数。#define SYS_trace 22
在
kernel/proc.h
定义的struct proc
中添加字段mask
来表示跟踪掩码:struct proc { ... // 其他字段不变 int mask; // 新加入的掩码字段 };
在
kernel/sysproc.c
中添加函数sys_trace()
:uint64 sys_trace(void) { argint(0, &myproc()->mask); return 0; }
修改
fork()
函数,使其能将跟踪掩码从父进程复制给子进程:int fork(void) { ... // copy mask np->mask = p->mask; return 0; }
在
kernel/syscall.c
添加syscall名称数组,修改syscall()
以打印跟踪信息:static char* syscall_names[] = {"", "fork", "exit", "wait", "pipe", "read", "kill", "exec", "fstat", "chdir", "dup", "getpid", "sbrk", "sleep", "uptime", "open", "write", "mknod", "unlink", "link", "mkdir", "close", "trace", "sysinfo"}; void syscall(void) { int num; struct proc* p = myproc(); num = p->trapframe->a7; // num = * (int*)0; if (num > 0 && num < NELEM(syscalls) && syscalls[num]) { // Use num to lookup the system call function for num, call // it, and store its return value in p->trapframe->a0 p->trapframe->a0 = syscalls[num](); // 在系统调用返回后添加打印信息的逻辑代码 if ((1 << num) & p->mask) { 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; } }
结果
运行结果:
测试结果:
Sysinfo (moderate)
要求
添加一个收集正在运行的系统的信息的系统调用sysinfo
。该系统调用需要一个struct sysinfo
的指针作为参数。内核需要填写该结构体的两个字段:(1)freemem
表示空闲内存字节数,(2)nproc
表示状态非UNUSED
的进程数。
提示:
- 按照上边的步骤使得程序能够被编译。(
user.h
定义、usys.pl
存根、syscall.h
定义等) - 该系统调用需要将
struct sysinfo
拷贝会用户空间,因此需要用到copyout
函数(使用方法可参考sys_fstat()
和filestat()
) - 计算空闲内存需要在
kernel/kalloc.c
中添加一个新函数 - 计算进程数量需要在
kernel/proc.c
中添加新函数
实现
步骤:
- 在Makefile的
UPROGS
中添加$U/_sysinfotest
; 在
user/user.h
中添加trace
函数声明,但需要在开头引入struct sysinfo
的声明;struct sysinfo; ... int sysinfo(struct sysinfo*);
在
user/usys.pl
添加系统调用存根;entry("sysinfo");
在
kernel/syscall.h
中定义系统调用序号。这样就能完成编译,但还没有在内核中实现该函数。#define SYS_sysinfo 23
在
kernel/sysproc.c
中添加函数sys_sysinfo()
:uint64 sys_sysinfo(void) { uint64 p; argaddr(0, &p); return sysinfo(p); }
找个地方定义内核的
sysinfo()
函数,我这里选择新建一个文件kernel/sysinfo.c
:(新建文件需要导入头文件、在Makefile特定位置添加文件名、在defs.h
中添加声明)int sysinfo(uint64 addr) { struct sysinfo info; struct proc* p = myproc(); info.freemem = getFreeMem(); info.nproc = getProcNum(); return copyout(p->pagetable, addr, (char*)&info, sizeof(info)); }
在
kalloc.c
中实现获取空闲内存函数:uint64 getFreeMem() { struct run* r; uint64 n = 0; acquire(&kmem.lock); r = kmem.freelist; while (r) { ++n; r = r->next; } release(&kmem.lock); printf("freemem = %d\n", n); return n * PGSIZE; }
在
proc.c
中实现获取进程数量函数:uint64 getProcNum() { uint64 n = 0; struct proc* p; acquire(&pid_lock); for (p = proc; p < &proc[NPROC]; ++p) { if (p->state != UNUSED) { ++n; } } release(&pid_lock); return n; }
结果
运行结果
测试结果
总结
通过实践终于明白了所谓的syscall
到底与普通的函数有什么区别:
- 最根本的区别是普通函数运行在用户态,而系统调用运行在内核态
- 用户态的函数可以直接传递参数,而系统调用不能直接传递参数,必须通过某种方式(
argint argaddr
) - 用户态函数可以直接返回,而系统调用必须通过某种方式拷贝到指定地址(
copyout
) - 上述两点总的来说就算用户态和内核态的内存布局不同,并不相通
syscall
的基本原理:
- 用户态的某个程序调用
syscall
,然后由此进入内核态的syscall()
函数 - 该函数根据某个寄存器中储存的系统调用序号来调用指定的
syscall
函数 - 系统调用函数第一件事就是要获取用户态传入的参数(如果有的话),然后再实现其功能逻辑
- 最后,如果系统调用需要返回数值或地址,则需要
copyout
函数,还是因为用户态和内核态的内存布局不同 - 具体有什么不同,应该在下一个实验Page tables中体会更深刻
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。