Mit6.828/6.S081 fall 2019的Lab1是Unix utilities,主要内容为利用xv6的系统调用实现sleep、pingpong、primes、find和xargs等工具。本文对各程序的实现思路及xv6的系统调用流程进行详细介绍。
前言在实验之前,推荐阅读一下官网LEC1中提供的资料。其中Introduction是对该课程的的概述,examples则是几个系统编程的样例,这两部分快速浏览一遍即可。对于xv6 book的第一章,则建议稍微细致地阅读一遍,特别是对fork()、exec()、pipe()、dup()这几个系统调用的介绍,会在后面实验中用到。
实验环境搭建参考上一篇文章。进入xv6-riscv-fall19项目后可以看到两个比较重要的目录:kernel为xv6内核源码,里面除了os工作的核心代码(如进程调度),还有向外提供的接口(system call);user中则是用户程序,如我们熟悉的ls,echo命令等。本次实验的目的就是在user中增加用户程序,借助kernel中提供的system call来实现所需的功能。
实验思路每一个Lab需要在对应的分支编写代码,进入xv6-riscv-fall19目录下,使用git checkout util切换到util分支,即可开始编写我们的程序。下面主要提供实现思路,具体实验代码请参考Github。
实验完成后使用make grade可以执行单元测试进行评分,会以gdb-server模式启动qemu,并在gradelib.py中模拟gdb-client对我们的程序进行测试。如果在make grade时报错Timeout! Failed to connect to QEMU,可以将gradelib.py的325行改为self.sock.connect(("127.0.0.1", port))。
sleepsleep功能为使进程睡眠若干个时钟周期(xv6中一个tick为100ms),首先创建user/sleep.c源文件,引入user.h头文件,系统调用和工具函数都定义在该文件里。核心代码如下:
sleep(atoi(argv[1]));完成编写后,在Makefile的UPROGS中追加一行$U/_sleep\。输入make qemu进行编译,成功后进入shell,输入sleep 10,如果进程睡眠了大约1s,则表示程序编写正确。
pingpong功能是父进程通过管道向子进程发送1字节,子进程收到后向父进程回复1字节。
由于管道是单向流动的,所以两次调用pipe()创建两个管道,分别对应两个方向。使用fork()创建子进程,在子进程中先从管道1read()再向管道2write(),父进程中则与之相反。
primesprimes的功能是输出2~35之间的素数,实现方式是递归fork进程并使用管道链接,形成一条pipeline来对素数进行过滤。
每个进程收到的第一个数p一定是素数,后续的数如果能被p整除则之间丢弃,如果不能则输出到下一个进程,详细介绍可参考文档。伪代码如下:
void primes() { p = read from left // 从左边接收到的第一个数一定是素数 if (fork() == 0): primes() // 子进程,进入递归 else: loop: n = read from left // 父进程,循环接收左边的输入 if (p % n != 0): write n to right // 不能被p整除则向右输出 }还需要注意两点:
文件描述符溢出: xv6限制fd的范围为0~15,而每次pipe()都会创建两个新的fd,如果不及时关闭不需要的fd,会导致文件描述符资源用尽。这里使用重定向到标准I/O的方式来避免生成新的fd,首先close()关闭标准I/O的fd,然后使用dup()复制所需的管道fd(会自动复制到序号最小的fd,即关闭的标准I/O),随后对pipe两侧fd进行关闭(此时只会移除描述符,不会关闭实际的file对象)。
pipeline关闭: 在完成素数输出后,需要依次退出pipeline上的所有进程。在退出父进程前关闭其标准输入fd,此时read()将读取到eof(值为0),此时同样关闭子进程的标准输入fd,退出进程,这样进程链上的所有进程就可以退出。
findfind功能是在目录中匹配文件名,实现思路是递归搜索整个目录树。
使用open()打开当前fd,用fstat()判断fd的type,如果是文件,则与要找的文件名进行匹配;如果是目录,则循环read()到dirent结构,得到其子文件/目录名,拼接得到当前路径后进入递归调用。注意对于子目录中的.和..不要进行递归。
xargsxargs的功能是将标准输入转为程序的命令行参数。可配合管道使用,让原本无法接收标准输入的命令可以使用标准输入作为参数。
根据lab中的使用例子可以看出,xv6的xargs每次回车都会执行一次命令并输出结果,直到ctrl+d时结束;而linux中的实现则是一直接收输入,收到ctrl+d时才执行命令并输出结果。
思路是使用两层循环读取标准输入:
内层循环依次读取每一个字符,根据空格进行参数分割,将参数字符串存入二维数组中,当读取到'\n'时,退出当前循环;当接收到ctrl+d(read返回的长度<0)时退出程序。
外层循环对每一行输入fork()出子进程,调用exec()执行命令。注意exec接收的二维参数数组argv,第一个参数argv[0]必须是该命令本身,最后一个参数argv[size-1]必须为0,否则将执行失败。
xv6系统调用流程