文章封面图

Essay2026 / 09 / 08

XV6实验1-用户态编程

0-开始前的准备

xv6是MIT的经典项目 它的灵魂直接继承自操作系统史的传奇——Unix 第六版 (Version 6, 简称 V6).但由于V6所运行在的机器太老了,可以把xv6看作是用现代C语言为现代CPU重新书写的“V6 精神续作”。 这里是我一些环境配置过程中踩到坑,方便各位

0.1-xv6实现的与未实现功能

内核级别实现的

  • 进程
  • 虚拟地址空间
  • UNIX LIKE文件与目录层次结构
  • 管道
  • 分时复用
  • 21个系统调用

内核级别未实现的

  • 用户ID与登录
  • 文件的权限保护
  • 网络栈,套接字(socket)
  • 可分页到disk(磁盘)的虚拟内存
  • 支持更多的设备驱动
  • 还有很多

0.2-xv6怎么启动

执行下面这条命令

make qemu

0.3-xv6怎么退出

在成功运行后,ctrl+a,然后按x即可退出xv6

0.4-如何编写用户级别应用

首先在user目录下写好文件 接下来就是进入xv6项目的makefile进行修改 详细请看 xv6-makefile导读

0.5-不知道如何使用操作系统接口

如果你正在使用Linux这样的操作系统,那么你可以通过其内置的man命令来查询各个系统接口的使用方式作为参考。 如chdir,就可以通过输入

man 2 chdir

0.4-评分测试

在写好代码后,就要运行测试了,其中在我自己的LINUX环境下,Python的版本实际上是python3,所以需要更改xv6项目的评分脚本grade-lab-util中的

#!/usr/bin/env python

变为

#!/usr/bin/env python3

如果一次性执行所有评分测试

make grade

如果要对单独一个进行测试,如sleep。输入

make GRADEFLAGS=sleep grade

0.5-你需要掌握的技能知识

为了做好这个lab,你还需要以下的能力

  • 对于RISCV64的ISA,寄存器布局等熟悉
  • 对于C99往后的语法1基本熟悉。清楚函数指针等
  • 对于qemu环境下启动的xv6,懂得如何使用gdb对代码进行调试。以及一些基本的debug技巧,如何打日志输出错误信息等等。即拥有较为熟练的C的debug能力

1-Sleep

sleep的实现非常简单,我们按照xv6的手册调用用户态的sleep即可

#include "kernel/types.h"
#include "user.h"

void
sh_sleep(int n){
    sleep(n);
}

int
main(int argc,char *argv[]){
    if(argc <= 1){
        fprintf(2,"sleep: no arguments\n");
        exit(1);
    } 
    int n;
    for(int i=1;i < argc;i++){
        n = atoi(argv[i]);
        sh_sleep(n);
    }
    exit(0);
}

2-pingpong

pingpong的核心就是使用管道,在夫进程与子进程之间传递数据 代码如下

#include "kernel/types.h"
#include "user/user.h"

void main(int argc, char* argv[])
{
    int p0[2];
    int p1[2];
    if (pipe(p0) == -1) {
        fprintf(2, "pipe error");
        exit(1);
    }
    if (pipe(p1) == -1) {
        fprintf(2, "pipe error");
        exit(1);
    }
    if (fork() == 0) {
        char c;
        int fd0 = dup(p0[0]);
        close(p0[0]);
        close(p0[1]);
        read(fd0, &c, 1);//等待父进程对于p0的写入,然后读取

        close(fd0);
        fprintf(1, "%d: received ping\n", getpid());//通过p1向父进程传输数据
        write(p1[1], &c, 1);
        close(p1[1]);
        close(p1[0]);
        exit(0);
    }
    else {
        char c;
        int fd;
        write(p0[1], "c", 1);//通过p0向子进程传输数据
        close(p0[1]);
        close(p0[0]);
        fd = dup(p1[0]);
        close(p1[0]);
        close(p1[1]);
        read(fd, &c, 1);
        if (c == 'c') { //通过p1接收来自子进程的数据,并校验
            fprintf(1, "%d:received pong\n", getpid());
        }
        exit(0);
    }
}

3-primes

答案如下。这里考查的关键就是关于管道,fd,进程的理解。这里极其关键的一个点在于xv6对于一个进程支持其打开的最大文件数量为16,所以在创建子进程的过程中及时关闭fd是很重要的。

#include "kernel/types.h"
#include "user/user.h"

void stage(int fd)
{
    int x;//当前流水线所要筛的数
    int y;//流水线从管道中读取到的数
    int p[2];
    int n;//记录read返回值
    n =read(fd, &x, 4);
    if(n == 0){//读取第一个数时,遇到EOF直接退出
        exit(0);
    }
    else if(n < 4){ //未读取到完整的int
        fprintf(1,"read error");
        exit(1);
    }
    fprintf(1,"prime %d\n", x);
    pipe(p);
    if (fork() == 0) {//开启子进程
        close(fd);
        close(p[1]);
        stage(p[0]);
        close(p[0]);
        exit(0);
    }
    else {//父进程继续从管道中读取
        close(p[0]);
        while((n = read(fd,&y,4)) == 4){
            if((y%x) != 0){//筛选
                write(p[1],&y,4);
            }
        }
        close(p[1]);
        close(fd);
        if(n != 0 && n < 4){//读取遇到问题
            fprintf(2,"write error\n");
            n = 1;
        }
        wait(0);
        exit(n);
    }
}

