在《操作系统》这门硬核课程中,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)主要包含两个独立的小任务:sleeppingpong。看似简单,但对新手来说有两个思维门槛:

  1. 如何让系统识别我的新程序? 在 XV6 中,你不能只建一个 .c 文件就完事。你必须在 MakefileUPROGS(用户程序列表)中注册它,系统在编译打包虚拟磁盘镜像(fs.img)时,才会把你的程序装进去。

  2. Pingpong 实验中的管道 (Pipe) 死锁危机 管道是单向的,为了实现父子进程的“双向奔赴”(打乒乓球),必须创建两个管道。 最大的难点在于关闭不需要的文件描述符(fd)。如果父进程或子进程忘记关闭写端,读端就会一直阻塞等待,导致整个程序死锁(Deadlock),卡在光标处永远无法退出。

🚀 三、 实验完整流程与满分代码

首先,切换到系统调用实验分支:

git checkout syscall

任务 1:实现 sleep 命令

需求: 编写一个用户级别的 sleep 程序,接收用户传入的参数(时钟滴答数),暂停指定时间。

完整操作步骤:

  1. user/ 目录下新建 sleep.c 文件。

  2. 写入以下满分代码:

#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);
}
  1. 非常关键:打开根目录的 Makefile,找到 UPROGS,在列表末尾加上你的程序:

UPROGS=\
	$U/_cat\
	...
	$U/_zombie\
	$U/_sleep\    # 注意:前面加 $U/,最后加 \

任务 2:实现 pingpong 命令

需求: 父进程通过管道发一个字节给子进程,子进程收到后打印 <pid>: received ping;然后子进程再通过另一个管道发一个字节给父进程,父进程收到后打印 <pid>: received pong

完整操作步骤:

  1. user/ 目录下新建 pingpong.c 文件。

  2. 写入以下满分代码(注意管道关闭的时机):

#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);
    }
}
  1. 同样打开根目录的 Makefile,在 UPROGS 中注册:

	$U/_sleep\
	$U/_pingpong\  

🎉 测试与验收

在终端执行编译并运行打分脚本:

make clean
make qemu
# 进入 xv6 系统后可以手动测试:
# $ sleep 10
# $ pingpong

或者直接在主机终端运行官方评分脚本验证满分:

./grade-lab-syscall sleep pingpong

如果你看到了绿色的 OK,恭喜你,你的操作系统内核之旅已经完美起步!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