Appearance
实验:Xv6 和 Unix 实用程序
本实验将让你熟悉 xv6 及其系统调用。
启动 xv6
查看 实验工具页面 获取如何设置计算机运行这些实验的信息。
获取实验的 xv6 源代码的 git 仓库:
$ git clone git://g.csail.mit.edu/xv6-labs-2024
Cloning into 'xv6-labs-2024'...
...
$ cd xv6-labs-2024这些实验所需的文件使用 Git 版本控制系统分发。对于每个实验,你将检出针对该实验定制的 xv6 版本。要了解更多关于 Git 的信息,请查看 Git 用户手册 或这个 面向计算机科学家的 Git 概述。Git 允许你跟踪对代码所做的更改。例如,如果你完成了一个练习并想检查点你的进度,你可以通过运行以下命令来提交你的更改:
$ git commit -am 'my solution for util lab exercise 1'
Created commit 60d2135: my solution for util lab exercise 1
1 files changed, 1 insertions(+), 0 deletions(-)
$你可以使用 git diff 查看你的更改,它会显示自上次提交以来的更改。git diff origin/util 显示相对于初始 util 代码的更改。origin/util 是这个实验的 git 分支名称。
构建并运行 xv6:
$ make qemu
riscv64-unknown-elf-gcc -c -o kernel/entry.o kernel/entry.S
riscv64-unknown-elf-gcc -Wall -Werror -O -fno-omit-frame-pointer -ggdb -DSOL_UTIL -MD -mcmodel=medany -ffreestanding -fno-common -nostdlib -mno-relax -I. -fno-stack-protector -fno-pie -no-pie -c -o kernel/start.o kernel/start.c
...
riscv64-unknown-elf-ld -z max-page-size=4096 -N -e main -Ttext 0 -o user/_zombie user/zombie.o user/ulib.o user/usys.o user/printf.o user/umalloc.o
riscv64-unknown-elf-objdump -S user/_zombie > user/zombie.asm
riscv64-unknown-elf-objdump -t user/_zombie | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$/d' > user/zombie.sym
mkfs/mkfs fs.img README user/xargstest.sh user/_cat user/_echo user/_forktest user/_grep user/_init user/_kill user/_ln user/_ls user/_mkdir user/_rm user/_sh user/_stressfs user/_usertests user/_grind user/_wc user/_zombie
nmeta 46 (boot, super, log blocks 30 inode blocks 13, bitmap blocks 1) blocks 954 total 1000
balloc: first 591 blocks have been allocated
balloc: write bitmap block at sector 45
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0
xv6 kernel is booting
hart 2 starting
hart 1 starting
init: starting sh
$如果你在提示符下输入 ls,你应该看到类似于以下的输出:
$ ls
. 1 1 1024
.. 1 1 1024
README 2 2 2227
xargstest.sh 2 3 93
cat 2 4 32864
echo 2 5 31720
forktest 2 6 15856
grep 2 7 36240
init 2 8 32216
kill 2 9 31680
ln 2 10 31504
ls 2 11 34808
mkdir 2 12 31736
rm 2 13 31720
sh 2 14 54168
stressfs 2 15 32608
usertests 2 16 178800
grind 2 17 47528
wc 2 18 33816
zombie 2 19 31080
console 3 20 0这些是 mkfs 包含在初始文件系统中的文件;大多数是你可运行的程序。你刚运行了其中一个:ls。
xv6 没有 ps 命令,但是如果你输入 Ctrl-p,内核将打印有关每个进程的信息。如果你现在尝试,你会看到两行:一行用于 init,一行用于 sh。
要退出 qemu 输入:Ctrl-a x(同时按 Ctrl 和 a,然后按 x)。
sleep
为 xv6 实现一个用户级的
sleep程序,类似于 UNIX sleep 命令。你的sleep应该暂停用户指定的 ticks 数量。tick 是 xv6 内核定义的时间概念,即定时器芯片两次中断之间的时间。你的解决方案应该在文件user/sleep.c中。
一些提示:
在开始编码之前,阅读 xv6 书籍 的第 1 章。
将你的代码放在
user/sleep.c中。查看user/中的一些其他程序(例如,user/echo.c、user/grep.c和user/rm.c)以了解如何将命令行参数传递给程序。将你的
sleep程序添加到 Makefile 中的UPROGS;一旦你这样做了,make qemu将编译你的程序,你将能够从 xv6 shell 运行它。如果用户忘记传递参数,sleep 应该打印一条错误消息。
命令行参数作为字符串传递;你可以使用
atoi将其转换为整数(参见 user/ulib.c)。使用系统调用
sleep。查看
kernel/sysproc.c获取实现sleep系统调用的 xv6 内核代码(查找sys_sleep),user/user.h获取可从用户程序调用的sleep的 C 定义,以及user/usys.S获取从用户代码跳转到内核的汇编代码以进行sleep。sleep 的
main应该在完成后调用exit(0)。查看 Kernighan 和 Ritchie 的书籍 The C programming language (second edition) (K&R) 来学习 C。
从 xv6 shell 运行程序:
$ make qemu
...
init: starting sh
$ sleep 10
(一段时间内什么都不会发生)
$当如上所示运行时,你的程序应该暂停。在命令行中(在 qemu 外部)运行 make grade 来查看你是否通过了 sleep 测试。
请注意 make grade 运行所有测试,包括下面任务的测试。如果你想为一个任务运行等级测试,输入:
$ ./grade-lab-util sleep这将运行匹配 "sleep" 的等级测试。或者,你可以输入:
$ make GRADEFLAGS=sleep grade这会做同样的事情。
pingpong
编写一个用户级程序,使用 xv6 系统调用通过一对管道在两个进程之间"乒乓"一个字节,每个方向一个管道。父进程应该向子进程发送一个字节;子进程应该打印 "
<pid>: received ping",其中<pid>是它的进程 ID,在管道上向父进程写入字节,然后退出;父进程应该从子进程读取字节,打印 "<pid>: received pong",然后退出。你的解决方案应该在文件user/pingpong.c中。
一些提示:
- 将程序添加到 Makefile 中的
UPROGS。 - 你需要使用
pipe、fork、write、read和getpid系统调用。 - xv6 上的用户程序有一组有限的库函数可用。你可以在
user/user.h中看到列表;源代码(除了系统调用)在user/ulib.c、user/printf.c和user/umalloc.c中。
从 xv6 shell 运行程序,它应该产生以下输出:
$ make qemu
...
init: starting sh
$ pingpong
4: received ping
3: received pong
$你的程序应该在两个进程之间交换一个字节,并产生如上所示的输出。运行 make grade 进行检查。
primes
使用管道为 xv6 编写一个并发素数筛选器程序,使用 此页面 中间位置的图片和周围文本中说明的设计。这个想法归功于 Unix 管道的发明者 Doug McIlroy。你的解决方案应该在文件
user/primes.c中。
你的目标是使用 pipe 和 fork 来设置管道。第一个进程将数字 2 到 280 输入到管道中。对于每个素数,你将安排创建一个进程,该进程从其左侧邻居通过管道读取,并通过另一个管道向其右侧邻居写入。由于 xv6 的文件描述符和进程数量有限,第一个进程可以在 280 处停止。
一些提示:
要小心关闭进程不需要的文件描述符,因为否则你的程序会在第一个进程到达 280 之前耗尽 xv6 的资源。
一旦第一个进程到达 280,它应该等待直到整个管道终止,包括所有子进程、孙进程等。因此,主要的 primes 进程应该只在所有输出都已打印并且所有其他 primes 进程都已退出后才退出。
提示:当管道的写入端关闭时,
read返回零。直接写入 32 位(4 字节)
int到管道中是最简单的,而不是使用格式化的 ASCII I/O。你应该只在需要时创建管道中的进程。
将程序添加到 Makefile 中的
UPROGS。如果编译器对函数
primes出现无限递归错误,你可能需要声明void primes(int) __attribute__((noreturn));来指示primes不返回。
你的解决方案应该实现一个基于管道的筛选器,并产生以下输出:
$ make qemu
...
init: starting sh
$ primes
prime 2
prime 3
prime 5
prime 7
prime 11
prime 13
prime 17
prime 19
prime 23
prime 29
prime 31
...
$find
为 xv6 编写一个简单版本的 UNIX find 程序:查找具有特定名称的目录树中的所有文件。你的解决方案应该在文件
user/find.c中。
一些提示:
- 查看 user/ls.c 了解如何读取目录。
- 使用递归允许 find 递归到子目录中。
- 不要递归到 "." 和 ".."。
- 对文件系统的更改在 qemu 的运行之间持续存在;要获得干净的文件系统,请运行
make clean然后运行make qemu。 - 你需要使用 C 字符串。查看 K&R(C 书籍),例如第 5.5 节。
- 注意 == 不像在 Python 中那样比较字符串。使用 strcmp() 代替。
- 将程序添加到 Makefile 中的
UPROGS。
你的解决方案应该产生以下输出(当文件系统包含文件 b、a/b 和 a/aa/b 时):
$ make qemu
...
init: starting sh
$ echo > b
$ mkdir a
$ echo > a/b
$ mkdir a/aa
$ echo > a/aa/b
$ find . b
./b
./a/b
./a/aa/b
$运行 make grade 查看我们的测试结果。
xargs
为 xv6 编写一个简单版本的 UNIX xargs 程序:其参数描述要运行的命令,它从标准输入读取行,并为每一行运行命令,将该行附加到命令的参数中。你的解决方案应该在文件
user/xargs.c中。
以下示例说明了 xargs 的行为:
$ echo hello too | xargs echo bye
bye hello too
$注意这里的命令是 "echo bye",附加的参数是 "hello too",使得命令变为 "echo bye hello too",输出 "bye hello too"。
请注意,UNIX 上的 xargs 进行了一种优化,它会一次将多个参数提供给命令。我们不期望你进行这种优化。要使 UNIX 上的 xargs 按照我们希望的方式运行,请使用 -n 选项设置为 1 运行。例如
$ (echo 1 ; echo 2) | xargs -n 1 echo
1
2
$一些提示:
- 使用
fork和exec在输入的每一行上调用命令。在父进程中使用wait等待子进程完成命令。 - 要读取单行输入,请一次读取一个字符,直到出现换行符('\n')。
- kernel/param.h 声明了 MAXARG,如果你需要声明 argv 数组,这可能很有用。
- 将程序添加到 Makefile 中的
UPROGS。 - 对文件系统的更改在 qemu 的运行之间持续存在;要获得干净的文件系统,请运行
make clean然后运行make qemu。
xargs、find 和 grep 结合得很好:
$ find . b | xargs grep hello将在 "." 目录下的每个名为 b 的文件上运行 "grep hello"。
要测试你的 xargs 解决方案,请运行 shell 脚本 xargstest.sh。你的解决方案应该产生以下输出:
$ make qemu
...
init: starting sh
$ sh < xargstest.sh
$ $ $ $ $ $ hello
hello
hello
$ $你可能需要回去修复 find 程序中的错误。输出中有许多 $ 是因为 xv6 shell 没有意识到它正在处理来自文件而不是控制台的命令,并且为文件中的每个命令打印一个 $。
提交实验
花费的时间
创建一个新文件 time.txt,并在其中放入一个整数,表示你在实验上花费的小时数。 git add 并 git commit 该文件。
答案
如果这个实验有问题,请在 answers-*.txt 中写下你的答案。 git add 并 git commit 这些文件。
提交
实验提交由 Gradescope 处理。 你需要一个 MIT gradescope 账户。 查看 Piazza 获取加入课程的入口代码。 如果需要更多帮助加入,请使用 此链接。
当你准备好提交时,运行 make zipball, 这将生成 lab.zip。 将此 zip 文件上传到相应的 Gradescope 作业。
如果你运行 make zipball 并且你有未提交的更改或未跟踪的文件,你会看到类似于以下的输出:
M hello.c
?? bar.c
?? foo.pyc
Untracked files will not be handed in. Continue? [y/N]检查以上行并确保你的实验解决方案所需的所有文件都被跟踪,即,不列在以 ?? 开头的行中。 你可以使用 git add {filename} 使 git 跟踪你创建的新文件。
警告
- 请运行
make grade确保你的代码通过所有测试。 Gradescope 自动评分器将使用相同的评分程序为你的提交分配成绩。- 在运行
make zipball之前提交任何修改的源代码。- 你可以在 Gradescope 上检查你的提交状态并下载提交的代码。Gradescope 实验成绩是你的最终实验成绩。
可选挑战练习
编写一个 uptime 程序,使用
uptime系统调用以 ticks 形式打印正常运行时间。在 find 的名称匹配中支持正则表达式。
grep.c对正则表达式有一些基本支持。xv6 shell(
user/sh.c)只是另一个用户程序。它缺少真实 shell 中的许多功能,但你可以修改和改进它。例如,修改 shell 使其在处理来自文件的 shell 命令时不打印$,修改 shell 以支持 wait,修改 shell 以支持制表符补全,修改 shell 以保持传递的 shell 命令历史记录,或者任何你希望你的 shell 做的其他事情。(如果你非常有雄心,你可能需要修改内核以支持你需要的内核功能;xv6 不支持太多。)