Introduction
本文旨在记录作者完成课程任务 HITSZ os-lab1 的过程,可供参考。
本实验需要完成的内容包括
- 编写用户程序
sleep.c,调用 xv6 提供的sleep()系统调用。 - 编写用户程序
pingpong.c,调用 xv6 提供的pipe(), read(), write(),实现父进程和子进程的通信 - 编写用户程序
find.c,调用 xv6 提供的open(), fstat()等文件系统相关系统调用,实现根据文件名的递归查找功能 - 使用 GDB 跟踪 xv6 的启动流程,并打印
initcode和init程序名称
实验仓库链接如下
指导书链接如下(需校内网)
仓库内有多个 lab 分支,本文所提的 lab1 对应的分支切换方法如下
git clone git@gitee.com:ftutorials/xv6-oslabs-hitsz.git
git checkout util
常用的指令如下:
make clean清理编译文件make qemu编译并进入 qemu 的 xv6 终端make diff执行git diff util-base-26 HEAD > commit.patch在对应结点基础上生成commit.patch文件,用于提交make grade对编写的程序进行评分测试make qemu-gdb CPUS=1启动调试,在另一个终端窗口上可输入make gdb来进行对应调试make gdb对代码进行调试git add .将当前目录下所有文件暂存git commit -m "lab 1 finished"提交更改到本地分支git push将本地分支同步到远程仓库git pull将远程仓库同步到本地
一些问题
下面是本人在 macOS 27 上完成 lab 过程中遇到的环境相关的问题,可供参考
GDB 相关问题
如果你的 GDB 版本较高,可能在编译时会遇到一些报错/警告,修改 Makefile 中的 CFLAGS 即可
CFLAGS = -Wall -Winfinite-recursion -O -fno-omit-frame-pointer -ggdb -DTEST
如果你在 macOS 上使用 riscv64-elf-gdb 发生了报错,考虑将原本的
make gdb
替换成
make GDB=riscv64-elf-gdb gdb
或者修改 Makefile
GDB = $(TOOLPREFIX)gdb
为
GDB = riscv64-elf-gdb
python 环境相关问题
如果你使用 make grade 发生 python 环境问题相关的报错,考虑修改 grade-lab-util 中的路径
#!/usr/bin/env python
任务1: sleep
实验要求在 user/sleep.c 中实现一个简化版 sleep 程序。它读取命令行参数中的时间长度,让当前进程休眠一段时间后返回。
运行效果示例如下(以下命令在 XV6 命令行 中运行):
$ sleep
Sleep 10
$
完成步骤如下,首先在 Makefile 中将 sleep 程序添加到编译目录中
...
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\ # 表示新增的 sleep 用户程序
...
然后新建文件 user/sleep.c,写入下面的内容
#include "kernel/types.h"
#include "user/user.h"
int
main(int argc, char *argv[])
{
if(argc != 2){
printf("Sleep needs one argument!\n");
exit(-1);
}
int ticks = atoi(argv[1]);
sleep(ticks);
printf("Sleep %d\n", ticks);
exit(0);
}
启动 qemu xv6
make qemu
运行用户程序,现象应该如下
init: starting sh
$ sleep 10
Sleep 10
$ sleep
Sleep needs one argument!
$
进行测试
sodium@nas-MacBook-Air-13 xv6-oslabs-hitsz % ./grade-lab-util sleep
make: `kernel/kernel' is up to date.
== Test sleep, no arguments == sleep, no arguments: OK (1.7s)
== Test sleep, returns == sleep, returns: OK (0.8s)
== Test sleep, makes syscall == sleep, makes syscall: OK (1.0s)
任务2: pingpong
你需要在 user/pingpong.c 中实现两个进程之间基于管道的双向通信。
任务要求如下:
父进程向子进程发送一条消息。 子进程收到后打印 received ping 相关信息。 子进程再通过另一条管道把消息发回父进程。 父进程收到后打印 received pong 相关信息。 运行效果示例如下(以下命令在 XV6 命令行 中运行)
init: starting sh
$ pingpong
4: received ping from pid 3
3: received pong from pid 4
$ pingpong
6: received ping from pid 5
5: received pong from pid 6
$
编写思路:建立两个管道得到用于通信的文件描述符,通过 fork 得到子进程,使用 read(), write() 进行读写
本文编写的 pingpong.c 代码如下,可供参考
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc, char *argv[]) {
int p1[2];
int p2[2];
pipe(p1);
pipe(p2);
int ppid = getpid(); // 获取当前进程 pid
int cpid = fork(); // parent 这边会返回 child 的 pid
if (cpid == 0) { // child 这边会返回 0
close(p1[1]);
close(p2[0]); // 关闭用不到的文件描述符
cpid = getpid();
char read_buffer[256];
read(p1[0], read_buffer, 256); // [0] 是读端
close(p1[0]);
printf("%d: received %s from pid %d\n", cpid, read_buffer, ppid);
char write_string[] = "pong";
write(p2[1], write_string, 5); // [1] 是写端
close(p2[1]);
} else { // parent
close(p1[0]);
close(p2[1]);
char write_string[] = "ping";
write(p1[1], write_string, 5);
close(p1[1]);
char read_buffer[256];
read(p2[0], read_buffer, 256);
close(p2[0]);
printf("%d: received %s from pid %d\n", ppid, read_buffer, cpid);
}
exit(0);
}
同样记得修改 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\ # 表示新增的 sleep 用户程序
$U/_find\ # find
$U/_pingpong\ # pingpong
进行测试
sodium@nas-MacBook-Air-13 xv6-oslabs-hitsz % ./grade-lab-util pingpong
make: `kernel/kernel' is up to date.
== Test pingpong lenient testing == pingpong lenient testing: OK (1.3s)
== Test pingpong strict testing with changing pids == pingpong strict testing with changing pids: OK (0.8s)
任务3: find
实验要求编写用户程序 find.c ,命令格式是
$ find <path> <name>
使用示例如下
查找文件
$ echo > b
$ find . b
./b
查找目录
$ mkdir c
$ find . c
./c
递归查找
$ mkdir a
$ echo > a/target
$ mkdir a/b
$ echo > a/b/target
$ echo > c/target
$ find . target
./c/target
./a/target
./a/b/target
指导书提供了下面的思路:
- 新建
user/find.c。 - 先参考
user/ls.c,理解目录是怎样被打开和读取的。 - 按
find <path> <name>的形式读取参数。 - 使用
open()和fstat()判断当前路径是普通文件还是目录。 - 如果是目录,就依次读取目录项;如果名字匹配,就输出路径。
- 如果读到的还是目录,就递归继续查找。
- 递归时一定要跳过
.和..。 - 别忘了把程序加入
UPROGS
该程序涉及到的数据结构和宏定义如下
// Directory is a file containing a sequence of dirent structures.
#define DIRSIZ 14
struct dirent {
ushort inum;
char name[DIRSIZ];
};
// stat.h
#define T_DIR 1 // Directory
#define T_FILE 2 // File
#define T_DEVICE 3 // Device
struct stat {
int dev; // File system's disk device
uint ino; // Inode number
short type; // Type of file
short nlink; // Number of links to file
uint64 size; // Size of file in bytes
};
// fcntl.h
#define O_RDONLY 0x000
#define O_WRONLY 0x001
#define O_RDWR 0x002
#define O_CREATE 0x200
#define O_TRUNC 0x400
涉及到的新接口如下
int fstat(int fd, struct stat*); // syscall
int stat(const char *n, struct stat *st); // ulib.c
stat() 帮你完成了 open 和 close 的操作,这样你只需要提供文件/目录路径,就可以返回 struct stat 类型变量,从而得知相关信息。
那么给定一个路径,我们就得使用 stat() ,获取到的 struct stat 变量的 type 来判断它是文件( T_FILE )或者目录( T_DIR ),如果是文件,那么可以直接比较字符串;如果是目录,那么除了比较字符串,我们还需要递归调用函数进行查找。
对于字符串处理,xv6 提供了一些接口,以下为一部分
uint strlen(const char *s):返回字符串长度,判断依据是\0char *strcpy(char *s, const char *t):字符串拷贝,使用时需注意越界问题int strcmp(const char *p, const char *q):void *memmove(void*, const void*, int);
一个关键问题:怎么知道目录下有哪些文件?
目录本身也是文件,它的组成其实是许多的 struct dirent 数据,因此,我们只需要使用 read() 既可读取到文件名字,那么知道名字又可以判断是目录或者文件了。
// buf = "{path}/"
strcpy(buf, path);
p = buf + strlen(buf);
*p++ = '/';
while (read(pathfd, &de, sizeof(de)) == sizeof(de)) { // 读取目录,得到 dirent
if (de.inum == 0) continue;
if (!strcmp(de.name, "..") || !strcmp(de.name, ".")) {// 根据 dirent 名字,忽略掉 . 和 ..
continue;
}
if (!strcmp(de.name, name)) { // 对比文件/文件夹名称
printf("%s/%s\n", path, de.name);
}
memmove(p, de.name, DIRSIZ); // 这里 p 是 char* ,指向 buf 的末尾
p[DIRSIZ] = 0; // buf = "{path}/{de.name}\0"
if (stat(buf, &st) < 0) { // 根据合成的路径获取 stat
printf("ls: cannot stat %s\n", buf);
continue;
}
if (st.type == T_DIR) { // 发现指向的是目录,递归调用
if (find(buf, name) == -1) {
goto error;
}
}
}
完整的代码如下,可供参考
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fcntl.h"
#include "kernel/fs.h"
int find(char *path, char *name) {
struct dirent de;
struct stat st;
int pathfd;
char buf[512], *p;
if (strlen(path) + 1 + DIRSIZ + 1 > sizeof buf) {
fprintf(2, "%s path too long\n", path);
return -1;
}
if ((pathfd = open(path, O_RDONLY)) < 0) {
fprintf(2, "cannot open %s\n", path);
return -1;
}
if (fstat(pathfd, &st) < 0) {
fprintf(2, "cannot stat %s\n", path);
goto error;
}
if (st.type != T_DIR) {
fprintf(2, "%s is NOT a Directory!\n ", path);
goto error;
}
// buf = "{path}/"
strcpy(buf, path);
p = buf + strlen(buf);
*p++ = '/';
while (read(pathfd, &de, sizeof(de)) == sizeof(de)) {
// printf("DEBUG: name=[%s], target=[%s], inum=%d\n", de.name, name, de.inum);
if (de.inum == 0) continue;
if (!strcmp(de.name, "..") || !strcmp(de.name, ".")) {
continue;
}
if (!strcmp(de.name, name)) {
printf("%s/%s\n", path, de.name);
}
memmove(p, de.name, DIRSIZ);
p[DIRSIZ] = 0; // buf = "{path}/{de.name}\0"
if (stat(buf, &st) < 0) {
printf("ls: cannot stat %s\n", buf);
continue;
}
if (st.type == T_DIR) { // recursively search
if (find(buf, name) == -1) {
goto error;
}
}
}
return 0;
error:
close(pathfd);
return -1;
}
int main(int argc, char *argv[]) {
if (argc != 3) {
printf("Using Example: $find <path> <name>\n");
exit(-1);
}
char *path = argv[1];
char *name = argv[2];
if (find(path, name) == -1) {
exit(-1);
}
exit(0);
}
测试结果如下
sodium@nas-MacBook-Air-13 xv6-oslabs-hitsz % ./grade-lab-util find
make: `kernel/kernel' is up to date.
== Test find, in current directory and create a file == find, in current directory and create a file: OK (1.2s)
== Test find, in current directory and create a dir == find, in current directory and create a dir: OK (0.8s)
== Test find, find file recursive == find, find file recursive: OK (1.2s)
== Test find, find dir recursive with no duplicates == find, find dir recursive with no duplicates: OK (1.2s)
完成三个任务后,可以执行 make grade 进行评分,结果如下
== Test sleep, no arguments ==
$ make qemu-gdb
sleep, no arguments: OK (4.5s)
== Test sleep, returns ==
$ make qemu-gdb
sleep, returns: OK (1.2s)
== Test sleep, makes syscall ==
$ make qemu-gdb
sleep, makes syscall: OK (1.0s)
== Test pingpong lenient testing ==
$ make qemu-gdb
pingpong lenient testing: OK (1.0s)
== Test pingpong strict testing with changing pids ==
$ make qemu-gdb
pingpong strict testing with changing pids: OK (1.1s)
== Test find, in current directory and create a file ==
$ make qemu-gdb
find, in current directory and create a file: OK (1.0s)
== Test find, in current directory and create a dir ==
$ make qemu-gdb
find, in current directory and create a dir: OK (1.0s)
== Test find, find file recursive ==
$ make qemu-gdb
find, find file recursive: OK (1.3s)
== Test find, find dir recursive with no duplicates ==
$ make qemu-gdb
find, find dir recursive with no duplicates: OK (1.0s)
Score: 60/60
任务4: xv6 启动流程分析
任务要求学会使用命令行 GDB 跟踪 XV6 的启动流程,并在 GDB 命令行 中于两处关键位置打印当前进程名:
p cpus[$tp]->proc->name
其中,$tp 是当前 CPU 编号对应的寄存器值。你最终需要分别打印出:
- 初始进程 initcode
- exec("/init") 之后的 init
最终效果示例如下:
$1 = "initcode\000\000\000\000\000\000\000"
$2 = "init", '\000' <repeats 11 times>
实验要求把命令保存为 commands.gdb ,进入 gdb 之后输入 source commands.gdb 打印现象。
虽然指导书说:“这个脚本不一定要写得很长;如果思路清楚,最短仅需 6 行,同学们可以尝试自己精简一下。”,但实测五行命令就足以打印要求的 init 和 initcode
break forkret
c
p cpus[$tp]->proc->name
c
p cpus[$tp]->proc->name
gdb 使用
这里只介绍最基本的命令行 GDB 使用方法,先前提到了两个命令。
make qemu-gdb CPUS=1启动调试,在另一个终端窗口上可输入make gdb来进行对应调试make gdb对代码进行调试
这里进行示范,首先在一个终端窗口上输入 make qemu-gdb ,可以看到如下的现象
sodium@nas-MacBook-Air-13 xv6-oslabs-hitsz % make qemu-gdb
*** Now run 'gdb' in another window.
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 -S -gdb tcp::25501
接下来在另一个终端窗口(同样的项目文件夹下),输入 make gdb
sodium@nas-MacBook-Air-13 os-lab-submit % make GDB=riscv64-elf-gdb gdb
make: *** No rule to make target `gdb'. Stop.
sodium@nas-MacBook-Air-13 os-lab-submit % cd ..
sodium@nas-MacBook-Air-13 xv6 % cd xv6-oslabs-hitsz
sodium@nas-MacBook-Air-13 xv6-oslabs-hitsz % make GDB=riscv64-elf-gdb gdb
riscv64-elf-gdb
GNU gdb (GDB) 17.2
Copyright (C) 2025 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "--host=aarch64-apple-darwin25.4.0 --target=riscv64-elf".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
The target architecture is set to "riscv:rv64".
⚠️ warning: No executable has been specified and target does not support
determining executable automatically. Try using the "file" command.
0x0000000000001000 in ?? ()
(gdb)
随后便可以在这个窗口输入命令进行调试,常用的命令如下
| 命令 | 完整写法 | 作用 | 最常见场景 |
|---|---|---|---|
help 命令 | help 命令 | 查看命令帮助 | 忘记命令语法时 |
b 位置 | break 位置 | 设置断点 | 想在某个函数或某一行停下时 |
c | continue | 继续运行 | 从当前断点继续跑时 |
n | next | 源码单步,不进入函数 | 想按行排查,但先不进入函数时 |
s | step | 源码单步,会进入函数 | 想跟进函数内部时 |
ni / si | nexti / stepi | 汇编单步 | 调试系统调用、陷入与返回时 |
finish | finish | 运行到当前函数返回 | 已经进函数,但想直接回到调用处时 |
p 表达式 | print 表达式 | 打印变量或表达式 | 如 p p->pid |
x/格式 地址 | examine /格式 地址 | 查看指定地址的内存内容 | 如 x/10i $pc、x/16x 0x80000000 |
i r | info registers | 查看寄存器值 | 想看 pc、sp、a0 等寄存器时 |
bt | backtrace | 查看调用栈 | 想知道当前是怎么走到这里时 |
q | quit | 退出 GDB | 结束本次调试时 |
下面是一些使用示例
(gdb) source commands.gdb
Breakpoint 1 at 0x80001bb8: file kernel/proc.c, line 482.
Thread 1 hit Breakpoint 1, forkret () at kernel/proc.c:482
482 void forkret(void) {
$1 = "initcode\000\000\000\000\000\000\000"
Thread 1 hit Breakpoint 1, forkret () at kernel/proc.c:482
482 void forkret(void) {
$2 = "init", '\000' <repeats 11 times>
(gdb) b swtch
Breakpoint 2 at 0x80002758
(gdb) c
Continuing.
Thread 1 hit Breakpoint 2, 0x0000000080002758 in swtch ()
=> 0x0000000080002758 <swtch+0>: 00153023 sd ra,0(a0)
(gdb) i r pc
pc 0x80002758 0x80002758 <swtch>
(gdb) i r a0
a0 0x80011f30 2147557168
(gdb) n
Single stepping until exit from function swtch,
which has no line number information.
scheduler () at kernel/proc.c:437
437 c->proc = 0;
(gdb) p cpus[$tp]->proc->name
$3 = "init", '\000' <repeats 11 times>
(gdb) p $tp
$4 = (void *) 0x0
xv6 启动流程
xv6 的启动流程大致如下
- 进入
entry.S中的_entry函数- 获取 CPU ID(
mhartid),设置启动栈(sp)
- 获取 CPU ID(
- 进入
start.c中的start()函数- 设置 previous mode 为 s-mode,,设置
mepc,禁用页表(satp设置为 0),设置中断/异常委托,初始化计时器、UART等 - 完成之后通过
mret进入mepc设置的地址(main)
- 设置 previous mode 为 s-mode,,设置
- 进入
main.c中的main()函数,调用很多初始化函数- 其中一个关键函数是
userinit(),它会分配一个新的进程,也就是initcode进程,并设置为RUNNABLE - 初始化完成之后,进入
scheduler()函数
- 其中一个关键函数是
proc.c中的scheduler()函数会遍历进程列表,找到RUNNABLE进程执行,然后会使用swtch切换到进程的上下文中- 首先执行的用户程序是
initcode,它的内容是调用exec(init, argv),将程序加载到内存中,执行完毕之后这个进程就会变成init init用户程序的内容则是打开控制台,并且fork()+exec("sh", argv),也就是打开 shell,然后init会调用wait进入睡眠。- 如果
sh退出了,那么init会被唤醒,重新fork()+exec()启动 shell - 如果不是
sh退出,那么说明是孤儿进程,init不会管这个进程,继续wait进入睡眠
- 首先执行的用户程序是
sh的内容大致是读取用户输入,并且执行对应的程序