xv6-riscv-lab1
本文最后更新于9 天前,内容如有失效请评论区留言。

对项目进行编译:make
运行编译好的系统:make qemu
打印当前进程信息:执行Ctrl+p
退出xv6-riscv环境:行Ctrl+a x(中间没有空格,直接点击x)

环境准备

WSL或者虚拟机
建议安装Ubuntu20版本,24版本太新,带上新版QEMU,自带了一个名为 OpenSBI 的固件(类似于 BIOS),它默认要占用  kernel/kernel 明确要求加载到的内存地址0x80000000,QEMU发现重叠,所以需要QEMU禁止加载OpenSBI固件,直接用xv6内核来引导系统;同时gcc版本太高会无限递归检测,需要在编译选项中关掉这个特定的递归检查…
不建议去顽强抵抗,我尝试过,依然有很多问题,总之,关于项目方面不必求新,只求合适。

安装交叉编译工具链硬件模拟器

sudo apt update && sudo apt install -y gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu qemu-system-misc qemu-system-riscv64
  • gcc-riscv64-linux-gnu:在你的电脑(x86)上编译出能跑在 RISC-V 芯片上的代码。
  • qemu-system-riscv64:用软件模拟出一台虚拟的 RISC-V 电脑,不需要买硬件就能跑 xv6。

可能之前你编译过一次,可能失败了,不妨先把项目弄干净。

make clean && find . -name "*.d" -delete && find . -name "*.o" -delete
  • make clean:删除已知的编译产物(.o 和二进制文件)。
  • find ... -delete:强制删掉那些记录了旧路径、导致报错的隐藏依赖文件.d)。

一键完成编译、链接、打包、启动

make qemu
  • .c 翻译成指令。
  • 把指令打成镜像(kernelfs.img)。
  • 启动 QEMU 模拟器并加载这些镜像。

添加sleep

本步骤要求为xv6-riscv系统增加sleep命令。增加的sleep命令将暂停给定的时钟滴答。
一个时钟滴答是xv6-riscv系统定义的时间描述,指系统的两次时钟中断之间的时间。

注意事项:

  1. 命令的源代码位于xv6-riscv/user/目录,文件名为sleep.c
  2. 在编码前,先阅读[1]的第一章
  3. 参考user目录下的其他程序作为例子 (user/echo.c,user/grep.c,  user/rm.c)
  4. 如果用户忘记给出命令行参数,应当进行提醒
  5. 命令行参数是以字符串形式提供,你可以用atoi函数(user/ulib.c)将其转换成一个整数
  6. 使用sleep系统调用
  7. 可通过kernel/sysproc.c来查看sleep系统调用(sys_sleep)在内核的实现,user/user.h查看sleep函数的定义,user/usys.S来查看用户态代码是如何跳转到内核的
  8. main函数中需调用exit()来退出主程序
  9. 将sleep命令添加到Makefile的UPROGS变量中

完成sleep命令后,执行make qemu对sleep命令进行测试:

$sleep 20

首先每个指令对应一个c文件,里面的核心就是写main函数

首先引入必要的头文件,这里不适用系统自带的C语言库,而是用xv6本身的,否则就不算是在设计自己的OS了。

kernel/types.h记录的是一些特别的数据类型,很多时候你写的文件没用到这些类型,但引入的头文件用了也需要引入。

typedef unsigned int   uint;
typedef unsigned short ushort;
typedef unsigned char  uchar;

typedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned int  uint32;
typedef unsigned long uint64;

typedef uint64 pde_t;

kernel/stat.h定义了文件状态结构体,包含文件大小,类型,inode节点号等,使用文件相关操作的时候引入。

#define T_DIR     1   // Directory
#define T_FILE    2   // File
#define T_DEVICE  3   // Device

struct stat {
  int dev;     // File system's disk device
  uint ino;    // Inode number
  short type;  // Type of file
  short nlink; // Number of links to file
  uint64 size; // Size of file in bytes
};

当调用 stat("main.c", &st) 时,内核会去磁盘查找该文件的 dinode,提取出关键信息(如大小、类型、链接数),然后填入这个结构体返回。
仅通过这个头文件,获取的关于文件的信息并不多,一般只是通过这个获取文件类型,判断是目录,文件还是设备。

