Skip to content

Xv6 and Unix utilities

Boot xv6 (easy)

使用 Ubuntu 20.04 ,安装需要的工具

sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu

部署 xv6

git clone git://g.csail.mit.edu/xv6-labs-2021
...
cd xv6-labs-2021
git checkout util

这里使用 ls 就可以看到 xv6 的源码了。
接下来执行 make qemu 就会编译运行 xv6 了。

使用 Ctrl-a x 就可以退出 xv6 (按住 Ctrl 按下 a ,松开 Ctrl 和 a ,按下 x)。

sleep (easy)

实现一个 sleep 接口。

实验思路

  1. 参考 user 下的其他程序,将头文件引入,(kernel/types.h 声明类型的头文件, user/user.h 声明系统第哦啊用的头文件,ulib.c 声明工具的头文件)
  2. int main(int argc, char* argv[]) 函数。其中,参数 argc 是所有参数的个数,argv[] 是参数的内容,第 0 个参数是程序的全名,其他的是用户输入的其他参数
  3. 先判断用户输入的参数是否正确,只要参数不等于 2 个,就知道用户输入的参数是有错误的,就可以打印返回错误信息。参考 user/echo.c ,可以使用 fprintf() 函数打印错误信息,最后需要调用 exit(1),0 表示正常,1 表示异常。
  4. 剩下的就是参数没有问题的情况,由于接收的是字符类型,所以应该先用 atoi() 函数将字符转换为整数,调用系统调用 sleep() 函数,最后调用系统调用 exit(0) 退出
  5. Makefile 中添加配置,在 UPROGS 的最后一行添加 $U/_sleep\

pingpong (easy)

实现一个 pingpong 接口。

实验思路

  1. 按照实验目的的思路,先使用 pipe 来创建管道,由于管道是半双工的,因此需要创建两个管道,一个是父进程向子进程发送字节,一个是子进程向父进程发送字节,其中 p[0] 是读端, p[1] 是写端
  2. 通过 fork 建立子进程,通过阅读 xv6文档可知,对于父进程, fork 会返回父进程的 pid ,同时创建并返回一个 pid 是 0 子进程;让父进程通过管道发送 "ping" 给子进程,然后使用 read 开始读取来自子进程的 pong
  3. 当子进程使用 read 读取管道,读取到父进程发送的 "ping" 时,通过 getpid 获取 "pid" ,然后打印出 "<pid>: received ping" ,之后使用 write 发送 "pong" 给父进程,最后退出子进程
  4. 当父进程读取到子进程的发送的 pong 时,打印出 "<pid>: received pong"

primes (moderate)/(hard)

实现一个多进程并发打印素数的接口。

实验思路

  1. 首先看链接中的图,可以看出,数字全部输入到第一条管道,然后打印出 2 ,然后将所有 2 的倍数都剔除,接着把被剔除的数字全部输入到下一条管道,第二个进程打印的管道传输来的第一个数字 3 ,然后将所有 3 的倍数剔除,以此类推
  2. 看图片上面的伪代码,首先 p 从左边的管道获取到一个数字,打印 p ,循环,n 从左边的的管道获取一个数字,如果 p 不能整除 n ,则将 n 发送到右边的管道
  3. 代码思路,main 函数中,使用 fork 生成父子进程,使用子进程调用一个 primes 函数,父进程利用管道将数字传输给子进程,子进程将读管道传输给 primes 函数
  4. primes 函数打印当前数字,生成一个用于父子传递的管道,对应提示网站的右边的传输,然后再使用 fork() 生成父子进程,然后子进程递归调用 primes函数,父进程接口读取参数中传来的数据,筛选完后发送给子进程

踩到的坑

  • 一定一定一定要记得把不需要还有用完的文件描述符关掉

    对于管道读操作,如果没有数据写入,会出现阻塞,直到有数据写入
    对于管道读操作,如果所有写端被关闭时,read() 会返回 0 (EOF)
    对于管道写操作,缓冲区满的时候,会出现阻塞,直到有空间写入
    对于管道写操作,如果所有读端被关闭时,会触发 SIGPIPE 信号(默认中止进程)

find (moderate)

写一个简单的版本的 UNIX 的查找程序:找到目录中所有带特定名称的文件。

实验思路

  1. 根据提示,先看一下 user/ls.c 是如何读取目录的,使用 open() 打开路径会返回一个文件描述符,使用 fstat() 获取路径信息;然后读取路径类型,如果路径是文件,直接打印,如果是文件夹,判断一下路径是否太长,太长直接返回错误,否则循环读取,每次读一个文件或文件夹,如果文件夹的 inodenum 是 0 ,说明这个文件失效了,直接跳过,接着将文件名复制到路径后面,再在路径末尾新增一个空字符串,最后格式化打印
  2. 先遍历当前目录的所有文件,查找出文件名带有目标字符的文件,如果找到直接打印,如果找到文件夹,就递归查找

xargs (moderate)

写一个简单版本的 UNIX 的 xargs 程序:从标准输入中读取行数,并为每一行运行一条命令,同时将行数作为参数提供给命令

实验思路

  1. 进行参数校验,如果大于 MAXARG ,直接返回错误
  2. 通过 read() 读取文件描述 0 ,可以读取到管道传递的数据。系统调用循环读取输入,直到遇到 EOF(文件结束符) ,需要开辟新的内存来存储读取到的参数,同时将所有的参数存储到一个指针数组中,但是不可以使用 main() 里的 argv[] ,因为容量不是固定的
  3. 最后,使用 fork() ,创建一个子进程,在子进程中调用 exec() ,父进程一定要使用 wait() 等待子进程执行结束再退出

扩展
至于为什么要使用子进程,我也很好奇,问了一下 ds ,原因如下

防止某个命令会修改环境变量,导致后续的操作逻辑出错
如果不使用子进程,不能实现并行执行
如果有多命令时,单个命令出错会导出整个 xargs 程序崩溃

踩到的坑

  • 头文件顺序对编译会有影响,user/user.h 不能第一个引入,否则会编译出错

Lab1 所有实现测试

执行 make grade

== Test sleep, no arguments == 
$ make qemu-gdb
sleep, no arguments: OK (2.8s) 
== Test sleep, returns == 
$ make qemu-gdb
sleep, returns: OK (0.7s) 
== Test sleep, makes syscall == 
$ make qemu-gdb
sleep, makes syscall: OK (1.0s) 
== Test pingpong == 
$ make qemu-gdb
pingpong: OK (1.0s) 
== Test primes == 
$ make qemu-gdb
primes: OK (0.9s) 
== Test find, in current directory == 
$ make qemu-gdb
find, in current directory: OK (1.0s) 
== Test find, recursive == 
$ make qemu-gdb
find, recursive: OK (1.2s) 
== Test xargs == 
$ make qemu-gdb
xargs: OK (1.1s) 
== Test time == 
time: FAIL 
    Cannot read time.txt
Score: 99/100
make: *** [Makefile:237: grade] Error 1

最后一个出错了,搜索了一下参考链接,只需要在 xv6-labs-2020 的目录下新建一个 time.txt 文件,然后在里面输入一个整数就可以了。应该是 mit 的老师用来了解学生的进度的

== Test xargs == 
$ make qemu-gdb
xargs: OK (1.2s) 
== Test time == 
time: OK 
Score: 100/100