MIT6.S081笔记:Lab1 Xv6 And Unix Utilities
关于 MIT 6.S081
这门课的前身是 MIT 著名的课程 6.828,MIT 的几位教授为了这门课曾专门开发了一个基于 x86 的教学用操作系统 JOS,被众多名校作为自己的操统课程实验。但随着 RISC-V 的横空出世,这几位教授又基于 RISC-V 开发了一个新的教学用操作系统 xv6,并开设了 MIT6.S081 这门课。由于 RISC-V 轻便易学的特点,学生不需要像此前 JOS 一样纠结于众多 x86 “特有的”为了兼容而遗留下来的复杂机制,而可以专注于操作系统层面的开发。
代码环境
Xv6 是在 x86 处理器上( x 即指 x86 )用 ANSI 标准 C 重新实现的 Unix 第六版(Unix V6,通常直接被称为 V6)。
编译与执行
cd xv6-labs-2020
git checkout util # 切换到对应分支 https://pdos.csail.mit.edu/6.828/2020/labs/util.html
make qemu # 启动 qemu
# ctrl+p 查看 xv6 中的进程
# ctrl + a x 退出 qemu
在 xv6-labs-2020 文件夹下执行
./grading-lab-util # 进行测试
学习笔记
记录学习这门课时的一些笔记,包括阅读相关材料的一些摘录,以及完成 lab 时的思路和代码。
Operating System Interfaces
这章主要介绍了操作系统提供的一些接口,通过这些接口,让用户可以去执行内核中的函数。这些函数称为系统调用。
shell 的工作原理
以 lab 中的 user/sh.c 为例
main 函数经过一些列的环境检查,fork 出一个子进程,子进程调用 exec 执行 shell 中给出的命令。
- getcmd 函数接受命令行中的语句,如 ls / 。
- 子进程执行 runcmd("ls /") 函数,父进程执行 wati(0) 函数等待子进程结束,并回收资源。
- 子进程在 runcmd 函数中执行到 exec(ls, /) 系统调用函数,完成命令。
过程如下图所示:
命令行中管道符工作原理
管道是内核中的一块 buffer , 以一对文件描述符的方式暴露给进程。
int p[2];
pipe(p);
p[0] // 负责将从管道读出数据
p[1] // 负责从管道写入数据
为了实现下图中划红线的命令:
xv6 的 shell 使用如下代码实现:
为了更好地理解(user/sh.c 100),也就是上图中所示的代码,写了下图近似代码通过打印日志来分析执行流程。
通过输出知道代码的执行过程的进程的父子关系如下图所示:
- 把标准输出关联到管道p[1],运行管道符左侧的命令,grep fork sh.c 会将结果写入的标准输出,但实际上是写到了 p[1];
- 把标准输入关联到管道pp[0],运行管道符右侧的命令(对于grep fork sh.c),其没有右侧命令,所以结束返回,如果有管道符如(b | c),则会继续创建子进程进行处理。
- 把标准输入关联到管道p[0],运行管道符右侧的命令(也就是 wc -l),wc -l 会从标准输出读入数据,但其实是从 p[0] 读入。
Other Note
这段讨论的是不使用子进程来执行 | 左右两边的命令,这么做会需要额外的代码来保证左右两边的命令执行的先后顺序。
Lab Unix Utilities
这个 lab 的主要任务是实现一些 xv6 的应用层工具。
sleep
实现 sleep 命令,实现代码如下图所示
在第 26 行会执行 sleep 系统调用,用于进入到系统的内核态执行 sleep 相关的内核函数,来完成进程的睡眠功能。
pingpong
申请 2 对管道,用于两个进程直接的通信。原理如下图所示:
注意关闭不使用的管道,xv6 所能使用的文件描述符有限。
primes
使用子进程和管道筛选素数,详细介绍。
总体思路为:
- 申请一对管道,第一个父进程写入 2 - 35 到管道当中。父进程在写完 2 - 35 后关闭管道,并 wait 等待子进程结束。
- 开启一个子进程,子进程读入的第一个数是从父进程处,通过管道得到的素数,记为 cur_prime,并打印。
- 子进程,申请与其子进程的管道,子进程从其父进程继续读入后续数字,同时开启当前子进程的子进程,并将其中不是 cur_prime 倍数的数依次写入其子进程的管道当中。写完之后,关闭管道,调用 wait() 等待其子进程结束。
find
需要注意的是最后一行,当 dirent 的 inum == 0 的时候,说明这个目录是空的。
memove() 函数用于复制字符串,当源字符串和目的字符串的地址空间有重合的时候,可以保证源字符能不被破坏的复制到目的字符串。复制完成后,源字符串中的内容发生改变。
代码可仿照 ls.c 来进行实现。注意当遍历到子文件夹时,需要进行递归处理。
#include "kernel/types.h"
#include "user/user.h"
#include "kernel/fs.h"
#include "kernel/stat.h"
void find(char* path, char* file_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;
}
while (read(fd, &de, sizeof(de)) == sizeof(de)) {
strcpy(buf, path);
p = buf + strlen(buf);
*p++ = '/';
if (de.inum == 0) {
continue;
}
memmove(p, de.name, DIRSIZ); // full path ex. path/item_name
p[DIRSIZ] = 0;
if(stat(buf, &st) < 0) {
printf("find: cannot stat %s\n", buf);
continue;
}
switch(st.type) {
case T_FILE: {
if (strcmp(de.name, file_name) ==0) { // find the file
printf("%s\n", buf);
}
break;
}
case T_DIR: {
if (strcmp(de.name, ".") != 0 && strcmp(de.name, "..") != 0) {
find(buf, file_name); // recursive
}
break;
}
}
}
close(fd);
}
int main(int argc, char *argv[])
{
if (argc != 3) {
printf("There should be one path argument and one filename argument\n");
exit(0);
}
char* path_name = argv[1];
char* file_name = argv[2];
find(path_name, file_name);
exit(0);
}
xargs
从标准输入读入一行数据,作为 xargs 命令后的命令的输入。
echo hello too | xargs echo bye
# 在终端打印 byte hello too
- 如上所示代码,从左往右执行。执行完 echo hello too 会向标准输入写入 hello too,由于后接管道符,所以标准输入被重定向到一个管道当中。
- 随后执行 xargs 命令,其实现代码中的 argc 为 3,argv[] 为 "xargs", "echo", "byte"。
- 由于 xargs 要实现的功能为将前一个命令写入管道的字符作为其输入。
- 所以当执行 xargs 程序时,其实际的 argv 数组为 "xargs", "echo", "byte", "hello", "too"。
- 这个程序的实现最核心的是要为 xargs 命令组装新的 argv 数组。然后 fork 出一个子进程调用 exec 执行 argv[1] 所使用的命令(这里是 echo),然后将其新的参数数组 argv 也传入 exec 函数中。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "kernel/param.h"
#include "user/user.h"
#define MAXLEN 100
int main(int argc, char* argv[])
{
if (argc <= 1) {
fprintf(2, "illegal argument number\n");
}
char* cmd = argv[1]; // 记录命令行输入的命令
char buf;
char argv_list[MAXARG][MAXLEN];
char* new_argv_list[MAXARG]; // 记录拼接完后的新参数列表
while(1) {
memset(argv_list, 0, MAXARG * MAXLEN);
for (int i = 1; i < argc; ++i) {
strcpy(argv_list[i-1], argv[i]); // 将除命令名之外的字符串,复制给 argv_list
}
int cur_argc = argc - 1;
int offset = 0;
int is_read = 0;
while((is_read = read(0, &buf, 1))) > 0) { // 逐个读入字符
if (buf == ' ') {
cur_argc++;
offset = 0;
continue;
}
if(buf == '\n') {
break; // 读完当前行
}
if (offset == MAXLEN) {
fprintf(2, "parameter too long\n");
}
if (cur_argc == MAXARG) {
fprintf(2, "too many argument\n");
}
// 将从 stdin中读取的参数,填到原 xargs 命令的参数后面
argv_list[cur_argc][offset++] = buf;
}
if (is_read <= 0) {
break; // 管道符之前的命令执行结果读取结束
}
// 组装新的 char* argv[] 参数
for (int i = 0; i <= cur_argc; ++i) {
new_argv_list[i] = argv_list[i];
}
if (fork() == 0) {
exec(cmd, new_argv_list);
exit(1);
}
wait(0);
}
exit(0);
}
References
- https://csdiy.wiki/操作系统/MIT6.S081/?h=mit
- https://blog.csdn.net/u013577996/article/details/108680888
- https://fanxiao.tech/posts/MIT-6S081-notes/
- https://blog.miigon.net/posts/s081-lab1-unix-utilities/
- https://www.yichuny.page/posts/6s081lab-utilities/
词汇
the downside of 缺点
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