XV6-Lab-2 实验报告: Lab: system calls
摘要
本篇是 xv6 的第 2 个 lab,通过增加自定义的系统调用,了解系统调用的链路及原理。就实验内容本身来说,难度不高,也只包含两个 exercise。
原理及分析
我们知道,系统调用是操作系统内核提供的接口,提供封装好的底层功能。然而,系统调用的函数或者地址不是直接暴露给用户的。内核只是提供了一个统一的系统调用入口,称为 ECALL
。ECALL 接收一个数字参数,当一个用户程序想要将程序执行的控制权转移到内核,它只需要执行 ECALL
指令,并传入一个数字。这里的数字参数代表了应用程序想要调用的系统调用。这样的设计有很多优点,最直观的是,可以在 ECALL
内调用系统调用前,进行参数检查或者权限检查,避免非法的调用。
在 xv6 中,这些 “系统调用号” 声明于 kernel/syscall.h
,例子如下:
1 | // System call numbers |
在 kernel/syscall.c
中,syscall
根据调用号执行对应的系统调用,代码如下:
1 | void |
trapframe
保存了程序陷入内核前的寄存器信息,系统调用是一种主动的陷入内核的做法。在上述 syscall
函数中,读取了调用号,若合法则跳转并设置返回值。syscalls
是一个数组,将系统调用号和具体的函数进行对应。这些函数实现在不同的文件中:进程相关的调用(如 fork
),实现在 kernel/sysproc.c
中,文件相关的调用(如 fstat, read, write
),实现在 kernel/file.c
中。
在 C 语言中,函数参数里的 void,表明函数不接收任何参数。如果没有 void 的话,void syscall()
表明函数可以接收任意参数。
在用户态,系统调用在 user/user.h
中声明,用于链接。user/usys.pl
是所谓的系统调用桩(stub),通过 kernel/syscall.h
,将系统调用转化为对应调用号的 ECALL
的调用。该文件会被编译为 user/usys.S
,片段如下。能够看出是声明的系统调用代码,通过 RISC-V 的 ecall
指令陷入内核。ecall
会跳转到异常处理的地址,这个地址是通过硬件编程写入的。
1 | # generated by usys.pl - do not edit |
如此一来,整个系统调用的流程就串起来了:
user/user.h
中的声明user/usys.pl
的系统调用桩,设置系统调用号- 中断处理逻辑
kernel/syscall.c
的syscall
函数- 对应的系统调用函数
相应地,如果需要新加系统调用,也得按这个流程来。os 处理中断的逻辑与系统调用不直接相关,可以后面再理解。
Exercises
trace
要求实现一个系统调用,能够根据一个 mask,监控某个进程对应系统调用执行结果,并打印。对于该进程创建的子进程,监控同样生效。
简单思考之后,不难想到这个调用需要更新一个进程级别的状态,在系统调用结束时,根据这个状态决定是否打印相关信息;在创建子进程时,需要传播这个状态。进程级的状态,最简单的是直接保存在进程结构体中,kernel/proc.c
中,proc
结构体维护了进程状态,包含锁、pid、页表等。可以在这个结构中新加一个字段,保存这个 mask。系统调用函数如下:
1 | // trace system calls of given mask |
需要注意的是,内核里系统调用的传参机制比较特殊,不是声明在函数里,而是通过 argint, argaddr
这种函数读参,猜测与中断的机制有关。
然后在 syscall
函数中的系统调用执行完毕后,根据 mask 判断是否打印信息即可。这里还需要维护一个系统调用号到系统调用名的数组 sysname
,方便打印调用名。
1 | void |
不要忘记在 fork
调用中传播这个 mask。另外,用户态系统调用的声明和桩代码比较简单,这里就略去了。
sysinfo
要求实现一个系统调用,能够返回系统中的可用内存数量以及运行中的进程数量。难点有几处。
首先,如何计算可用内存大小?参考 kernel/kalloc.c
,发现有一个名为 kmem
的变量保存了空闲物理页信息。
1 | struct run { |
通过计算这个 freelist
链表的长度,乘以单个页面的大小,即可得到可用内存的大小,代码如下:
1 | int free_memory(void) |
其次,如何计算运行中的进程数量。参考 kernel/proc.c
,发现其声明了一个大小为 64 的 proc
数组,猜测其应该是支持的最大进程数量。
1 | struct proc proc[NPROC]; // 64 |
allocproc
函数印证了这一点,该函数申请一个新的进程,是通过遍历上述数组,找到 p->state == UNUSED
实现的。因此可以通过遍历数组,统计得到运行中的进程数量,代码如下:
1 | // num of processes |
上面两个函数的声明需要加在 kernel/defs.h
中,方便调用。然后,可以在 kernel/sysproc.c
中新建系统调用。
1 | uint64 |
copyout
用于将内核的数据拷贝到用户态,参考 fstat
调用可以了解它的用法。
最后,完成用户态的相关代码即可。
结果
1 | == Test trace 32 grep == |