xv6项目开源—01

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是利用系统调用函数forkpipe在父进程和子进程前交换一个字节

代码:

#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);
}

参考链接:

MIT 6.S081 Operating System - 知乎 (zhihu.com)

  • 8
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星空有大海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值