Lab1
Boot xv6 (easy)
bash
git clone git://g.csail.mit.edu/xv6-labs-2025
cd xv6-labs-2025
make qemu # Build and run xv6
运行后进入qemu,要退出 qemu,请键入:Ctrl-a x(同时按下Ctrl和a后松开,然后按下x)
sleep (easy)
本练习旨在让您熟悉如何在 xv6 上编写用户程序以及pause系统调用。
为 xv6实现一个用户级
睡眠程序,类似于 UNIX 的 sleep 命令。你的睡眠程序应该暂停用户指定的时钟周期数。时钟周期是 xv6 内核定义的一个时间单位,即定时器芯片两次中断之间的时间间隔。你的代码应该写在``user/sleep.c文件中 。
提示:
-
在开始编写代码之前,请阅读xv6 书的第一章。
-
把你的代码放在
user/sleep.c中。看看``user/目录下的其他一些程序 (例如user/echo.c、user/grep.c和user/rm.c),了解命令行参数是如何传递给程序的。 -
将您的睡眠程序添加到 Makefile 中的UPROGS;完成此操作后,make qemu将编译您的程序,您就可以从 xv6 shell 运行它。 -
如果用户忘记传递参数,sleep 应该打印错误消息。
-
命令行参数以字符串形式传递;您可以使用
atoi将其转换为整数(请参阅 user/ulib.c)。 -
使用系统调用
pause()。 -
有关实现pause()系统调用的 xv6 内核代码,请参阅kernel/sysproc.c(查找sys_pause);有关用户程序可调用的pause()的 C 定义,请参阅user/user.h;有关从用户代码跳转到内核以``调用 pause() 的汇编代码,请参阅user/usys.S。```````````` -
请参阅 Kernighan 和 Ritchie 合著的*《C 程序设计语言》(第二版)*(K&R)一书,了解 C 语言。
My Solution:
c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int
main(int argc, char *argv[])
{
if(argc != 2) {
fprintf(2, "usage: sleep <time>\n");
exit(1);
}
pause(atoi(argv[1]));
exit(0);
}
sixfive (moderate)
在本练习中,您将使用系统调用 open 和 read、C 字符串以及在 C 中处理文本文件。
对于每个输入文件,sixfive必须打印文件中所有 5 或 6 的倍数。数字是由字符串"-\r\t\n./,""中字符分隔的十进制数字序列。因此,对于"xv6"中的"6",sixfive 不应该打印 6,而应该打印例如"/6,""。
以下示例说明了 sixfive 的行为:
bash
$ sixfive sixfive.txt
5
100
18
6
$
提示:
- 一次读取一个字符到输入文件。
- 您可以使用strchr测试字符是否与任何分隔符匹配(请参阅 user/ulib.c)。
- 文件开头和结尾是隐式分隔符。
My Solution:
c
#include "kernel/types.h"
#include "user/user.h"
#include "kernel/fcntl.h"
// 定义状态
#define ST_READY 0 // 准备好接收新数字
#define ST_DIGITS 1 // 正在读数字
#define ST_INVALID 2 // 非法字符状态
void sixfive(int fd) {
char c;
int state = ST_READY;
long long current_val = 0;
char *seps = " -\r\t\n./,";
// 逐字符读取,这是题目给出的 Hint
while (read(fd, &c, 1) > 0) {
if (strchr(seps, c)) {
// 遇到分隔符,如果之前正在读有效数字,则判断并打印
if (state == ST_DIGITS) {
if (current_val % 5 == 0 || current_val % 6 == 0) {
printf("%lld\n", current_val);
}
}
// 遇到分隔符后,重置状态为 READY
state = ST_READY;
current_val = 0;
} else if (c >= '0' && c <= '9') {
// 遇到数字
if (state == ST_READY || state == ST_DIGITS) {
state = ST_DIGITS;
current_val = current_val * 10 + (c - '0');
}
// 如果本来就是 INVALID 状态,则保持 INVALID
} else {
// 遇到既非分隔符也非数字的字符(如 'v')
state = ST_INVALID;
current_val = 0;
}
}
// 隐式分隔符:处理文件末尾的情况
if (state == ST_DIGITS) {
if (current_val % 5 == 0 || current_val % 6 == 0) {
printf("%lld\n", current_val);
}
}
}
int main(int argc, char *argv[]) {
int fd, i;
if (argc < 2) {
fprintf(2, "usage: sixfive files...\n");
exit(1);
}
for (i = 1; i < argc; i++) {
if ((fd = open(argv[i], O_RDONLY)) < 0) {
fprintf(2, "sixfive: cannot open %s\n", argv[i]);
exit(1);
}
sixfive(fd);
close(fd);
}
exit(0);
}
memdump (easy)
这个练习将帮助你更多练习使用 C 指针。开始之前,请阅读《C 程序设计语言(第二版)》中 Kernighan 和 Ritchie(K&R)所著的 5.1 节(指针和地址)到 5.6 节(指针数组)以及 6.4 节(指向结构的指针)。
看看 user/memdump.c 。你的任务是实现这个函数 memdump(char *fmt, char *data) 。 memdump() 的目的是按照 fmt 参数描述的格式打印 data 指向的内存内容。格式是一个 C 字符串。字符串的每个字符指示如何打印 data 的连续部分。因此,例如,一个具有多个字段的 C 结构体可以通过包含多个字符的格式字符串来打印。
你的 memdump()应该处理以下格式字符:
- i: 将接下来的 4 个字节的数据以十进制形式打印为 32 位整数。
- p: 将接下来的 8 个字节的数据以十六进制形式打印为 64 位整数。
- h: 将接下来的 2 个字节的数据以十进制形式打印为 16 位整数。
- c: 将接下来的 1 个字节的数据以 8 位 ASCII 字符形式打印。
- s: 接下来的 8 个字节包含一个指向 C 字符串的 64 位指针;打印该字符串。
- S: 剩余的数据包含一个空终止的 C 字符串的字节;打印该字符串。
你可以自由使用 C 的 printf() 中的 memdump() 。
如果 memdump 程序没有参数被执行,它会调用 memdump() 传递一些示例格式字符串和数据。如果 memdump() 正确实现,输出将会是:
yaml
$ memdump
Example 1:
61810
2025
Example 2:
a string
Example 3:
another
Example 4:
BD0
1819438967
100
z
xyzzy
Example 5:
hello
w
o
r
l
d
你可能会得到 Example 4 输出第一行的不同十六进制地址。
如果 memdump 程序带参数被调用,它会读取其标准输入直到文件结束,然后调用 memdump() ,并传递格式和输入数据。所以,一旦 memdump() 被实现:
shell
$ echo deadc0de | memdump hhcccc
25956
25697
c
0
d
e
$ echo deadc0de | memdump p
64616564
$
实现 memdump() 。
My Solution:
c
void memdump(char* fmt, char* data) {
// Your code here.
char* p = fmt;
char* curr = data; // 使用一个临时指针来遍历数据
while (*p) {
if (*p == 'i') {
// 打印 4 字节整数
printf("%d\n", *(int*)curr);
curr += 4;
} else if (*p == 'p') {
// 打印 8 字节十六进制整数
printf("%lx\n", *(uint64*)curr);
curr += 8;
} else if (*p == 'h') {
// 打印 2 字节短整数
// 注意:xv6 的 printf 可能需要 %d 来打印 short
printf("%d\n", *(short*)curr);
curr += 2;
} else if (*p == 'c') {
// 打印 1 字节字符
printf("%c\n", *curr);
curr += 1;
} else if (*p == 's') {
// 数据里存的是一个指向字符串的 8 字节指针
char* str_ptr = *(char**)curr;
printf("%s\n", str_ptr);
curr += 8;
} else if (*p == 'S') {
// 数据里直接就是字符串的内容
printf("%s\n", curr);
// 'S' 标志通常意味着处理剩余的所有数据,或者直到遇到 \0
// 根据题目描述 "the rest of the data",打印后可以结束
return;
}
p++;
}
}
find (moderate)
这个练习探讨了路径名和目录,以及系统调用 open、read 和 fstat。
为 xv6 编写一个简单的 UNIX find 程序:查找目录树中所有具有特定名称的文件。你的解决方案应保存在文件 user/find.c 中。
一些提示:
- 查看 user/ls.c,了解如何读取目录。
- 使用递归使 find 能够深入子目录。
- 不要递归进入"."和".."。
- 每次你调用 make qemu ,它都会构建一个新的 fs.img,删除先前运行中创建的文件。如果你希望使用先前使用的文件系统启动 qemu,请使用 make qemu-fs 。
- 你需要使用 C 字符串。例如查看 K&R(C 语言书籍)中的第 5.5 节。
- 注意,== 不像 Python 那样比较字符串。请使用 strcmp()。
- 将程序添加到 Makefile 中的
UPROGS。
你的解决方案应该产生以下输出(当文件系统包含文件 b 、 a/b 和 a/aa/b 时):
less
$ 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 来看看我们的测试会怎么认为。
My Solution:
需要注意#include "kernel/types.h"必须放在最前面,因为其他头文件中没有引入"kernel/types.h",使用uint等类型会报错
c
#include "kernel/types.h"
#include "kernel/fcntl.h"
#include "kernel/fs.h"
#include "kernel/stat.h"
#include "user/user.h"
// 获取路径中的文件名部分(指向 path 字符串中最后一个 '/' 之后的字符)
// 例如: "a/b/c" -> "c", "file" -> "file"
char* fmtname(char* path) {
char* p;
// 从字符串末尾开始向前查找第一个 '/'
for (p = path + strlen(path); p >= path && *p != '/'; p--);
p++; // 移动到 '/' 之后的字符,或者如果没找到 '/' 则指向 path 开头
return p;
}
void find(char* path, char* target) {
char buf[512], *p;
int fd;
struct dirent de;
struct stat st;
// 打开路径
if ((fd = open(path, O_RDONLY)) < 0) {
fprintf(2, "find: cannot open %s\n", path);
return;
}
// 获取文件状态
if (fstat(fd, &st) < 0) {
fprintf(2, "find: cannot stat %s\n", path);
close(fd);
return;
}
switch (st.type) {
case T_FILE:
// 如果是文件,检查文件名是否匹配
// fmtname(path) 获取当前路径的文件名部分
if (strcmp(fmtname(path), target) == 0) {
printf("%s\n", path);
}
break;
case T_DIR:
// 如果是目录,检查路径长度是否溢出缓冲区
if (strlen(path) + 1 + DIRSIZ + 1 > sizeof buf) {
printf("find: path too long\n");
break;
}
// 构造当前目录的路径前缀,例如 "a/"
strcpy(buf, path);
p = buf + strlen(buf);
*p++ = '/';
// 循环读取目录项
while (read(fd, &de, sizeof(de)) == sizeof(de)) {
if (de.inum == 0) continue;
// 【关键】必须跳过 "." 和 "..",否则会无限递归
if (strcmp(de.name, ".") == 0 || strcmp(de.name, "..") == 0)
continue;
// 将目录项名称拼接到路径后,例如 "a/b"
memmove(p, de.name, DIRSIZ);
p[DIRSIZ] = 0; // 确保字符串以 null 结尾
// 递归调用 find
find(buf, target);
}
break;
}
close(fd);
}
int main(int argc, char* argv[]) {
if (argc != 3) {
fprintf(2, "Usage: find <path> <name>\n");
exit(1);
}
// 调用 find 函数,传入路径和目标文件名
find(argv[1], argv[2]);
exit(0);
}
exec (moderate)
这个练习涉及系统调用 fork、exec 和 wait。
向 find 添加一个 "-exec cmd ",它将针对 find 找到的每个文件 f 执行程序 " cmd file ",而不是打印匹配的文件名。
以下示例说明 find -exec 行为: 注意这里的命令是"echo hi"和文件 是 "./wc",使得命令为 "echo hi ./wc", 它输出 "hi ./wc"。
一些提示:
- 使用
fork和exec在每个文件上调用命令。在父级使用wait等待孩子完成指令。 - kernel/param.h 声明了 MAXARG,如果你需要声明一个 argv 数组,这可能很有用。
要测试你的 find 解决方案,请运行 shell 脚本 findtest.sh。你的解决方案应该产生以下输出:
ruby
$ make qemu
...
init: starting sh
$ sh < findtest.sh
$ echo DONE
$ $ $ $ $ hello
hello
hello
$ $
输出中有 很多 $ ,因为 xv6 shell 感觉它正在处理来自文件的命令而不是来自控制台,并且为文件中的每个命令打印一个 $ 。
My Solution:
c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
#include "kernel/fcntl.h"
#include "kernel/param.h" // 提供 MAXARG 定义
// 获取路径中的文件名部分
char*
fmtname(char *path)
{
char *p;
for(p=path+strlen(path); p >= path && *p != '/'; p--)
;
p++;
return p;
}
// 运行 exec 命令
// cmd_argv: 用户在命令行提供的命令参数 (例如 {"echo", "hi"})
// cmd_argc: 命令参数的个数
// file_path: 当前找到的文件路径 (例如 "./a/b")
void
run_exec(char **cmd_argv, int cmd_argc, char *file_path)
{
char *argv[MAXARG];
int i;
// 1. 检查参数数量是否溢出
// 需要 cmd_argc 个原参数 + 1 个文件路径 + 1 个 NULL 结束符
if(cmd_argc + 2 > MAXARG){
fprintf(2, "find: too many arguments for exec\n");
return;
}
// 2. 构造新的 argv 数组
// 复制原本的命令参数
for(i = 0; i < cmd_argc; i++){
argv[i] = cmd_argv[i];
}
// 追加当前文件路径
argv[i] = file_path;
// 追加 NULL 结束符
argv[i+1] = 0;
// 3. Fork 和 Exec
int pid = fork();
if(pid == 0){
// 子进程
exec(argv[0], argv);
// 如果 exec 返回,说明出错了
fprintf(2, "find: exec %s failed\n", argv[0]);
exit(1);
} else if (pid > 0){
// 父进程等待子进程结束
wait(0);
} else {
fprintf(2, "find: fork failed\n");
}
}
// 查找函数
// path: 当前搜索路径
// target: 目标文件名
// exec_argv: 如果非空,则指向 -exec 后的命令参数数组
// exec_argc: 命令参数个数
void
find(char *path, char *target, char **exec_argv, int exec_argc)
{
char buf[512], *p;
int fd;
struct dirent de;
struct stat st;
if((fd = open(path, O_RDONLY)) < 0){
fprintf(2, "find: cannot open %s\n", path);
return;
}
if(fstat(fd, &st) < 0){
fprintf(2, "find: cannot stat %s\n", path);
close(fd);
return;
}
// 检查是否匹配
// 逻辑:如果是文件,且名字匹配 target,则处理
// 注意:标准 find 会在目录匹配时也执行,但根据题目输出示例,
// 主要是针对找到的 target 执行。这里我们保持文件名的匹配逻辑。
if(st.type == T_FILE && strcmp(fmtname(path), target) == 0){
if(exec_argv != 0){
// 如果有 -exec 选项,执行命令
run_exec(exec_argv, exec_argc, path);
} else {
// 否则,打印路径
printf("%s\n", path);
}
}
// 如果是目录,则递归
if(st.type == T_DIR){
if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf){
printf("find: path too long\n");
} else {
strcpy(buf, path);
p = buf+strlen(buf);
*p++ = '/';
while(read(fd, &de, sizeof(de)) == sizeof(de)){
if(de.inum == 0)
continue;
if(strcmp(de.name, ".") == 0 || strcmp(de.name, "..") == 0)
continue;
memmove(p, de.name, DIRSIZ);
p[DIRSIZ] = 0;
// 递归调用,传递 exec 参数
find(buf, target, exec_argv, exec_argc);
}
}
}
close(fd);
}
int
main(int argc, char *argv[])
{
if(argc < 3){
fprintf(2, "Usage: find <path> <name> [-exec <cmd>...]\n");
exit(1);
}
char *path = argv[1];
char *target = argv[2];
char **exec_argv = 0;
int exec_argc = 0;
// 检查是否有 -exec 参数
if(argc > 3){
if(strcmp(argv[3], "-exec") == 0){
// argv[4] 开始是命令,例如 {"echo", "hi"}
exec_argv = &argv[4];
exec_argc = argc - 4; // 计算命令参数个数
} else {
fprintf(2, "find: syntax error, expected -exec\n");
exit(1);
}
}
find(path, target, exec_argv, exec_argc);
exit(0);
}