在《操作系统》这门硬核课程中,MIT的 XV6-labs-2022 实验绝对是检验真理的唯一标准。本文作为系列开篇,将带你从零开始,跨越环境配置的重重陷阱,并以满分姿态拿下实验一(系统调用:sleep与pingpong)。
🛠️ 一、 环境配置与踩坑记录
很多同学在敲下第一行代码前,就已经被环境配置折磨得痛不欲生。本次实验推荐在Ubuntu或银河麒麟 (Kylin Linux)环境下进行。
踩坑点 1:RISC-V 交叉编译工具链缺失
XV6 是运行在 RISC-V 架构上的,而我们的电脑大多是 x86 架构。如果你直接make qemu,必然会遇到riscv64-unknown-elf-gcc: command not found的报错。避坑方案:必须安装完整的 RISC-V 工具链和 QEMU 模拟器。在基于 Debian/Ubuntu 的系统(如银河麒麟)中,执行以下命令:
sudo apt-get update sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu踩坑点 2:Makefile 变量名冲突
安装完工具链后,有时候系统里的 GCC 前缀是riscv64-linux-gnu-,而 XV6 的 Makefile 里默认寻找的是riscv64-unknown-elf-。避坑方案:不用慌,XV6 的 Makefile 很智能,它会尝试匹配。但如果依然报错,可以直接打开根目录的Makefile,找到TOOLPREFIX这一行,手动把它强行指定为你的本地工具链前缀:
TOOLPREFIX = riscv64-linux-gnu-踩坑点 3:权限与残留文件问题
在反复编译的过程中,经常会出现fs.img磁盘镜像损坏或占用报错。避坑方案:养成良好习惯,每次重新编译或遇到莫名其妙的报错时,先执行make clean清理残留对象,再执行make qemu。
🧠 二、 实验核心难点剖析
实验一(Syscall)主要包含两个独立的小任务:sleep和pingpong。看似简单,但对新手来说有两个思维门槛:
如何让系统识别我的新程序?在 XV6 中,你不能只建一个
.c文件就完事。你必须在Makefile的UPROGS(用户程序列表)中注册它,系统在编译打包虚拟磁盘镜像(fs.img)时,才会把你的程序装进去。Pingpong 实验中的管道 (Pipe) 死锁危机管道是单向的,为了实现父子进程的“双向奔赴”(打乒乓球),必须创建两个管道。 最大的难点在于关闭不需要的文件描述符(fd)。如果父进程或子进程忘记关闭写端,读端就会一直阻塞等待,导致整个程序死锁(Deadlock),卡在光标处永远无法退出。
🚀 三、 实验完整流程与满分代码
首先,切换到系统调用实验分支:
git checkout syscall任务 1:实现 sleep 命令
需求:编写一个用户级别的sleep程序,接收用户传入的参数(时钟滴答数),暂停指定时间。
完整操作步骤:
在
user/目录下新建sleep.c文件。写入以下满分代码:
#include "kernel/types.h" #include "kernel/stat.h" #include "user/user.h" // 包含 exit, sleep, atoi 等声明 int main(int argc, char *argv[]) { // 检查参数数量是否合法 (应为 sleep + 数字) if (argc != 2) { fprintf(2, "Usage: sleep <ticks>\n"); exit(1); } // 将字符串参数转换为整型 int ticks = atoi(argv[1]); // 调用系统调用 sleep sleep(ticks); // 正常退出 exit(0); }非常关键:打开根目录的
Makefile,找到UPROGS,在列表末尾加上你的程序:
UPROGS=\ $U/_cat\ ... $U/_zombie\ $U/_sleep\ # 注意:前面加 $U/,最后加 \任务 2:实现 pingpong 命令
需求:父进程通过管道发一个字节给子进程,子进程收到后打印<pid>: received ping;然后子进程再通过另一个管道发一个字节给父进程,父进程收到后打印<pid>: received pong。
完整操作步骤:
在
user/目录下新建pingpong.c文件。写入以下满分代码(注意管道关闭的时机):
#include "kernel/types.h" #include "kernel/stat.h" #include "user/user.h" int main(int argc, char *argv[]) { int p2c[2]; // 父传子的管道 (Parent to Child) int c2p[2]; // 子传父的管道 (Child to Parent) char buf[1]; // 传递的 1 字节缓冲 // 创建管道 pipe(p2c); pipe(c2p); if (fork() == 0) { // --- 子进程逻辑 --- close(p2c[1]); // 子进程只读不写,关闭 p2c 的写端 close(c2p[0]); // 子进程只写不读,关闭 c2p 的读端 // 1. 阻塞读取父进程发来的 'ping' read(p2c[0], buf, 1); printf("%d: received ping\n", getpid()); // 2. 向父进程发送 'pong' write(c2p[1], "b", 1); // 3. 用完后关闭所有文件描述符 close(p2c[0]); close(c2p[1]); exit(0); } else { // --- 父进程逻辑 --- close(p2c[0]); // 父进程只写不读,关闭 p2c 的读端 close(c2p[1]); // 父进程只读不写,关闭 c2p 的写端 // 1. 向子进程发送 'ping' write(p2c[1], "a", 1); // 2. 阻塞读取子进程发回的 'pong' read(c2p[0], buf, 1); printf("%d: received pong\n", getpid()); // 3. 用完后关闭所有文件描述符 close(p2c[1]); close(c2p[0]); // 等待子进程完全退出,防止产生僵尸进程 wait(0); exit(0); } }同样打开根目录的
Makefile,在UPROGS中注册:
$U/_sleep\ $U/_pingpong\🎉 测试与验收
在终端执行编译并运行打分脚本:
make clean make qemu # 进入 xv6 系统后可以手动测试: # $ sleep 10 # $ pingpong或者直接在主机终端运行官方评分脚本验证满分:
./grade-lab-syscall sleep pingpong如果你看到了绿色的OK,恭喜你,你的操作系统内核之旅已经完美起步!