关于文件
首先要清楚,文件内容本身在硬盘是分散的,所以需要指向的指针并且存储指针,关于该文件的指针集中存储在硬盘,那么如何找到这块指针区域,因为与文件本身强绑定,应该可以用文件名来查找,然而文件名大小不固定,并且是字符串,不方便存储,也不方便搜索查找,所以使用数字,也相当于ID去一一对应,所以有了inode号(本质是offset),而存储指针的区域我们称为Inode结构,Inode结构就是存储指针的地方,然后最后就是需要文件名和inode产生对应,并且这样修改文件名不会影响到inode,也算是解耦,方便修改和维护了。

Inode存储指针具体形式是,可以直接存储少量的直接指针,当文件过大,指针数量过多,则需要用间接指针指向这些指针存储的区域(会开辟空闲的数据块存储这些指针)。

现在可以整理出整体流程:用户提供文件名,系统根据文件名得到对应的Inode号,通过Inode号(offset)找到Inode结构的磁盘地址,通过Inode的直接指针或者通过间接指针找到的指针,找到文件在磁盘的具体位置。

xv6的inode结构体在kernel/fs.h有实现(见添加find)

user/user.h声明了很多基本的函数,一般写指令必定需要这个头文件。

struct stat;
struct rtcdate;

// system calls
int fork(void);
int exit(void) __attribute__((noreturn));
int wait(void);
int pipe(int*);
int write(int, const void*, int);
int read(int, void*, int);
int close(int);
int kill(int);
int exec(char*, char**);
int open(const char*, int);
int mknod(const char*, short, short);
int unlink(const char*);
int fstat(int fd, struct stat*);
int link(const char*, const char*);
int mkdir(const char*);
int chdir(const char*);
int dup(int);
int getpid(void);
char* sbrk(int);
int sleep(int);
int uptime(void);
int ntas();
int crash(const char*, int);
int mount(char*, char *);
int umount(char*);

// ulib.c
int stat(const char*, struct stat*);
char* strcpy(char*, const char*);
void *memmove(void*, const void*, int);
char* strchr(const char*, char c);
int strcmp(const char*, const char*);
void fprintf(int, const char*, ...);
void printf(const char*, ...);
char* gets(char*, int max);
uint strlen(const char*);
void* memset(void*, int, uint);
void* malloc(uint);
void free(void*);
int atoi(const char*);

在写代码先一定要阅读已经定义的函数,很多时候直接利用。
写sleep指令的本质,是在sleep 100后能调用到sleep(100),指令不同于函数,识别指令要看指令名称和参数是否正确,argv[0]就是指令名称,后面的就是指令名后跟随的参数。

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

int main(int argc, char *argv[])
{
  if(argc < 2)
  {
    fprintf(2, "Usage: sleep <ticks>\n");
    exit();
  }
  // argv[0] 是 "sleep",argv[1] 是用户输入的数字字符串
  int ticks = atoi(argv[1]);
  if(sleep(ticks) < 0)
  {
    fprintf(2, "sleep: system call failed\n");
    exit();
  }
  exit();
}

sleep.c很简单,我们主要看看命令行输入sleep 100后会做什么?

  • 读取输入,拆分字符串
  • shell创建fork,调用exec("sleep", argv),内核接手工作,去磁盘根目录查找sleep文件,搜索sleep获得对应的Inode号,进而找到Inode结构体,从中获得指向文件内容的指针,吧磁盘上的二进制指令读入内存,执行写的代码。

添加find

本步骤实现一个简单版的UNIX find命令:找到给定目录具有给定名称的所有文件。

注意事项:

  1. 命令的源代码位于xv6-riscv/user/目录,文件名为find.c
  2. 参考user/ls.c看如何读目录
  3. 使用递归来查询子目录
  4. 不要对"."和".."目录进行递归
  5. 用户对文件系统的改变在不同qemu环境运行之间是保持的,可通过make clean,make qemu重新来过
  6. 通过修改Makefile的UPROGS变量添加命令

最后,对添加的命令进行测试:

root@cg:~/xv6-riscv#make qemu
$echo > b
$mkdir a
$echo > a/b
$find . b
./b
./a/b

根据示例,知道参数1是路径,参数2是目标文件名。
首先了解下kernel/fs.h

// On-disk file system format.
// Both the kernel and user programs use this header file.


#define ROOTINO  1   // root i-number
#define BSIZE 1024  // block size

