该lab是对系统内核函数的设计
代码模块的话,这篇博客写的很详细了,
https://blog.miigon.net/posts/s081-lab2-system-calls/
trace:在proc中添加一个内核函数掩码,然后再调用syscall时,判断是否是要追踪的内核函数,是就进入
系统内核函数的调用逻辑大致为下面
- 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方法结合进程的页表,才能顺利找到用户态指针(逻辑地址)对应的物理内存地址。
trace
// user/trace.cintmain(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方法,我们可以理解为,用户态在调用内核态方法前,已经是作为一个进程,在生命进程时,就会把传进来的内核函数掩码装进我们新加的一个trace_mask中,方便后续调用。
// sysproc.c// add a sys_trace() function in kernel/sysproc.cuint64sys_trace(void){int mask;if (argint(0, &mask) < 0) { // 取 a0 寄存器中的值返回给 maskreturn -1;}struct proc *p = myproc();p->trace_mask = mask; // 把掩码放进进程控制块,方便后续调用return 0;}
而所有的内核函数调用,最后都是调用到一个方法,在这个方法中,就去判断当前调用的方法是不是要追踪的函数掩码,是的话,就打印一下语句。
// syscall.cvoidsyscall(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 寄存器中// 如果当前进程设置了对该编号系统调用的 trace,则打出 pid、系统调用名称和返回值。int trace_mask = p->trace_mask;if ((trace_mask >> num) & 1) {printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num - 1], p->trapframe->a0);}} else {printf("%d %s: unknown sys call %d\n",p->pid, p->name, num);p->trapframe->a0 = -1;}}
sysinfo
计算忙碌进程数及空闲内存字节
xv6 中,空闲内存页的记录方式是,将空虚内存页本身直接用作链表节点,形成一个空闲页链表,每次需要分配,就把链表根部对应的页分配出去。每次需要回收,就把这个页作为新的根节点,把原来的 freelist 链表接到后面。注意这里是直接使用空闲页本身作为链表节点,所以不需要使用额外空间来存储空闲页链表,在 kalloc() 里也可以看到,分配内存的最后一个阶段,是直接将 freelist 的根节点地址(物理地址)返回出去了: 常见的记录空闲页的方法有:空闲表法、空闲链表法、位示图法(位图法)、成组链接法。这里 xv6 采用的是空闲链表法。
