[MIT 6.S081] Lab: system calls
Lab: system calls
前言
这次实验是实现内核中的两个 syscall
:trace
和 sysinfo
。
回顾一下第三节课,用户态的进程想要执行某个系统调用,例如 exex(init, argv)
,首先会将 init
和 argv
的地址分别存放在 a0
和 a1
寄存器中,然后 a7
存放所要调用的系统调用,最后执行 ecall
。之后要结束系统调用时,将系统调用的返回值传递给 a7
,最后再一次执行 ecall
。这样,一个系统调用就做完了。
ecall
指令用于 CPU 状态的转换。
System call tracing
这里所要实现的是 sys_trace
,也就是跟踪命令所调用的系统调用。它的指令格式如下:
trace trace_mask command
其中,trace_mask
是一个用来控制追踪系统调用类型的掩码。
首先,我们需要在 Makefile
为 UPROGS
添加 $U/_trace
。
UPROGS=\ ... $U/_trace\
观察 user/trace.c
,可以发现其中的 trace
函数缺失,需要在 user/user.h
中补全一个函数声明。
// system calls ... int trace(int);
按照 Lab 提供的信息,我们还需要在 user/usys.pl
中为刚刚添加的系统调用 trace
添加一个 entry
。
sub entry { my $name = shift; print ".global $name\n"; print "${name}:\n"; print " li a7, SYS_${name}\n"; print " ecall\n"; print " ret\n"; } ... entry("trace");
观察上面的汇编代码,我们可以发现,这个 perl
文件的作用就是生成一段汇编,其中 name
将会替换为 entry
中的系统调用名称。
.global trace global: li a7, SYS_trace ecall ret
还记得某处的 initcode
数组吗?它的实际代码如下:
# exec(init, argv) .global start start: la a0, init la a1, argv li a7, SYS_exec ecall # for(;;) exit(); exit: li a7, SYS_exit ecall jal exit # char init[] = "/init\0"; inti: .string "/init\0" # char *argv[] = { init, 0 }; .p2align 2 argv: .long init .long 0
我们可以看到,a7
寄存器将会承载 syscall
的编号。那么我们的 SYS_trace
代号在哪里呢?观察 kernal/syscall.h
,可以发现所有的 syscall
编号借由宏定义在这里。我们只需要在最后加一个就可以了。
... #define SYS_close 21 #define SYS_trace 22
此时我们需要看一下,通用的 syscall
到底是干了什么。观察 kernal/syscall.c
,找到 void syscall()
函数定义,将会发现它利用之前的编号,在一个函数指针数组中直接查找对应的系统调用。trace
也是一个系统调用,因此我们为这个表补全一个 sys_trace
。
// in kernal/syscall.c ... extern uint64 sys_uptime(void); extern uint64 sys_trace(void); static uint64 (*syscalls[])(void) = { ... [SYS_trace] sys_trace, };
到此,我们就开始实现 sys_trace
了。
通过 Lab 内容,我们观察 kernal/sysproc.c
,可以看到这里包含了很多的 sys_name
函数定义,还记得我们之前的 sys_trace
吗?我们只是指明了它在某个地方实现,但是目前为之依然还是找不到它。因此,我们要在这个文件下添加一个新的系统调用函数 sys_trace
,同时它的签名符合刚才的函数指针表。
uint64 sys_trace(void) { // 获取 `trace` 的参数 trace_mask int trace_mask; if (argint(0, &trace_mask) < 0) return -1; // ? return 0; }
现在获取到了 trace_mask
,但是我们要如何知道被执行的系统调用名称呢,难道是为每个系统调用添加吗?
回顾刚刚的一个通用系统调用 void syscall(void)
,可以知道的是,它将知道所有的系统调用具体是什么,那么我们直接修改它就可以了。问题是,要如何让它知道我们需要打印信息呢?
对于每一个执行的进程,应该有一个东西在描述它,那么这个东西最适合的就是描述进程的结构体 struct proc
了。我们为它添加一个 trace_mask
成员,通过它来传递 trace
的信息了。
// in kernal/proc.h struct proc { struct spinlock lock; ... char name[16]; // Process name (debugging) uint64 trace_mask; };
回到 sys_trace
函数,我们添加了这个成员后,就可以通过它来传递参数了。那么该如何传递呢?=
吗?假如命令是 trace a trace b com
,那么最后一个命令的关系应该为 a | b
,因此这里需要实现的是 |
。
uint64 sys_trace(void) { // 获取 `trace` 的参数 trace_mask int trace_mask; if (argint(0, &trace_mask) < 0) return -1; // 使用 myproc() 获取当前进程的描述块 myproc()->trace_mask |= trace_mask; return 0; }
到此为止,我们依然还未实现输出跟踪信息。回到 void syscall(void)
函数中,我们将利用 trace_mask
实现。为了方便,这里需要在多添加一个表,用于记录每个系统调用的名称。此外,对于 trace_mask
,如果要输出某一个系统调用,其关系应该为 (trace_mask & (1 << SYS_which)) == 1
。
static const char* syscalls_name[] = { [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", [SYS_sysinfo] "sysinfo", }; 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](); // return value // match trace if ((1 << num) & p->trace_mask) { // pid: syscall syscall_name ret_value printf("%d: syscall %s -> %d\n", p->pid, syscalls_name[num], p->trapframe->a0); } } else { printf("%d %s: unknown sys call %d\n", p->pid, p->name, num); p->trapframe->a0 = -1; } }
有一个问题,初始化呢?进程 fork 后又怎么办呢?
观察 kernal/proc.c
中,谁负责新建一个进程呢?allocproc
。谁负责 fork 呢? fork
。
在 allocproc
中,我们只需要记得将新建后的进程块为其 trace_mask
初始化为
static struct proc* allocproc(void) { ... p->trace_mask = 0; return p; }
在 fork
中,我们应该是新的进程块与其父进程块的 trace_mask
一致。
int fork(void) { int i, pid; struct proc *np; struct proc *p = myproc(); // Allocate process. if((np = allocproc()) == 0){ return -1; } // copy trace mask np->trace_mask = p->trace_mask; ... return pid; }
至此,sys_trace
实现完毕了。
Sysinfo
sysinfo
的步骤和 trace
的基本步骤差不多,不太需要修改哪些结构体之类的,但是它需要添加点函数,一个计算剩余的内存块大小,一个计算分配的进程数量。
计算内存块的大小,需要观察内存的组织形式。在 kernal/kalloc.c
可以看到,内存的组织形式是个链表,直接遍历就行了。要注意的是注意上一下锁。
// in kernel/kalloc.c // Get free memory byte uint64 free_mem_byte(void) { uint64 free_byte = 0; acquire(&kmem.lock); struct run *p = kmem.freelist; while (p) { free_byte += PGSIZE; p = p->next; } release(&kmem.lock); return free_byte; }
计算分配出去的进程数,就更直接了,直接遍历进程数组,获取对应的信息即可。
// in kernel/proc.c // Get process count where state is not unused uint64 free_proc_count(void) { uint64 cnt = 0; for (struct proc* p = proc; p < &proc[NPROC]; p ++) { cnt += p->state != UNUSED; } return cnt; }
最后就是 sys_sysinfo
的实现,我们只需在函数内将上述两个信息获取到后,将这个信息拷贝到用户态中指定的位置,这个方式可以参考其他位置的实现,重点是看 copyout
是如何使用的。
// in kernel/sysproc.c extern uint64 free_mem_byte(); extern uint64 free_proc_count(); uint64 sys_sysinfo(void) { uint64 dst; if (argaddr(0, &dst) < 0) return -1; struct sysinfo info = { .freemem = free_mem_byte(), .nproc = free_proc_count() }; if (copyout(myproc()->pagetable, dst, (char*)&info, sizeof(info)) < 0) return -1; return 0; }
Grade
本文作者:FlandreScarlet
本文链接:https://www.cnblogs.com/FlandreScarlet/p/17979656
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步