xv6项目开源—01
参考链接:xv6 第一章 | kiloGrand (gitee.io)
理论:
1)xv6使用了传统形式的内核——一个向其他运行中的程序提供服务的特殊程序。每一个正在运行的程序(称为进程),都拥有自己的包含指令、数据、栈的内存空间。指令实现程序的运算,数据是用于运算过程的变量,栈则管理程序的过程调用。一台计算机通常有许多进程,但只有一个内核。
xv6系统的所有调用接口:
系统调用 | 描述 |
---|---|
int fork() | 创建一个进程,返回子进程的PID |
int exit(int status) | 调用了_exit(),终止当前进程,并将status传递给wait()。 |
int wait(int *status) | 等待子进程结束,并将status接收到参数*status中,返回其PID |
int kill(int pid) | 终止给定PID的进程,成功返回0,失败返回-1 |
int getpid() | 返回当前进程的PID |
int sleep(int n) | 睡眠n个时钟周期 |
int exec(char *file, char *argv[]) | 通过给定参数加载并执行一个文件;只在错误时返回 |
char *sbrk(int n) | 使进程内存增加n字节,返回新内存的起始地址 |
int open(char *file, int flags) | 打开一个文件,flags表示读或写,返回fd(文件描述符) |
int write(int fd, char *buf, int n) | 将buf中n字节写入到文件描述符中;返回n |
int read(int fd, char *buf, int n) | 从文件描述符中读取n字节到buf;返回读取字节数,文件结束返回0 |
int close(int fd) | 释放文件描述符fd |
int dup(int fd) | 返回一个新文件描述符,其引用与fd相同的文件 |
int pipe(int p[]) | 创建管道,将读/写文件描述符放置在p[0]和p[1] |
int chdir(char *dir) | 改变当前目录 |
int mkdir(char *dir) | 创建新目录 |
int link(char *file1, char * file2) | 为文件file1创建一个新的名称file2 |
2)close系统调用会释放一个文件描述符。新分配的文件描述符总是当前进程中最小的未使用描述符。
3)管道是一个小的内核缓冲区,作为一对文件描述符提供给进程,一个用于读,一个用于写。将数据写入管道的一端就可以从管道的另一端读取数据。管道为进程提供了一种通信方式。
4)xv6文件系统包含了数据文件(拥有字节数组)和目录(拥有对数据文件和其他目录的命名引用)
安装:
# 按官方指南手册 安装必须的工具链
$ sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu
# 单独移除掉qemu的新版本, 因为不知道为什么build时候会卡壳
$ sudo apt-get remove qemu-system-misc
# 额外安装一个旧版本的qemu
$ wget https://download.qemu.org/qemu-5.1.0.tar.xz
$ tar xf qemu-5.1.0.tar.xz
$ cd qemu-5.1.0
$ ./configure --disable-kvm --disable-werror --prefix=/usr/local --target-list="riscv64-softmmu"
$ make
$ sudo make install
# 克隆xv6实验仓库
$ git clone git://g.csail.mit.edu/xv6-labs-2020
$ cd xv6-labs-2020
$ git checkout util
# 进行编译
$ make qemu
# 编译成功并进入xv6操作系统的shell
$ xv6 kernel is booting
$ hart 2 starting
$ hart 1 starting
$ init: starting sh
$ (shell 等待用户输入...)
# 尝试一下ls指令
$ ls
. 1 1 1024
.. 1 1 1024
README 2 2 2226
xargstest.sh 2 3 93
cat 2 4 23680
echo 2 5 22536
forktest 2 6 13256
grep 2 7 26904
init 2 8 23320
kill 2 9 22440
ln 2 10 22312
ls 2 11 25848
mkdir 2 12 22552
rm 2 13 22544
sh 2 14 40728
stressfs 2 15 23536
usertests 2 16 150160
grind 2 17 36976
wc 2 18 24616
zombie 2 19 21816
console 3 20 0
utility功能:
1、sleep
第一个utility功能是调用系统函数sleep来实现用户函数sleep. 主要是让我们熟悉一下怎么在这个xv6环境里写代码. 需要注意的是, 在这一系列的实验里, 我们是无法调用传统意义上的C标准库的. 所有能够调用的系统函数都在user/user.h里.
代码:
#include "kernel/types.h"
#include "user/user.h"
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(2, "usage: sleep [ticks num]\n");
exit(1);
}
// atoi sys call guarantees return an integer
int ticks = atoi(argv[1]);
int ret = sleep(ticks);
exit(ret);
}
/*
#include "kernel/types.h" 和 #include "user/user.h" 是包含了一些必要的头文件,其中包含了系统类型定义和一些用户级系统调用的函数原型。
int main(int argc, char *argv[]) 是程序的入口函数,argc是命令行参数的个数,argv是一个字符串数组,存储了命令行参数。
if (argc != 2) 检查命令行参数的个数是否等于2个,即除了程序名之外,还需要传入一个参数(就是要睡眠的时钟滴答数)。如果不等于2,则输出usage提示,然后调用exit(1)退出程序。
int ticks = atoi(argv[1]) 使用atoi函数将命令行参数(字符串形式)转换为整数,存储在ticks变量中。这个值就是要睡眠的时钟滴答数。
int ret = sleep(ticks) 调用sleep系统调用,传入ticks参数,让程序睡眠指定的时钟滴答数。sleep系统调用返回一个整数值,表示剩余未睡眠的时钟滴答数。
exit(ret) 将sleep系统调用的返回值作为程序的退出状态码。如果ret为0,表示睡眠完成,否则表示由于某些原因(如被信号中断)而未完全睡眠。
2、pingpong
第二个utility功能pingpong是利用系统调用函数fork和pipe在父进程和子进程前交换一个字节
代码:
#include "kernel/types.h"
#include "user/user.h"
int main(int argc, char *argv[]) {
int pid;
int pipes1[2], pipes2[2];
char buf[] = {'a'};
pipe(pipes1);
pipe(pipes2);
int ret = fork();
// parent send in pipes1[1], child receives in pipes1[0]
// child send in pipes2[1], parent receives in pipes2[0]
// should have checked close & read & write return value for error, but i am
// lazy
if (ret == 0) {
// i am the child
pid = getpid();
close(pipes1[1]);
close(pipes2[0]);
read(pipes1[0], buf, 1);
printf("%d: received ping\n", pid);
write(pipes2[1], buf, 1);
exit(0);
} else {
// i am the parent
pid = getpid();
close(pipes1[0]);
close(pipes2[1]);
write(pipes1[1], buf, 1);
read(pipes2[0], buf, 1);
printf("%d: received pong\n", pid);
exit(0);
}
}
/*
详细注释版本
#include "kernel/types.h" // 包含系统类型定义
#include "user/user.h" // 包含用户库函数,如pipe、fork、read、write等
int main(int argc, char *argv[]) {
int pid; // 用于存储进程ID
int pipes1[2], pipes2[2]; // 定义两个管道,pipes1和pipes2
char buf[] = {'a'}; // 定义并初始化消息缓冲区,存放将要发送的数据
pipe(pipes1); // 创建第一个管道
pipe(pipes2); // 创建第二个管道
int ret = fork(); // 创建子进程,并将返回值存储在ret中
// 父进程在pipes1[1]写入,子进程在pipes1[0]读取
// 子进程在pipes2[1]写入,父进程在pipes2[0]读取
// 应该检查close、read和write的返回值以处理错误,但示例中未进行检查
if (ret == 0) {
// 子进程执行的分支
pid = getpid(); // 获取当前进程ID
close(pipes1[1]); // 关闭子进程中不需要的管道端(pipes1的写入端)
close(pipes2[0]); // 关闭子进程中不需要的管道端(pipes2的读取端)
read(pipes1[0], buf, 1); // 从pipes1的读取端读取一个字节的数据到buf
printf("%d: received ping\n", pid); // 打印接收到的消息(ping)
write(pipes2[1], buf, 1); // 将数据写入pipes2的写入端,发送给父进程
exit(0); // 子进程退出
} else {
// 父进程执行的分支
pid = getpid(); // 获取当前进程ID
close(pipes1[0]); // 关闭父进程中不需要的管道端(pipes1的读取端)
close(pipes2[1]); // 关闭父进程中不需要的管道端(pipes2的写入端)
write(pipes1[1], buf, 1); // 将数据写入pipes1的写入端,发送给子进程
read(pipes2[0], buf, 1); // 从pipes2的读取端读取一个字节的数据到buf
printf("%d: received pong\n", pid); // 打印接收到的消息(pong)
exit(0); // 父进程退出
}
}
3、primes
第三个utility功能primes是用pipeline来实现Sieve质数算法
#include "kernel/types.h"
#include "user/user.h"
// 定义了runprocess函数,该函数用于运行素数筛选器进程
void runprocess(int listenfd) {
int my_num = 0; // 当前进程持有的素数
int forked = 0; // 标记是否已经fork过子进程
int passed_num = 0; // 从管道读取的数
int pipes[2]; // 新的管道,用于与下一个进程通信
while (1) {
// 从左邻进程的管道中读取一个整数
int read_bytes = read(listenfd, &passed_num, 4);
// 如果读取字节为0,说明左邻进程没有更多的数字提供
if (read_bytes == 0) {
close(listenfd); // 关闭监听的管道端
if (forked) {
// 如果已经fork了子进程,告诉子进程没有更多的数字,并等待子进程结束
close(pipes[1]);
int child_pid;
wait(&child_pid);
}
exit(0); // 当前进程退出
}
// 如果是当前进程第一次读取数字,该数字即为该进程负责的素数
if (my_num == 0) {
my_num = passed_num;
printf("prime %d\n", my_num); // 打印素数
}
// 如果读取的数字不是当前素数的倍数,需要传递给下一个进程
if (passed_num % my_num != 0) {
if (!forked) {
// 如果还没有fork子进程,则创建新的管道和子进程
pipe(pipes);
forked = 1;
int ret = fork();
if (ret == 0) {
// 子进程逻辑
close(pipes[1]); // 关闭写端
close(listenfd); // 关闭从父进程继承的读端
runprocess(pipes[0]); // 递归调用,以新管道读端为参数
} else {
// 父进程逻辑
close(pipes[0]); // 关闭读端
}
}
// 将不能被当前素数整除的数字传递给右邻进程
write(pipes[1], &passed_num, 4);
}
}
}
int main(int argc, char *argv[]) {
int pipes[2];
pipe(pipes); // 创建主管道
for (int i = 2; i <= 35; i++) {
// 初始化:向管道写入2到35的整数
write(pipes[1], &i, 4);
}
close(pipes[1]); // 关闭写端
runprocess(pipes[0]); // 以管道读端为参数,启动素数筛选器
exit(0);
}
4、find
第四个utility功能是find. 给定一个初始路径和目标文件名, 要不断递归的扫描找到所有子目录前匹配的文件全路径. 实验手册里让我们模仿ls用户函数的实现来实现这个功能. 主要是要学一下文件系统的操作.
#include "kernel/types.h" // 定义了基本数据类型
#include "kernel/fcntl.h" // 定义了文件控制常量,比如O_RDONLY
#include "kernel/fs.h" // 定义了文件系统的结构,比如dirent
#include "kernel/stat.h" // 定义了获取文件状态所需的结构体,比如stat
#include "user/user.h" // 提供用户级函数的声明,如open、read、write等
/* 从完整路径中提取文件名 */
char *basename(char *pathname) {
char *prev = 0;
char *curr = strchr(pathname, '/');
while (curr != 0) {
prev = curr;
curr = strchr(curr + 1, '/');
}
return prev ? prev + 1 : pathname; // 修正返回文件名而不包含'/',如果没有'/',返回原路径
}
/* 递归搜索 */
void find(char *curr_path, char *target) {
char buf[512], *p;
int fd;
struct dirent de;
struct stat st;
// 尝试打开当前路径
if ((fd = open(curr_path, O_RDONLY)) < 0) {
fprintf(2, "find: cannot open %s\n", curr_path);
return;
}
// 获取路径的状态信息
if (fstat(fd, &st) < 0) {
fprintf(2, "find: cannot stat %s\n", curr_path);
close(fd);
return;
}
switch (st.type) {
// 如果是文件
case T_FILE:
char *f_name = basename(curr_path); // 提取文件名
if (!f_name || strcmp(f_name, target) != 0) break; // 如果文件名不匹配,结束
printf("%s\n", curr_path); // 打印匹配的文件路径
break;
// 如果是目录
case T_DIR:
memset(buf, 0, sizeof(buf)); // 清空缓冲区
strcpy(buf, curr_path); // 复制当前路径到buf
buf[strlen(buf)] = '/'; // 在路径末尾添加'/'
p = buf + strlen(buf); // p指向buf的末尾
while (read(fd, &de, sizeof(de)) == sizeof(de)) {
if (de.inum == 0 || !strcmp(de.name, ".") || !strcmp(de.name, "..")) continue; // 忽略"."和".."
strcpy(p, de.name); // 将目录项名复制到p指向的位置
find(buf, target); // 递归调用find
}
break;
}
close(fd); // 关闭文件描述符
}
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(2, "usage: find [directory] [target filename]\n"); // 检查参数数量,提醒使用方法
exit(1);
}
find(argv[1], argv[2]); // 调用find函数开始搜索
exit(0); // 退出程序
}
5、xargs
第五个utility是xargs. 将标准输入里每一个以’\n’分割的行作为单独1个额外的参数, 传递并执行下一个命令. 这题主要感觉是考察fork + exec的使用.
#include "kernel/param.h" // 包含系统参数,例如MAXARG
#include "kernel/types.h" // 定义了基本数据类型
#include "user/user.h" // 提供用户级函数的声明,如fork、read、write等
#define buf_size 512 // 定义缓冲区大小
int main(int argc, char *argv[]) {
char buf[buf_size + 1] = {0}; // 定义一个字符数组作为缓冲区,用于存储从标准输入读取的数据
uint occupy = 0; // 记录缓冲区中当前占用的字节数
char *xargv[MAXARG] = {0}; // 用于存储将要执行的命令及其参数的数组
int stdin_end = 0; // 标记标准输入是否结束
for (int i = 1; i < argc; i++) {
xargv[i - 1] = argv[i]; // 将命令行参数(除了程序名外)复制到xargv中,用于之后的exec调用
}
while (!(stdin_end && occupy == 0)) {
// 如果标准输入未结束或缓冲区内有未处理的数据,继续循环
if (!stdin_end) {
int remain_size = buf_size - occupy; // 计算缓冲区剩余空间
int read_bytes = read(0, buf + occupy, remain_size); // 从标准输入读取数据
if (read_bytes < 0) {
fprintf(2, "xargs: read returns -1 error\n"); // 读取出错
}
if (read_bytes == 0) {
close(0); // 读取结束,关闭标准输入
stdin_end = 1; // 标记标准输入结束
}
occupy += read_bytes; // 更新缓冲区占用字节数
}
// 处理缓冲区中的数据
char *line_end = strchr(buf, '\n'); // 查找缓冲区中的换行符
while (line_end) {
char xbuf[buf_size + 1] = {0};
memcpy(xbuf, buf, line_end - buf); // 将一行数据复制到xbuf中
xargv[argc - 1] = xbuf; // 设置执行命令的参数
int ret = fork(); // 创建子进程
if (ret == 0) {
// 子进程
if (!stdin_end) {
close(0); // 如果标准输入未结束,关闭子进程的标准输入
}
if (exec(argv[1], xargv) < 0) {
fprintf(2, "xargs: exec fails with -1\n"); // exec调用失败
exit(1);
}
} else {
// 父进程
// 移动缓冲区,剔除已处理的行
memmove(buf, line_end + 1, occupy - (line_end - buf) - 1);
occupy -= line_end - buf + 1; // 更新占用字节数
memset(buf + occupy, 0, buf_size - occupy); // 清空缓冲区剩余部分
// 回收子进程,避免僵尸进程
int pid;
wait(&pid);
// 继续查找下一行
line_end = strchr(buf, '\n');
}
}
}
exit(0);
}
参考链接: