sleep (easy)
实验目标
本实验是 6.S081 的"热身关",目的不在难度,而是先把手感建立起来:
- 熟悉 xv6 实验环境——git 分支怎么切、内核怎么构建、怎么运行、怎么调试、怎么评分。
- 写出第一个用户程序
sleep,体会"用户程序如何通过系统调用向内核请求服务"。 - 跑通评分脚本,建立"写代码 → 改 Makefile → 编译 → 测试"这一整套开发闭环。
后续所有实验都建立在这套环境之上,所以这一关虽简单,但不建议跳过——把工具链跑顺,后面才不会被环境问题卡住。
前置知识
动手前,先把环境和几个核心概念理清楚。
1. 实验仓库结构
仓库里几个文件/目录后续会反复出现:
| 路径 | 作用 |
|---|---|
kernel/ | 操作系统内核空间源码,负责底层硬件管理与系统功能实现 |
user/ | 用户空间应用程序源码,运行在内核提供的环境之上(我们写的sleep.c就放这里) |
Makefile | 构建脚本,用来编译出 xv6 内核与所有用户程序镜像 |
grade-lab-<xxx> | 评分脚本,完成某个小实验或整个 lab 后运行它即可打分 |
2. 常用命令速查
切换到第一个实验分支(util):
gitcheckout util开发与运行相关:
makeqemu# 构建并启动 xv6# 退出 xv6:先按 Ctrl+a,再按 xmakegrade# 给整个 lab 打分makeGRADEFLAGS=sleep grade# 只测 sleep 这一个子任务(util lab 中)GDB 双终端调试:
# 终端 1:以单核 + gdb 方式启动 qemumakeCPUS=1qemu-gdb# 终端 2:连上 gdbriscv64-unknown-elf-gdb kernel/kernel3. 系统调用与 sleep 的含义
sleep 实验的核心,是让你第一次亲手"调用一个系统调用"。这一点在 xv6 手册里有明确定位。
xv6 handout(Chapter 1: Operating system interfaces)
“xv6 provides the basic interfaces—system calls, pages, files, and directories—that Unix provides.”手册系统调用一览表(Figure 1-1)中对 sleep 的描述是:
sleep(n): Pause for n clock ticks.
也就是说,sleep本质是一个系统调用:用户程序调用它,内核让当前进程挂起n个时钟滴答(tick)后再被唤醒。参数单位是 tick,qemu 下时钟约 100Hz,所以sleep 10大致暂停零点几秒(实际观感因人而异,把数字调大效果更明显)。
关键概念:用户程序自己没有"暂停自己"的权限——能不能睡、睡多久,必须由内核来裁决。这正是“系统调用 = 用户态请求内核服务的唯一合法入口”这个 OS 核心思想的第一个具象例子。
实现思路
顺着"写一个命令行工具并让它真正能用"的思路,只需两步:
- 在
user/下新建sleep.c- 程序需要接收一个参数(要睡的 tick 数),所以先检查
argc是否为 2,不对就打印用法并exit(1)。 - 参数通过
argv[1]传入,它是字符串,需要用atoi转成整数后传给sleep()。 - 正常执行完调用
exit(0)表示成功退出。
- 程序需要接收一个参数(要睡的 tick 数),所以先检查
- 在
Makefile的UPROGS里注册- 新写的用户程序不会被自动编译进内核镜像,必须手动把
$U/_sleep加进UPROGS列表,否则sleep命令在 xv6 里根本不存在。
- 新写的用户程序不会被自动编译进内核镜像,必须手动把
这一步虽小,却揭示了 xv6 一个贯穿全程的套路:任何用户程序 / 系统调用,都要在 Makefile 里"登记"才能被构建。后面 labs 加新程序、新系统调用时,这个动作会反复出现。
代码实现
user/sleep.c—— 用户态 sleep 命令
/* * user/sleep.c */#include"kernel/types.h"#include"user/user.h"intmain(intargc,char*argv[]){if(argc!=2){printf("usage: sleep <ticks>\n");exit(1);}sleep(atoi(argv[1]));exit(0);}几点说明:
sleep()和exit()的函数声明都在user/user.h里(它们最终会触发ecall陷入内核),所以必须包含"user/user.h"。argc != 2的检查对应命令行sleep <ticks>恰好一个参数;参数个数不对就视为用法错误,返回非零退出码1。atoi(argv[1])把字符串形式的 tick 数转成整数;exit(0)表示程序正常结束。
Makefile—— 把程序编进内核镜像
/* * Makefile */UPROGS=\ $U/_cat\ $U/_echo\ $U/_forktest\ $U/_grep\ $U/_init\ $U/_kill\ $U/_ln\ $U/_ls\ $U/_mkdir\ $U/_rm\ $U/_sh\ $U/_stressfs\ $U/_usertests\ $U/_grind\ $U/_wc\ $U/_zombie\ $U/_sleep\ # 添加 $U/_sleep\验证
方式一:手动在 xv6 里测
makeqemu# 启动 xv6进入 shell 后执行(数字大一点暂停更明显):
sleep10观察程序是否真的暂停了相应时间,再正常回到 shell 提示符。
方式二:用评分脚本测
在 Linux 终端直接运行:
./grade-lab-utilsleep应看到三个子测试全部通过:
==Test sleep, no arguments==sleep, no arguments: OK(1.6s)==Test sleep, returns==sleep, returns: OK(0.3s)==Test sleep, makes syscall==sleep, makes syscall: OK(0.9s)三个测试分别验证了:无参数时的健壮性、执行后能正常返回、以及确实发起了sleep系统调用。
踩坑:python 找不到
如果运行脚本报:
/usr/bin/env: 'python': No such file or directory原因是系统里只有python3没有python。把grade-lab-util文件开头的:
#!/usr/bin/env python改成:
#!/usr/bin/env python3保存后重新执行./grade-lab-util sleep即可。后续想测 util 里的其他小实验,把命令末尾的实验名换掉就行。
复盘
本实验解决了什么
表面上只是写了 20 行代码实现一个sleep命令,实际上它帮你完成了三件"基础设施"级别的事:
- 跑通了工具链——git 分支、make 构建、qemu 运行、gdb 调试、grade 评分,这一整套环境从此不再是黑盒。
- 建立了"系统调用"的第一手直觉——用户程序
sleep()→ 陷入内核 → 内核让进程挂起,这是你第一次亲手走完"用户态请求内核服务"的完整动作。 - 记住了 xv6 的登记套路——新用户程序要在
Makefile的UPROGS里注册,这个动作后面会不断重复。
收获
- 什么是系统调用:能讲清"用户态程序没有直接操作硬件/调度进程的权限,必须通过
ecall之类指令陷入内核,由内核代劳并返回结果"——sleep 就是一个最小的例子。 - 用户态与内核态的边界:为什么
sleep不能由用户程序自己while空转实现?因为那样会霸占 CPU;真正的"挂起"必须由内核从调度层面剥夺其运行权。这个问题体现了你是否对特权级和调度有基本理解。 - argc / argv 与程序退出码:
argc != 2的用法校验、exit(0)表示成功、exit(1)表示错误,是命令行工具的基本素养,也是面试中 shell / 进程模型相关问题的常客。