// Disk layout:
// [ boot block | super block | log | inode blocks |
//                                          free bit map | data blocks]
//
// mkfs computes the super block and builds an initial file system. The
// super block describes the disk layout:
struct superblock {
  uint magic;        // Must be FSMAGIC
  uint size;         // Size of file system image (blocks)
  uint nblocks;      // Number of data blocks
  uint ninodes;      // Number of inodes.
  uint nlog;         // Number of log blocks
  uint logstart;     // Block number of first log block
  uint inodestart;   // Block number of first inode block
  uint bmapstart;    // Block number of first free map block
};

#define FSMAGIC 0x10203040

#define NDIRECT 12
#define NINDIRECT (BSIZE / sizeof(uint))
#define MAXFILE (NDIRECT + NINDIRECT)

// On-disk inode structure
struct dinode {
  short type;           // File type
  short major;          // Major device number (T_DEVICE only)
  short minor;          // Minor device number (T_DEVICE only)
  short nlink;          // Number of links to inode in file system
  uint size;            // Size of file (bytes)
  uint addrs[NDIRECT+1];   // Data block addresses
};

// Inodes per block.
#define IPB           (BSIZE / sizeof(struct dinode))

// Block containing inode i
#define IBLOCK(i, sb)     ((i) / IPB + sb.inodestart)

// Bitmap bits per block
#define BPB           (BSIZE*8)

// Block of free map containing bit for block b
#define BBLOCK(b, sb) ((b)/BPB + sb.bmapstart)

// Directory is a file containing a sequence of dirent structures.
#define DIRSIZ 14

struct dirent {
  ushort inum;
  char name[DIRSIZ];
};

相比于stat.h,可以获得更多关于文件的信息。

  • struct superblock: 磁盘的“地图”。它告诉内核:日志区在哪、i-node 区在哪、数据块从哪开始。
  • struct dinode (Disk Inode): 这就是之前提到的Inode结构体。
    • addrs[NDIRECT+1],本质就是指针,直接指向了硬盘上的数据块编号。
    • 它的空间非常紧凑(通常是 64 字节),以便在一个 1024 字节的磁盘块中存放多个。
  • struct dirent: 目录条目,目录项结构体。简单地把文件名和inode号对应起来。

最终目标是获得目标文件的路径,所以会用buf才存。
对于查找,首先打开path,获取path上的文件状态信息,以及检查path起始路径是否位目录,最后检查路径长度(DIRSIZ是文件名长度的最大值),防止buf溢出。
接着开始构造buf,先将path+/放入buf,然后循环从目录文件fd(指向path下的目录)中读取目录项dirent。

  • 文件inode=0,位置为空
  • 目录下包含...,前者表示当前目录,后者表示上一层目录,显然不在这找,同时防止递归死循环
  • 将文件名拼接到buf上,得到新的path,通过stat获取文件状态信息,如果类型是目录,则递归查询;如果是文件,则判断是否文件名相同,同则输出当前buf
#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; // 目录项结构体(包含文件名和 inode 编号)
    struct stat st;   // 文件状态结构体(包含文件类型、大小等)

    // get file descriptor fd
    if((fd = open(path, 0)) < 0)
    {
        fprintf(2, "find: cannot open %s\n", path);
        return;
    }

    // get file status
    if(fstat(fd, &st) < 0)
    {
        fprintf(2, "find: cannot stat %s\n", path);
        close(fd);
        return;
    }

    if(st.type != T_DIR)
    {
        fprintf(2, "find: %s is not a directory\n", path);
        close(fd);
        return;
    }

    if(strlen(path) + 1 + DIRSIZ + 1 > sizeof(buf))
    {
        fprintf(2, "find: path too long\n");
        close(fd);
        return;
    }

    strcpy(buf, path);
    p = buf + strlen(buf);
    *p++ = '/';

    // 在fd指向的目录中循环读取每个目录项
    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的指针不变,所以下次会覆盖当前添加的文件名
        p[DIRSIZ] = 0; // 强制添加字符串结束符

        if(stat(buf, &st) < 0)
        {
            fprintf(2, "find: cannot stat %s\n", buf);
            continue;
        }

        if(st.type == T_DIR)
            find(buf, name);
        else if(st.type == T_FILE)
        {
            if(strcmp(de.name, name) == 0)
                printf("%s\n", buf);
        }
    }
    close(fd);
}

