
对项目进行编译: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翻译成指令。 - 把指令打成镜像(
kernel和fs.img)。 - 启动 QEMU 模拟器并加载这些镜像。
添加sleep
本步骤要求为xv6-riscv系统增加sleep命令。增加的sleep命令将暂停给定的时钟滴答。
一个时钟滴答是xv6-riscv系统定义的时间描述,指系统的两次时钟中断之间的时间。
注意事项:
- 命令的源代码位于xv6-riscv/user/目录,文件名为sleep.c
- 在编码前,先阅读[1]的第一章
- 参考user目录下的其他程序作为例子 (user/echo.c,user/grep.c, user/rm.c)
- 如果用户忘记给出命令行参数,应当进行提醒
- 命令行参数是以字符串形式提供,你可以用atoi函数(user/ulib.c)将其转换成一个整数
- 使用sleep系统调用
- 可通过kernel/sysproc.c来查看sleep系统调用(sys_sleep)在内核的实现,user/user.h查看sleep函数的定义,user/usys.S来查看用户态代码是如何跳转到内核的
- main函数中需调用exit()来退出主程序
- 将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命令:找到给定目录具有给定名称的所有文件。
注意事项:
- 命令的源代码位于xv6-riscv/user/目录,文件名为find.c
- 参考user/ls.c看如何读目录
- 使用递归来查询子目录
- 不要对"."和".."目录进行递归
- 用户对文件系统的改变在不同qemu环境运行之间是保持的,可通过make clean,make qemu重新来过
- 通过修改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 个字符,它的结尾可能没有 \0。strcpy 会一直拷贝直到遇到 \0,这可能会导致越界读;而 memmove 强制只拷 14 个字节,更安全。
文件名可能填满了 14 个字节而没有 \0。为了让 buf 变成一个标准的、可以被 printf 或 stat 正确识别的 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"。
注意事项:
- 针对输入的每一行,使用fork和exec来调用命令。在父进程中使用wait等待子进程完成命令
- 为了按行读入,可以逐字符读入直到换行符("\n")出现
- kernel/param.h中声明了MAXARG,可以用来定义一个argv数组
- 将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位置开始写,其返回值(通常记作 r 或 status)有三种情况:
- 正数 (
): - 表示实际读取到的字节数。
- 比如你要求读 10 个字节,但文件只剩 3 个字节了,它会返回 3。
- 在你的
xargs中,你要求读 1 个字节(read(0, &c, 1)),所以成功时它返回 1。
- 零 (
): - 表示到达文件末尾 (EOF)。
- 这意味着管道另一端(比如
echo)已经关掉了,或者文件已经读完了。这是while(1)循环退出的唯一正确信号。
- 负数 (
): - 表示发生错误(比如无效的句柄、硬件故障等)。
-e 的作用是让 echo 命令能够解析“转义字符”
echo -e "hello\n\nworld" | xargs echo
不过xv6并没有实现-e,所以会输出-e "hello\n\nworld"
添加findx
为了实现更加灵活的查找,UNIX自带的find命令的参数可以带有通配符。
本步骤为xv6-riscv系统增加带首/尾星号通配符的find命令:findx,找到给定目录的文件名部分符合参数要求的所有文件。
注意事项:
- 命令的源代码位于xv6-riscv/user/目录,文件名为findx.c,最终的可执行命令为findx
- 如果命令行参数首字母为星号,例如:
*example,则匹配给定目录及以下所有名称以example结束的文件 - 如果命令行参数尾字母为星号,例如:
example*,则匹配给定目录及以下所有名称以example开始的文件 - 如果命令行参数首尾都为星号,例如:
*example*,则匹配名称中带有example字符串的文件 - 参考前面步骤的user/find.c的实现
- 使用递归来查询子目录
- 不要对"."和".."目录进行递归
- 用户对文件系统的改变在不同qemu环境运行之间是保持的,可通过make clean,make qemu重新来过
- 通过修改Makefile的UPROGS变量添加命令
最后,对添加的命令进行测试:
root@cg:~/xv6-riscv#make qemu
$echo > 123abc
$mkdir a
$echo > a/123abc
$findx . 123*
./123abc
./a/123abc
findx和find函数的逻辑是完全一致的,唯一不同的就是查找的pattern,以及匹配用了专门设计的函数strmatch。
*example用strcmp(比较两个字符串直到遇到\0),所以二者相等最后也要一起遇到\0example*不能用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();
}