MIT 6.S081 Lab 1. Xv6 and Unix utilities

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.cuser/grep.cuser/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

你的解决方案应该产生以下输出(当文件系统包含文件 ba/ba/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"。

一些提示:

  • 使用 forkexec 在每个文件上调用命令。在父级使用 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);
}
相关推荐
大模型铲屎官8 小时前
【操作系统-Day 46】文件系统核心探秘:深入理解连续分配与链式分配的实现与优劣
人工智能·python·深度学习·大模型·操作系统·文件系统·计算机组成原理
大模型铲屎官10 小时前
【操作系统-Day 47】揭秘Linux文件系统基石:图解索引分配(inode)与多级索引
linux·运维·服务器·人工智能·python·操作系统·计算机组成原理
一个平凡而乐于分享的小比特10 小时前
操作系统中的“千年虫”
操作系统·千年虫
Hello_MyDream1 天前
继承和线程
操作系统
序属秋秋秋1 天前
《Linux系统编程之进程控制》【进程替换】
linux·c语言·c++·操作系统·进程·系统编程·进程替换
lcreek1 天前
Linux虚拟文件系统(VFS)核心架构解析
linux·操作系统
胡萝卜3.01 天前
程序构建核心解析:从预处理到链接的完整指南
运维·服务器·c++·人工智能·操作系统·编译原理·系统编成
♛识尔如昼♛2 天前
操作系统(1)第一章- 操作系统的概念和功能
操作系统
序属秋秋秋2 天前
《Linux系统编程之进程控制》【进程创建 + 进程终止】
linux·c语言·c++·操作系统·进程·进程创建·进程终止