## 任务 | 测试项目 | 状态 | 分数 | 问题描述 | | -------------------------- | --- | ----- | ------------------------- | | sleep, no arguments | 通过 | 5/5 | 无 | | sleep, returns | 通过 | 5/5 | 无 | | sleep, makes syscall | 通过 | 10/10 | 无 | | pingpong | 通过 | 20/20 | 无 | | primes | 失败 | 0/20 | exec 失败,未输出素数 | | find, in current directory | 通过 | 10/10 | 无 | | find, recursive | 通过 | 10/10 | 无 | | xargs | 失败 | 0/19 | 'hello' 出现次数错误(期望3次,实际0次)| | time | 失败 | 0/1 | 缺少 time.txt 文件 | ## 知识点 1. `exit(0)`: - 表示程序正常退出 - 是约定俗成的成功状态码 - 通常意味着程序完成了它的任务且没有遇到错误 1. `exit(1)` (或任何非零值): - 表示程序异常退出 - 是错误状态码 - 通常用于表示程序遇到了错误或异常情况 ## 思想 [用户态、内核态](用户态、内核态.md) ## 在[Makefile](Makefile.md)添加条目 在 MIT 6.828 的 Lab1 中,你需要在 Makefile 里加入一些条目(targets、规则和变量)来编译和打包你要在 xv6 操作系统里运行的用户态程序。这么做的原因是: 1. **统一编译流程**:Makefile 是一个自动化编译工具。在 Lab1 里,你不光有内核代码(kernel code),也有用户态的测试程序(比如 pingpong、primes、xargs 等)。如果不在 Makefile 中指定这些程序的编译规则,`make` 就不会自动帮你编译、链接这些用户程序,也无法将它们打包进 xv6 的文件系统镜像中。换句话说,Makefile 里的改动让编译器和链接器知道该如何处理、产出对应的可执行文件。 2. **生成可执行文件和镜像文件系统**:xv6 的用户程序需要在实验给的 "用户态文件系统镜像" 中出现,这样系统在启动后才能从内核的 shell 中直接运行这些程序。Makefile 中增加的新内容会规定: - 如何从 `.c` 源文件生成可执行文件(如 `xargs`、`pingpong` 等)。 - 如何将编译好的用户程序打包进 xv6 的文件系统镜像(`fs.img`)。 没有这些信息,`make` 不会把用户程序放进系统镜像里,启动后你就无法在 xv6 中运行这些命令。 3. **实验要求与流程**:MIT 6.828 实验要求你不断丰富 xv6 的功能。早期的 Lab(如 Lab1)就会让你添加一些简单的用户态程序或工具,以测试、展示或检验你对内核和系统调用的理解。这些工具如果不通过 Makefile 进行适当配置,编译系统不清楚要处理哪些文件、生成什么目标文件,以及如何将其整合到可用的文件系统中。 总结来说,给 Makefile 增加相应条目是为了让你的用户态代码自动被编译、链接,并打包进 xv6 的镜像,从而在 xv6 环境中正常运行。这与 Java 项目中使用 `javac` 编译、`jar` 打包的流程是类似的,只不过这里是 C 程序和系统镜像的生成,而 Makefile 起到的作用有点类似于自动化构建脚本,让整个构建和打包过程自动化、可重复、可维护。 ## 具体实现 ### argv 和 argv 是什么的简称 argc 和 argv 是 C 语言中 main 函数的标准参数: - argc = "argument count" - 表示命令行参数的数量 - 至少为1(因为程序名本身就是第一个参数) - argv = "argument vector" - 是一个指向字符串数组的指针 - 存储所有的命令行参数 - argv[0] 是程序名 - argv[1] 到 argv[argc-1] 是实际的命令行参数 让我帮你分析一下 Lab1 Unix utilities 的实现思路。根据文档,这个 lab 包含几个主要任务: 1. sleep (easy) 2. pingpong (easy) 3. primes (moderate) 4. find (moderate) 5. xargs (moderate) 让我们一个一个来实现: ## 1. sleep (easy) 这是最简单的任务,需要实现一个类似 UNIX sleep 命令的程序。实现步骤: 1. 创建文件 `user/sleep.c`: ```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 ticks\n"); exit(1); } int ticks = atoi(argv[1]); sleep(ticks); exit(0); } ``` 1. 在 Makefile 的 UPROGS 中添加 _sleep ### 1. 为什么 argc 要等于 2? - argc 表示命令行参数的数量,包括程序名称 - 当我们运行 `sleep 10` 时: - argv[0]是 "sleep" (程序名) - argv[1]是 "10" (参数) - 所以总共应该有 2 个参数 - 如果 argc!= 2,说明用户输入格式不对,比如: - `sleep` (缺少参数,argc = 1) - `sleep 10 20` (参数过多,argc = 3) ### 2. 为什么要用 atoi 而不直接用 argv? - argv 中的参数都是字符串类型 (char*) - sleep 系统调用需要的是整数类型的 ticks - atoi 函数将字符串转换为整数,比如: - "10" -> 10 - "100" -> 100 ### 3. 为什么两个分支都需要 exit? - exit(1) 表示程序异常退出 - 用于参数错误的情况 - 返回非零值表示错误 - exit(0) 表示程序正常退出 - 用于程序完成正常功能后退出 - 返回 0 表示成功 - 如果不调用 exit: - 程序会继续执行到 main 函数结束 - 可能导致未定义行为 - 父进程无法知道子进程是否正确执行 ### 4. 为什么需要 include 这三个 有了他们,下面的代码才拥有 1.使用基本数据类型 2.使用[[文件系统]] 3.调用[[system call]]的能力 ```c // 定义了基本数据类型,比如:uint (无符号整型)uint64 (64位无符号整型)uchar (无符号字符) 这些是系统编程常用的类型定义 #include "kernel/types.h" // 定义了文件系统相关的结构体和常量, 包含文件状态信息(比如文件大小、创建时间等), 虽然在 sleep 程序中没有直接使用,但这是 xv6 用户程序的标准包含文件 #include "kernel/stat.h" //定义了用户程序可以使用的系统调用接口, 包含了 sleep、fork、exit 等函数的声明 #include "user/user.h" ``` ## 2. pingpong (easy) 需要实现一个使用管道在父子进程间传递字节的程序: ```c #include "kernel/types.h" #include "kernel/stat.h" #include "user/user.h" int main() { int p1[2], p2[2]; // Two pipes char buf[1]; pipe(p1); pipe(p2); if(fork() == 0) { // Child process close(p1[1]); close(p2[0]); read(p1[0], buf, 1); printf("%d: received ping\n", getpid()); write(p2[1], "x", 1); exit(0); } else { // Parent process close(p1[0]); close(p2[1]); write(p1[1], "x", 1); read(p2[0], buf, 1); printf("%d: received pong\n", getpid()); wait(0); exit(0); } } ``` ### 为啥 fork() == 0是子进程反之是父 fork() 的特点:调用一次,返回两次 - 在父进程中返回子进程的 PID(大于 0) - 在子进程中返回 0 ### read 和 write 方法的第三个参数啥意思,为啥都是 1 在 read 和 write 函数中,第三个参数表示要读取或写入的字节数。 ```c // 从文件描述符 fd 读取最多 n 个字节到 buf ssize_t read(int fd, void *buf, size_t n); // 向文件描述符 fd 写入 buf 中的 n 个字节 ssize_t write(int fd, const void *buf, size_t n); ``` ## 3. find (moderate) 实现一个简单版本的 UNIX find 命令。根据文档([1](https://pdos.csail.mit.edu/6.S081/2020/labs/util.html))的提示: 1. 参考 `user/ls.c` 来了解如何读取目录 2. 使用递归来遍历子目录 3. 不要递归进入 "." 和 ".." 4. 使用 strcmp() 来比较字符串 ```c #include "kernel/types.h" #include "kernel/stat.h" #include "user/user.h" #include "kernel/fs.h" void find(char *path, char *name) { char buf[512], *p; int fd; struct dirent de; struct stat st; if((fd = open(path, 0)) < 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: if(strcmp(path + strlen(path) - strlen(name), name) == 0) { printf("%s\n", path); } break; case T_DIR: if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf) { printf("find: path too long\n"); break; } 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; find(buf, name); } break; } close(fd); } int main(int argc, char *argv[]) { if(argc != 3) { fprintf(2, "Usage: find path name\n"); exit(1); } find(argv[1], argv[2]); exit(0); } ``` 我建议按照这个顺序逐个实现,每完成一个就测试一下。文档中提供了详细的测试用例([1](https://pdos.csail.mit.edu/6.S081/2020/labs/util.html))。 需要我继续说明 xargs 和 primes 的实现吗?