Part 0: 预备阶段
xv6 是通过 trap(和中断略有不同但处理流程相同)实现系统调用的,以下针对系统调用的流程做了具体分析。首先是vector.pl
(Generate vectors.S, the trap/interrupt entry points)这个 perl 脚本,它在运行时生成 vector.S
用于存放中断处理程序的入口地址:
1 |
|
vectors
存放中断处理入口点,共 256 项;每一个中断处理做的事情是 push 0 和中断号并跳转至 trapasm.S: alltraps
执行后续操作。trap.c
中设置了 256 个中断门描述符,并将 T_SYSCALL
号中断的第二个参数设为1指定这是一个陷阱门,其 DPL 记为 DPL_USER
(0x3):
1 |
|
trap.h
说明了用于处理系统调用的中断号为 64(0x40):
1 |
|
也就是说,所有的系统调用均由同一个第 64 号中断处理程序实现,因此需要区分不同的系统调用。syscall.h
定义了不同系统调用的调用号:
1 |
|
在 usys.S
中实现了系统调用:
1 |
|
由于系统调用名声明为 global,因此当用户程序链接 usys.o
后系统调用名对用户程序就可见了。系统调用的过程比较简单,先将系统调用号存入寄存器 %eax
中,再执行 int 0x40
触发64号系统中断。一旦触发了系统调用,cpu 就需要离开原来的指令,跳转至系统调用的代码执行;等系统调用完成后返回继续执行原来的指令,这就需要在触发系统调用时保存原先程序的上下文。xv6 会为每个进程创建 4KB 大小的内核栈保存上下文,结构体 proc.h: struct proc
存放了内核栈的地址 kstack
:
1 |
|
其中指针 tf
指向 x86.h: struct trapframe
存放了具体的上下文信息,包括中断发生时 cpu 的寄存器状态等。该帧包含了 cpu 从当前进程的内核态恢复到用户态的所有信息。待上述事宜处理完成后,trapasm.S
会将 %esp
压栈作为参数,然后调用 trap.c: trap(struct trapframe *tf)
执行具体的中断处理程序:
1 |
|
1 |
|
syscall 的具体逻辑就很简单了,trap()
根据中断号 tf->trapno
判断是否是 syscall;当中断号为 64 时则调用 syscall.c: syscall()
,取出 tf->eax
中的系统调用号再调用具体的内核函数。其中 syscalls 函数表储存了系统调用号对应的内核函数:
1 |
|
系统调用函数的第 n 个参数是通过函数 argint
、argptr
、argstr
、argfd
获得的,它们分别获取整数、指针、字符串起始地址和文件描述符,以 argint
为例:
1 |
|
用户栈顶位置为 %esp
,参数就恰好在 %esp
之上(%esp + 4),因此第 n 个参数就在 %esp + 4 + 4*n。这里要注意,系统调用参数的大小只能是 4 个字节,因此它只能是int型或者指针。系统调用的实现 sysproc.c
和 sysfile.c
仅仅是封装,它们通过以上几个函数来解析参数(如果需要的话),然后调用真正的实现。系统调用结束后,trapasm.S
会将前面所有的寄存器(除了%eax)恢复:
1 |
|
最终,当整个系统调用完成后,cpu 恢复到原先进程的上下文,只有 %eax
改变成系统调用的返回值,cpu 继续执行 syscall 之后的代码,以上就是系统调用的大体流程。
Part 1: System call tracing
print syscall-name
我们的第一个任务是追踪系统调用,要求是当系统调用被启用时打印出它的名字和返回值。我们只需要在 syscall.c: syscall()
函数里添加该功能即可。为了打印方便,我们仿照系统调用函数表 static int (*syscalls[])(void)
定义一个系统调用函数名称表:
1 |
|
然后在 xv6 保存系统调用函数的返回值语句前添加打印语句即可:
1 |
|
补充:由于这个功能在每次系统调用的时候都会打印,十分影响其他实验的进行,因此笔者在 param.h
里定义了一个 flag 变量 PRINT_SYSCALL
,默认为 0,如果需要打开该功能可以自行将该参数改为 1,这样就实现了功能的开关闭。修改后的代码如下:(后面的实验会关闭该功能)
1 |
|
1 |
|
以下是运行 make qemu
启动xv6时系统调用的部分结果展示:
1 |
|
print arguments of syscall
另一个任务是打印出系统调用的参数。我们已经知道封装在 sysproc.c
和 sysfile.c
里的系统调用函数是通过函数 argint
、argptr
、argstr
、argfd
解析参数的,因此我们只需要在原语句里添加打印信息即可,我们以 sysfile.c: open
为例:
1 |
|
所有的系统调用函数都声明在头文件 user.h
内,我们只需要对照着对 sysproc.c
和 sysfile.c
两个文件下有参数的系统调用函数做类似改动即可,此处不再赘述。
1 |
|
以下是 xv6 启动时的系统调用参数展示:
1 |
|
为方便观察,我们注释掉了其他的系统调用函数参数的打印,仅保留了 open
,如下是执行 ls
的系统调用函数 open
的参数展示:
1 |
|
Part 2: Date system call
第二部分需要我们为 xv6 添加一个新的系统调用用于打印当前的 UTC 时间。参考指导书执行 grep -n uptime *.[chS]
查看现有的系统调用 uptime
的源文件分布:
1 |
|
实际上根据我们最开始对系统调用整个流程的具体分析,添加一个系统调用仅仅涉及到以下几个文件:
syscall.c
定义了系统调用号和内核函数的对应关系并声明内核函数syscall.h
定义了系统调用号sysproc.c
和sysfile.c
利用参数解析封装了系统调用函数user.h
声明了系统调用函数对应的内核函数usys.S
定义了系统调用的过程
最后在 Makefile
文件中和bigger_files实验类似,添加需要的用户代码信息 UPROGS
即可。我们按照参考书首先在xv6里添加文件 date.c
实现 date
系统调用的逻辑:
1 |
|
其中,结构体 rtadate
预定义在头文件 date.h
里,存储了具体的日期信息。然后在 sysproc.c
中添加封装函数 sys_date
,其中 cmostime
实现了系统时间的读取:
1 |
|
最后按照上述系统调用的流程对相应文件进行声明/定义等修改即可,最终执行 date
指令的效果如下:
1 |
|