void main(int argc, char* argv[])
{
    int p[2];
    pipe(p);
    for(int i=2;i<36;i++){
        write(p[1],&i,4);
    }
    if(fork() == 0){
        close(p[1]);
        stage(p[0]);
        close(p[0]);
        exit(0);
    }
    else {
        close(p[0]);
        close(p[1]);
        wait(0);
        exit(0);
    }
}

如果完成了这个实验,建议读读pipe的源码 详细可以读这个文章 从0开始读源码-第0节-pipe

4-find

开始find之前,需要了解文件系统的核心概念与源代码 看文件系统的源码

4.1-文件数据结构

层面结构体定义位置是什么
磁盘上struct dinodekernel/fs.h每个文件的身份证,记录其类型,大小,数据块位置等等
内核内存struct inodekernel/file.hdinode的缓存版本,并且带引用计数/锁
用户程序struct statkernel/stat.h内核通过stat()/fstat()给用户看的文件状态信息
目录内容struct direntkernel/fs.h目录文件的一行,目录文件的内容就是这样的一个数组

在这样的数据结构下带来一些点

  • 文件名长度限制在14字节以内

4.2-读取目录下的文件

读取目录下文件的步骤如下

  • 1.获取目录文件的路径path
  • 2.读取目录文件内的dirent条目。每读取一个,将dirent.name与path进行拼接,获得目录下文件的路径
  • 3.获取到文件的路径,然后就可以获取其状态,打开等等
  • 4.若目录文件的dirent没有读完,回到继续执行

4.3-程序的构造

所以很显然地我们能够开始想怎么去写代码了 我们定义一个find,接口为

//查找path下所有与name匹配的文件并输出
void find(char *path,char *name);

而find内部,参考ls的实现,则可以这样去递归实现

void find(char *path,char *name){
    char buf[512];
    int fd = open(path,0);
    struct stat st = fstat(fd,&st);
    switch(st.type){
    case T_FILE:
        if(strcmp(basename(path),name)){
            printf(%s\n);
        }
        break;
    case T_DIR:
        //拼接路径名到buf的代码(省略)
        //然后读取DIR文件,遍历里面的每个文件,执行find

    }
    break;
}

4.4-源码

#include "kernel/types.h"
#include "kernel/fs.h"
#include "kernel/stat.h"
#include "user/user.h"

char* basename(char* path)
{
    char* p;

    // Find first character after last slash.
    for (p = path + strlen(path); p >= path && *p != '/'; p--);
    p++;

    // Return blank-padded name.
    return p;
}

void find(char* path, char* name)
{
  char buf[512], *p;
  int fd;
  struct stat st;
  struct dirent de;

  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(basename(path),name)){
      printf("%s\n",path);
    }
    break;
  case T_DIR:
    if (strlen(path) + 1 + DIRSIZ + 1 > sizeof buf) {
      fprintf(2, "find: path is too long\n");
      close(fd);
      return;
    }
    strcpy(buf, path);  // 将路径名移动至buf内,buf将记录目录下文件的路径
    p = buf + strlen(buf);
    *p++ = '/'; //p指向路径最后的文件名
    while (read(fd, &de, sizeof(de)) == sizeof(de)) {
      if (de.inum == 0) {
        continue;  // 为什么inum为0要continue
      }
      if(!strcmp(de.name,".") || !strcmp(de.name,"..")){
        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);
}

5-xargs

这个比较简单,就是从标准输入内实现读取一行的功能,然后每读取一行,进行参数拼接,fork,exec相应的程序即可。 源码如下

#include "kernel/param.h"
#include "kernel/types.h"
#include "user/user.h"

char** addArgv(int argc, char* argv[], char* arg)
{
    int i =0;
    static char* na[MAXARG] = {0};
    for (i = 0; i < MAXARG; i++) {
        na[i] = 0;
    }
    for (i = 0; i < argc; i++) {
        na[i] = argv[i];
    }
    na[argc] = arg;
    return na;
}

void execArgs(char* path, char* argv[])
{
    #ifdef DEBUG
    fprintf(2,"DEBUG: path=%s argv[0]=%s argv[1] = %s argv[2]=%s\n",
             path,argv[0],argv[1],argv[2]);
    #endif

    if (fork() == 0) {
        if (exec(path, argv) == -1) {
            fprintf(1, "xargs: exec %s failed\n", path);
            exit(1);
        }
    }
    else {
        wait(0);
    }
}

int readline(char* buf, int max)
{
    int i = 0, n = -1;

    for (i = 0; (i + 1) < max; i++) {
        n = read(0, buf + i, 1);
        if (n <= 0 || buf[i] == '\n') {
            break;
        }
    }
    buf[i] = '\0';
    if (i > 0) {
        return i;
    }
    // fprintf(2,"xargs: raed error with %d\n",n);
    return -1;
}

int main(int argc, char* argv[])
{
    if(argc < 2){
        fprintf(2,"xargs: too few arguments");
        exit(0);
    }
    char buf[512];
    while (readline(buf, sizeof(buf)) > 0) {
        execArgs(argv[1], addArgv(argc - 1, &argv[1], buf));
    }
    exit(0);
}