int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        fprintf(2, "Usage: find <path> <name>\n");
        exit();
    }
    find(argv[1], argv[2]);
    exit();
}

open(path, 0) 打开了一个目录,所以 fd 现在指向的是磁盘上存储的目录文件

因为 xv6 的 dirent.name 长度固定为 14 字节(DIRSIZ)。如果文件名刚好是 14 个字符,它的结尾可能没有 \0strcpy 会一直拷贝直到遇到 \0,这可能会导致越界读;而 memmove 强制只拷 14 个字节,更安全。

文件名可能填满了 14 个字节而没有 \0。为了让 buf 变成一个标准的、可以被 printfstat 正确识别的 C 字符串,我们必须在文件名的最大长度位置强行放一个 0(即 \0)。

添加xargs

本步骤要求为xv6-riscv系统增加xargs命令。

增加的xargs命令从标准输入读行,并且把读入的行作为参数执行一个命令。本步骤的结果在user/xargs.c
下面的例子展示了xargs的行为:

$echo hello too|xargs echo bye
bye hello too
$

在上面的例子中,xargs要执行的命令是"echo bye",通过xargs传递的参数是"hello too",所以执行"echo bye hello too",最后的输出是"bye hello too"。

注意事项:

  1. 针对输入的每一行,使用fork和exec来调用命令。在父进程中使用wait等待子进程完成命令
  2. 为了按行读入,可以逐字符读入直到换行符("\n")出现
  3. kernel/param.h中声明了MAXARG,可以用来定义一个argv数组
  4. 将xargs命令添加到Makefile的UPROGS变量中

xargs, find 和grep命令可以组合使用:

$find . b |xargs grep hello

上面的命令针对.目录及以下的每个名为b的文件,执行grep hello(搜索hello)

根据xargs的行为看,实质就是指令重组装,接收用户指令,提取参数,组装一个新的指令。
思路:读取参数,构造新的参数,最后fork一个子进程执行exec,从而执行具体的新指令。
读取参数就采取逐行读,逐字节读一行,用while循环实现,当遇到\n就终止该行读。

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

int main(int argc, char *argv[])
{
    char buf[512];
    char *new_argv[MAXARG];
    int i;

    if(argc < 2)
    {
        fprintf(2, "Usage: xargs <command> [args...]\n");
        exit();
    }

    for(i = 1; i < argc; i++)
    {
        new_argv[i-1] = argv[i];
    }
    // 读所有行
    while(1)
    {
        int n = 0;
        int status;
        // 读一行
        while((status = read(0, &buf[n], 1)) == 1 && buf[n] != '\n')
        {
            n++;
        }
        if(status <= 0 && n == 0) break;

        buf[n] = 0; // 读到的这一行添加0结尾,转为字符串

        new_argv[argc - 1] = buf; // 将这一行作为最后一个参数
        new_argv[argc] = 0; // exec要求argv以0结尾
        // 比如 {"echo", "bye", "hello too", 0}

        if(fork() == 0)
        {
            exec(new_argv[0], new_argv);
            fprintf(2, "xargs: exec %s failed\n", new_argv[0]);
            exit();
        }
        else wait();
    }
    exit();
}

read 的返回逻辑是什么?
read(fd, buf, n) 会将读到的n个字节从buf位置开始写,其返回值(通常记作 rstatus)有三种情况:

  • 正数 (>0):
    • 表示实际读取到的字节数
    • 比如你要求读 10 个字节,但文件只剩 3 个字节了,它会返回 3
    • 在你的 xargs 中,你要求读 1 个字节(read(0, &c, 1)),所以成功时它返回 1
  • 零 (0):
    • 表示到达文件末尾 (EOF)
    • 这意味着管道另一端(比如 echo)已经关掉了,或者文件已经读完了。这是 while(1) 循环退出的唯一正确信号。
  • 负数 (1):
    • 表示发生错误(比如无效的句柄、硬件故障等)。

-e 的作用是让 echo 命令能够解析“转义字符”

echo -e "hello\n\nworld" | xargs echo

不过xv6并没有实现-e,所以会输出-e "hello\n\nworld"

添加findx

为了实现更加灵活的查找,UNIX自带的find命令的参数可以带有通配符。
本步骤为xv6-riscv系统增加带首/尾星号通配符的find命令:findx,找到给定目录的文件名部分符合参数要求的所有文件。

注意事项:

  1. 命令的源代码位于xv6-riscv/user/目录,文件名为findx.c,最终的可执行命令为findx
  2. 如果命令行参数首字母为星号,例如:*example,则匹配给定目录及以下所有名称以example结束的文件
  3. 如果命令行参数尾字母为星号,例如:example*,则匹配给定目录及以下所有名称以example开始的文件
  4. 如果命令行参数首尾都为星号,例如:*example*,则匹配名称中带有example字符串的文件
  5. 参考前面步骤的user/find.c的实现
  6. 使用递归来查询子目录
  7. 不要对"."和".."目录进行递归
  8. 用户对文件系统的改变在不同qemu环境运行之间是保持的,可通过make clean,make qemu重新来过
  9. 通过修改Makefile的UPROGS变量添加命令

最后,对添加的命令进行测试:

root@cg:~/xv6-riscv#make qemu
$echo > 123abc
$mkdir a
$echo > a/123abc
$findx . 123*
./123abc
./a/123abc

findx和find函数的逻辑是完全一致的,唯一不同的就是查找的pattern,以及匹配用了专门设计的函数strmatch。

  • *examplestrcmp(比较两个字符串直到遇到\0),所以二者相等最后也要一起遇到\0
  • example*不能用strcmp,从前往后,即便前缀相等,如果不能同时遇到\0则不等,所以采取for循环
  • *example*,一开始想的是KMP,但是KMP是用空间换时间,显然在OS中这不算好,还是采取for循环遍历
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"

int strmatch(char *name, char *pattern)
{
    int n_len = strlen(name);
    int p_len = strlen(pattern);

    // *example
    if(pattern[0] == '*')
    {
        char *target = pattern + 1;
        int t_len = strlen(target);
        if(n_len < t_len) return 0; // name比target短说明一定不匹配
        return strcmp(name + n_len - t_len, target) == 0;
    }

    // example*
    if(pattern[p_len - 1] == '*')
    {
        int t_len = p_len - 1;
        if(n_len < t_len) return 0;
        for(int i = 0; i < t_len; i++)
        {
            if(name[i] != pattern[i]) return 0;
        }
        return 1;
    }

    // *example*
    if(pattern[0] == '*' && pattern[p_len - 1] == '*')
    {
        int sub_len = p_len - 2;
        for(int i = 0; i <= n_len - sub_len; i++)
        {
            int j;
            for(j = 0; j < sub_len; j++)
            {
                if(name[i + j] != pattern[1 + j]) break;
            }
            if(j == sub_len) return 1;
        }
    }

    return strcmp(name, pattern) == 0;
}

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

    // get file descriptor fd
    if((fd = open(path, 0)) < 0)
    {
        fprintf(2, "find: cannot open %s\n", path);
        return;
    }

    // get file status
    if(fstat(fd, &st) < 0)
    {
        fprintf(2, "find: cannot stat %s\n", path);
        close(fd);
        return;
    }

    if(st.type != T_DIR)
    {
        fprintf(2, "find: %s is not a directory\n", path);
        close(fd);
        return;
    }

    if(strlen(path) + 1 + DIRSIZ + 1 > sizeof(buf))
    {
        fprintf(2, "find: path too long\n");
        close(fd);
        return;
    }

    strcpy(buf, path);
    p = buf + strlen(buf);
    *p++ = '/';

    // 在fd指向的目录中循环读取每个目录项
    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;

        if(stat(buf, &st) < 0)
        {
            fprintf(2, "find: cannot stat %s\n", buf);
            continue;
        }

        if(st.type == T_DIR)
            findx(buf, pattern);
        else if(st.type == T_FILE)
        {
            if(strmatch(de.name, pattern))
                printf("%s\n", buf);
        }
    }
    close(fd);
}

int main(int argc, char *argv[])
{
    if(argc < 3)
    {
        fprintf(2, "Usage: findx <directory> <pattern*|*pattern|*pattern*>\n");
        exit();
    }
    findx(argv[1], argv[2]);
    exit();
}
标题:xv6-riscv-lab1
作者:Echo_Kang
链接:https://echokang.top/xv6-riscv-lab1/
声明:采用 CC BY-NC-SA 4.0 协议,转载请注明出处。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