Run MOS on RISC-V (移植过程)


阅前须知/免责声明

本文用于记录 OS 挑战任务——将 MOS 操作系统移植到 RISC-V 架构。
本文仅供学习交流使用,不得用于商业用途。本文中的所有内容均为原创,如需转载请联系本人。

本文不是移植报告或指导书,仅是移植过程的记录,每个 Lab 的内容都是我自己参考众多资料后摸索出来的,可能有错误,不保证正确性或最优解。
并且本文中的内容可能不是最新的,因为我在移植过程中也在不断地学习和尝试,所以可能会有一些错误或不完善的地方,欢迎指正。
本文中提到的实现不代表我最终的实现,特别是前几个 Lab,很可能在之后的 Lab 中被修改或替换。

最终移植后的源代码将在挑战性任务答辩结束后开源,敬请期待。

Lab 0 环境配置

按照实验教程所述,本实验推荐使用常见 Linux 发行版作为开发环境,虽然 Mac 可能也可以,但是在本地安装那么庞大的环境,特别是还有可能出错,实在是不太推荐。如果有想尝试本地安装的,建议 Windows 用户使用 WSL2,Mac 用户使用 Docker。这里我选择了 Github Codespaces,学生一个月免费 180 CPUhours,足够使用了(后来发现可能不太够用,所以还是很推荐 Docker)。

交叉编译器

  1. 首先使用 sudo su 提权到 root 用户,这里可能要求输入密码;
  2. 执行 apt update 更新索引;
  3. 执行 apt install gcc-riscv64-unknown-elf 安装实验所需要的交叉编译器。

QEMU

  1. 执行 git clone https://github.com/qemu/qemu.git --recursive 从 Github 克隆 QEMU 源代码;
  2. 执行 cd qemu 切换到克隆好的仓库下;
  3. 执行 mkdir build 随后 cd build 新建一个 build 目录并切换过去;
  4. 尝试执行 ../configure --target-list=riscv32-softmmu ,此时可能因为没有相关依赖而失败,按要求尝试安装所需依赖。实测只需要下面两种:
    1. ninja: 使用 apt install ninja-build 安装;
    2. pixman-1: 使用 apt install libpixman-1-dev 安装。
  5. 执行 make 编译(执行 make install 可以直接把 qemu-system-riscv32 装在 /usr/local/bin 下,可以直接使用)

一点 Tips:

如果遇到一些不知如何安装的依赖项,推荐 command-not-found.comGoogle 进行搜索。

另外如果克隆源代码时不使用 --recursive 则会发现 QEMU 的子模块全为空,则可以执行 git submodule update --init --recursive 进行递归克隆。

编译 QEMU 可能会消耗一定量的时间,请提前有个心理准备。

OpenSBI

如果需要,先 git clone https://github.com/riscv-software-src/opensbi.git,然后直接安装指导书进行编译即可。

$ export CROSS_COMPILE=riscv64-unknown-elf-
$ export PLATFORM_RISCV_XLEN=32
$ make PLATFORM=generic

Lab 1 内核启动与 printk 的实现

我深深感受到了这个移植任务的繁重。。。

但在终于能跑出正确结果的那一刻,喜悦难以言表。

通过编译

大致工作表单:

name file
compiler, flags include.mk
disable interrupts start.S
declarations include/asm/asm.h
registers include/asm/regdef.h
lds kernel.lds
temporarily delete asm kern/panic.c
  1. 更改交叉编译器,并删掉不支持的编译选项,然后编译选项添加 -march=rv32gc-mabi=ilp32,链接选项添加 -melf32lriscv(参考资料 1);
  2. 删掉不支持的伪指令,并更改关闭中断的汇编指令(参考资料 2, 5);
  3. 删掉不支持的伪指令;
  4. 更改寄存器名字和序号对应表;
  5. 链接脚本可以参考实验指导书;
  6. 暂时注释掉用到的汇编指令。

完成上述操作,你应该能成功通过编译。

在这一步中,你可能遇到 #include_next 的相关错误 -ffreestanding 可能对你有所帮助。

-ffreestanding: Do not assume that standard C libraries and "main" exist.

启动内核

如果直接运行上面 make 出来的内核会有一个非常严重的问题。我们注意到 OpenSBI 引导后,默认会跳转到 0x80200000,但是通过 objdump 得知,我们编译出的 mos0x80200000 位置处是 lib/elfloader.c 中的 elf_from,这显然不是我们想要的。这里我经过了 n 次尝试,最终妥协了,找到的方案是让链接器最先链接 start.o 就可以让 _start 位于 0x80200000 位置了。

Note: 受到浙大 OS 实验教程启发,我们有更好的方案让 _start 位于 0x80200000 的位置。(参考资料 6)

这样我们总算是可以成功启动内核了。

printk

在这一步中我们需要修改 console.c 使得它能调用 OpenSBI 提供的接口进行输入输出和退出系统。

由于我们的操作系统位于 Supervisor 一级,但是只有 Machine 级才能直接向物理地址写入字符实现输入输出,所以我们需要调用位于 Machine 级的 OpenSBI 提供给我们的接口(参考资料 3, 4)。

这里我们可以创建一个头文件用于储存所有 OpenSBIecall 调用(并不是都一定要实现,用不到的可以暂时不实现)。

#ifndef _SBI_H_
#define _SBI_H_
#include <types.h>

#define SBI_SUCCESS 0
#define SBI_ERR_FAILED -1
#define SBI_ERR_NOT_SUPPORTED -2
#define SBI_ERR_INVALID_PARAM -3
#define SBI_ERR_DENIED -4
#define SBI_ERR_INVALID_ADDRESS -5
#define SBI_ERR_ALREADY_AVAILABLE -6
#define SBI_ERR_ALREADY_STARTED -7
#define SBI_ERR_ALREADY_STOPPED -8

long sbi_set_timer(uint32_t stime_value);
long sbi_console_putchar(int ch);
long sbi_console_getchar(void);
long sbi_clear_ipi(void);
long sbi_send_ipi(const unsigned long *hart_mask);
long sbi_remote_fence_i(const unsigned long *hart_mask);
long sbi_remote_sfence_vma(const unsigned long *hart_mask,
                           unsigned long start,
                           unsigned long size);
long sbi_remote_sfence_vma_asid(const unsigned long *hart_mask,
                                unsigned long start,
                                unsigned long size,
                                unsigned long asid);
void sbi_shutdown(void);

#endif

然后建议实现函数 sbi_ecall 专门用于写 ecall 的汇编,让其它函数调用即可。

根据浙大 OS 实验的建议,这里建议实现 ecall 的函数定义为

struct sbiret {
	long error;
	long value;
};

struct sbiret
sbi_ecall(int ext, int fid, u_int arg0, u_int arg1,
		  u_int arg2, u_int arg3, u_int arg4, u_int arg5);

一种可能的实现如下(内嵌汇编)(代码片段)

mv a0, %[arg0]
mv a1, %[arg1]
mv a2, %[arg2]
mv a3, %[arg3]
mv a4, %[arg4]
mv a5, %[arg5]
mv a6, %[fid]
mv a7, %[ext]
ecall
mv %[err], a0
mv %[val], a1

最后 console.c 只需要调用 sbi_console_putchar, sbi_console_getchar, sbi_shutdown 即可。

完成这一步后你就可以运行 lab1 的测试用例了。

Note: 需要修改 Makefile 才能一键运行和测试

panic

还记得我们为了通过编译把 panic.c 中的汇编直接注释掉了吗,接下来我们希望能够补全他们。

使用 csrr 指令来获取 CSR 寄存器的值吧!

Note

关于在 vscode 中更好的调试:
可以配置 launch.json:

{
	"name": "mos-kernel-debug",
	"type": "cppdbg",
	"request": "launch",
	"miDebuggerPath": "/usr/bin/riscv64-unknown-elf-gdb",
	"miDebuggerServerAddress": ":1234",
	"program": "${workspaceFolder}/target/mos",
	"args": [],
	"cwd": "${workspaceFolder}",
	"stopAtEntry": false,
	"environment": [],
	"externalConsole": false,
	"logging": {
		"engineLogging": false
	},
}

随后,先运行 make dbgMakefile 也要记得配置哦~),然后按下 F5 进行调试

Tips: 如果想要给 .S 文件添加断点,可以在调试栏中手动输入标签名或机器地址

参考资料

  1. RISC-V嵌入式开发入门篇1:RISC-V GCC工具链的介绍_半斗米的博客-CSDN博客
  2. RISC-V 指令概况 - 计算机组成原理(2021年)
  3. Lab 1: RV64 内核引导 - 知乎
  4. riscv-sbi-doc/riscv-sbi.adoc at master · riscv-non-isa/riscv-sbi-doc · GitHub
  5. PolarFire® SoC MSS Technical Reference Manual
  6. 浙江大学22年秋操作系统实验

Lab2 MMU 设置和内存管理

关于这一单元要实现的内容,实验书中写的非常详细。然而,关于具体如何实现^^

这里给几个重要的参考提示吧。

  1. 关于 sfence.vma,是一个用于刷新 fence 的指令,大致可以类比 mips 中的刷新 tlb 相关指令。其主要需要使用的地方如下:(摘至 riscv-privileged-20211203.pdf)
    `sfence.vma`
  2. riscvS 态的物理内存开始于 0x80000000
  3. riscv 的获取总内存只能在 M 态进行。 qemu-system-riscv32 默认内存是 64MB
  4. 关于特权位,首先要改宏,其次原来 PTE_D 的地方都应该改为 PTE_W。另外还要注意设置 PTE_RPTE_X。并且还要注意他们的相互关系。本 Lab 我们无需关心其它特权位。
  5. _do_tlb_refill 相关函数其实是需要的,但是并非在这个 lab 中,而且写法肯定也完全不同,表达的含义也略有差别,passive_alloc 确实可以考虑留下来。
  6. 由于在 Lab2 中我们只有内核态,而且即便建立了 SV32 虚拟内存映射,实质上还是一个等值映射(即虚拟地址和对应物理地址相等),所以直接使用物理内存也是可以的。但是我还是强烈建议在 Lab2 建立虚拟地址映射,这也是为后续的用户态的加入做好准备。
  7. 另外善用 qemu 的指令也很重要。在模拟时,按下 Ctrl+A 随后按下 C 可以看到 (qemu) 的标志出现,表示此时你可以输入 qemu 支持的指令。可以用 help 查看帮助。一般来说,最常见的是 info mem 查看虚拟内存映射,info registers 查看寄存器。需要注意在输入指令模式下模拟不会停止(除非输入指令 stop),因此建议搭配 gdb 使用。
  8. 更多技术细节可以参考浙大 OS 实验 Lab4。

参考资料

  1. 10.4 自制操作系统: risc-v 虚拟内存系统_sv39_richard.dai的博客-CSDN博客
  2. GitHub - ZJU-SEC/os22fall-stu: https://zju-sec.github.io/os22fall-stu/
  3. RISC-V Technical Specifications - Home - RISC-V International

Lab3 异常处理和进程管理

这一个 Lab 的主要任务是完成异常处理和进程管理,进程管理的难度不大,改动较少,异常处理内容就比较多(这是因为 riscvmips 异常处理相关的寄存器是完全不一样的)。

异常处理 1 - 时钟中断

本部分建议参考浙大 OS 实验 Lab2

riscv 中设置时钟中断分为三步:

  1. 调整 sstatussie 寄存器的值,使得 CPU 允许中断发生。
  2. 使用 rdtime 指令获取当前时间 (CPU周期数)。
  3. 调用 OpenSBI 的接口设置下一次中断发生的时间。

在这一个子任务中,可以暂时注释掉有关保存上下文到 TrapFrame 的代码,简单进行一些直接压栈即可,另外可以将 schedule 改为输出一条消息后立即 sret,不进行进程管理,方便查看时钟中断是否实现正确。另外也要注意调整调整时钟发生的频率,验证时钟中断发生频率是否合理。

进程管理

主要任务如下:

  1. 更改页表映射的权限位,包括但不限于 base_pgdir 以及 elf_load_seg
  2. 在为进程初始化虚拟内存的时候,应该将内核虚拟地址全部映射到进程的页表中,这样做才能在不切换页表的条件下进行 S 态异常处理和系统调用。这也是为什么我推荐在上一个 Lab 中做好内核的虚拟地址映射。
  3. 在进入异常处理的时候进行上下文保存。保存在 KSTACKTOP 中。合理利用 sscratch 寄存器会对这个过程有所帮助。
  4. 初始化进程的 TrapFrame 的时候需要根据需要初始化,使得 sretspsstatus 等寄存器都有合理值。
  5. 正确实现 env_pop_tf,这一步需要进行切换页表操作,以及为恢复上下午做准备(设置合理的栈帧)。切换页表是立即生效的,所以理论上你不需要刷新 fence。但是可能出现的问题是如果 asid 出现了循环利用,这时就会出现 satp 中的页表与 fence 中的不同,所以需要考虑在删除旧页表的时候,或者初始化新页表时刷新 fence

正确完成后应该能通过测试 3.13.2,和 3.3

异常处理 2 - 缺页异常

由于我们的系统加载进程时,将进程的所有页表载入内存中,所以不可能发生缺页异常……吗?

实际上仍然会发生缺页异常,这是因为进程会用到一些没有声明在 elf 中的内存。最典型的例子是栈空间。而栈空间的大小是未知的,所以需要动态加载。当然你确实可以事先分配适当的空间,然后一旦用户使用超过这个空间则通知用户出现 Stack Overflow。不过我更推荐支持无限栈的方式。

Tips:

/* Exception Code for Supervisor (SXLEN == 32):
 o ------------+----------------+------------------
 o   Interrupt | Exception Code | Description
 o ------------+----------------+------------------
 o       1     |        1       | Supervisor software interrupt
 o       1     |        5       | Supervisor timer interrupt
 o       1     |        9       | Supervisor external interrupt
 o       1     |      ≥16       | Designated for platform use
 o ------------+----------------+------------------
 o       0     |        0       | Instruction address misaligned
 o       0     |        1       | Instruction access fault
 o       0     |        2       | Illegal instruction
 o       0     |        3       | Breakpoint
 o       0     |        4       | Load address misaligned
 o       0     |        5       | Load access fault
 o       0     |        6       | Store/AMO address misaligned
 o       0     |        7       | Store/AMO access fault
 o       0     |        8       | Environment call from U-mode
 o       0     |        9       | Environment call from S-mode
 o       0     |       12       | Instruction page fault
 o       0     |       13       | Load page fault
 o       0     |       15       | Store/AMO page fault
 o       0     |      ≥24       | Designated for custom use
 o ------------+----------------+------------------
 */

异常 12,13,和 15 分别对应取指缺页,读缺页,和写缺页。
Note: AMO 的意识是读写原子操作,如原子自加操作等等。

在我们的 MOS 系统中,至少需要实现 15 号异常的处理。因为栈空间显然是先写后读的。

处理好了栈空间的缺页异常,我们就可以通过测试 3.4

但是这里有一个小 bug 需要解决,在 riscv32IC0x0000 是未知指令,真正的 nop 指令是 addi zero, zero, 0,编码为 0x0001。所以你需要自行更改测试点的 entry.S

关于调试

在这一个 Lab 中,我们的系统引入了用户态程序,然而用户态程序可能有多个,他们会使用同样的虚拟地址(尽管物理地址不同,但是 gdb 无权观测物理地址),而且不会在内核启动的同时载入。因此需要调试用户程序时,需要在内核启动并加载对应虚拟地址后,手动加载用户程序符号表,然后再添加断点。参考资料 1,可以使用 add-symbol-file <filename> <address>address 对于我们来说通常应该是 0x400000.

参考资料

  1. 如何在gdb中加载多个符号文件 - 问答 - 腾讯云开发者社区-腾讯云
  2. GitHub - QAQdev/onekos: A mini kernel of 2022 ZJU OS

Lab 4 系统调用和 Fork

页表自映射

指导书中非常清晰的表明了页表自映射在 riscv 中的实现方式是完全不同的。也给出了两种推荐的方式。当然了我个人是一点也不推荐其中的任何一种方式的。因为放弃自映射机制对 MOS 操作系统来说会有其它方面的严重影响,对于 Lab 4 来说,fork 是需要使用到自映射页表的,如果放弃自映射机制,可能需要新增一个 syscall 用于处理用户态对页表的访问请求。而指导书中给出的另外一种方式,需要为每个进程直接分配 4MB 的页表空间,这对于一个只有 64MB 内存的系统来说,实在是有点太多了,从节俭角度出发,确实无法接受。

至于我采用了什么方式,就留个悬念,下次再聊吧。

下面我给出一个用户态程序,这个程序可以检查你的页表自映射的正确性:

#include <lib.h>

Pde *pgdir = (Pde *)(UVPT | UVPT >> 10);

typedef struct _buffer
{
	u_int vaddr;
	u_int paddr;
	u_int size;
	u_int attr;
} buffer;

static void putchar(int ch)
{
	static char buf[64] = {};
	static u_int pos = 0;
	buf[pos++] = ch;
	buf[pos] = 0;
	if (ch == '\n') debugf(buf), pos = 0;
}

static void print(buffer buf)
{
	if (!(buf.attr & PTE_V)) return;
	debugf("%08x %08x %08x ", buf.vaddr, buf.paddr, buf.size);
	if (buf.attr & PTE_R) putchar('r');
	else putchar('-');
	if (buf.attr & PTE_W) putchar('w');
	else putchar('-');
	if (buf.attr & PTE_X) putchar('x');
	else putchar('-');
	if (buf.attr & PTE_U) putchar('u');
	else putchar('-');
	if (buf.attr & PTE_G) putchar('g');
	else putchar('-');
	if (buf.attr & PTE_A) putchar('a');
	else putchar('-');
	if (buf.attr & PTE_D) putchar('d');
	else putchar('-');
	if (buf.attr & PTE_COW) putchar('c');
	else putchar('-');
	if (buf.attr & PTE_LIBRARY) putchar('l');
	else putchar('-');
	putchar('\n');
}

static buffer buf = {};
static void pte_search(Pte *pte) {
	u_int va_pte = ((u_int) pte - UVPT) << 10;
	u_int va, pa, attr;
	if ((u_int) pte == (u_int) pgdir) return;
	for (int i = 0; i < 1024; i++) {
		if (pte[i] & PTE_V) {
			va = va_pte | i << 12;
			pa = PTE_ADDR(pte[i]);
			attr = pte[i] & 0x3ff;
			if (attr == buf.attr &&
			    va == buf.vaddr + buf.size &&
			    pa == buf.paddr + buf.size) {
				buf.size += BY2PG;
			} else {
				print(buf);
				buf.vaddr = va;
				buf.paddr = pa;
				buf.size = BY2PG;
				buf.attr = attr;
			}
		}
	}
}

static void map_search(Pde *pde) {
	for (int i = 0; i < 1024; i++) {
		if (pde[i] & PTE_V) {
			user_assert((pde[i] & 0x3ff) == PTE_V);
			pte_search((Pte *)(UVPT | i << 12));
		}
	}
}

int main() {
	debugf("%-8s %-8s %-8s %-9s\n", "vaddr", "paddr", "size", "attr");
	debugf("-------- -------- -------- ---------\n");
	map_search(pgdir);
	print(buf);
	return 0;
}

如果你的自映射机制实现合理,该程序输出的结果应该和 qemu 命令 info mem 的结果几乎一致。

几乎一致的意思是,根据你的自映射机制自行判断正确性。
一种可能的情况,你可以只实现 USTACKTOP 以下的页表项的查询。

简单的 syscall

这里主要需要改的就是一些很细微的差别。比如把 tf->regs[2]mips: $v0)改成 tf->regs[10]riscv: a0)之类的。还有以前用 PTE_ADDR 向下去整的(虽然本来就不该这样用),要改为 ROUNDDOWN。然后权限位都得改一改。以及记得添加权限 PTE_U

ForkCopy-on-Write

这里我尝试了很多次,发现虽然页表有映射,但是被设置为只读,然后尝试写入时仍然会触发 15 号异常 Store/AMO page fault 而非 7 号异常 Store/AMO access fault,这可能和 OpenSBI 的实现有关。不过问题不大,我们只需要在触发 15 号异常时检测一下页面是否存在就可以判断是不是 copy-on-write 了。

其它问题

注意本 Lab 在测试的时候可能需要改一些测试点的代码,主要是因为权限位的设置问题。

另外记得检查你自己实现的页表自映射有没有改变什么东西,有没有正确的释放资源。

以及,测试的时候记得在 -O0 模式(调试模式)和 -O2 模式(MOS_PROFILE=release 模式)都测试一下。然后我就测出了 bug,还以为是编译器优化的问题,跑到 GitHub 上发了个 issue,最后发现是我自己蠢了:(

以及 vscode 调试的时候可能有一个问题是 Debugger was unable to continue the process. 这个问题我也很想去 GitHub 上发 issue,但是发现这个问题时而发生时而不发生,非常神奇。最后我发现,当你的程序位于用户态时,不能有任何内核态断点,否则 gdb 会因为无法访问内核态断点而拒绝继续进行调试。

Lab 5 文件系统

准备工作

首先使用如下命令查看 QEMU 支持的 Virt 设备:

mkdir -p target
qemu-system-riscv32 -machine virt,dumpdtb=target/virt.dtb
dtc -I dtb -O dts target/virt.dtb > target/virt.dts

注意如果提示 dtc 不存在,则可以通过 apt-get install device-tree-compiler 下载

虽然实验指导书上明确有写默认的磁盘挂载地址在哪里,但是建议还是看一看 virt.dts 文件,可能会有意外收获

VIRTIO 基础知识

在开始正式写代码之前,一定要事先了解 VirtIO 的基础知识,不然就无从下手了。

参考资料推荐

这里我下面列出的参考资料中的 1,2,3 其实都是很不错的介绍。虽然可能不太完善,但是是中文的,且较为简短,适合新手。官方文档(资料 5)其实也是很好的,但是很长,不适合用于了解基础知识,可以用于写代码时的参考。官方文档上有详细的步骤,不过如果不了解基础知识,可能完全看不懂。代码层面上,参考资料 4 是非常完美的。xv6 是 MIT 开发的用作教学的操作系统,涵盖了进程,页表,中断,互斥锁,调度,和文件系统等多个方面,参考资料 4 是一些读者使用 riscv64 的版本。

简要介绍

下面我也给出我自己的理解和简要介绍。

Virtio 是一种磁盘虚拟化技术,采用半虚拟化,其效率远高于 IDE。Virtio 实现了一种统一化的标准,对于任何支持的设备,都能为驱动提供统一的标准和接口。我们的任务就是要根据 Virtio 的统一化标准,实现指定的接口,完成驱动与设备之间的通讯。

基本术语:

  1. Driver: 驱动,又称 Guest,实现在虚拟机中,为前端,也就是我们要实现的部分
  2. Device: 设备,又称 Host,实现在虚拟机监控器中,为后端,由 QEMU 实现
  3. Virtio Queue:是 Virtio 的数据传输的载体,为核心部分,也是接下来要重点关注的

Virtio Queue

Virtio Queue 是 Virtio 中数据传输的载体,一对设备和驱动之间可以有多个 Virtio Queue。Virtio Queue 主要包括三个部分:

  1. Descriptor Table

    描述符表,描述符结构体应包含 addr(地址),len(长度),flags(标志),next(指针)四个部分。
    addr: 想要共享的内存的起始地址,应当是虚拟机物理地址
    len: 想要共享的内存的长度
    flags: 记录该描述符是否为 Device 可写的,是否有下一个描述符
    next: 指向该描述符链的下一个描述符;通常一次数据传输中,一个描述符并不够用,所以需要将多个链连在一起使用

  2. Available Ring

    可用描述符队列,该队列主要维护可用描述符链头的 id,该队列的队尾由 Driver 维护,Driver 向队尾写入新的描述符链向 Device 传递请求,Device 从队头取出描述符链进行处理。

  3. Used Ring

    已用描述符队列,和上面正好相反,用于维护已用描述符链头的 id,队尾由 Device 维护,Device 会将从 Available Ring 中取出的,已经完成的请求放回 Used Ring 队列中,Driver 从队头取出描述符链进行回收。

实际使用的描述符个数以及通知方式应当在设备初始化阶段协商。

这部分源代码可以参考 /usr/include/linux/virtio_ring.h

设备初始化

参考官方文档 3.1 和 4.2.3,一共有 8 步:

  1. 重置设备
  2. Guest OS 声明已经发现 Device
  3. Guest OS 声明可以驱动 Device
  4. Driver 读取 Device 所支持的功能/特性说明,并选择一个子集使用
  5. Driver 告知 Device 将会使用/实现的功能/特性
  6. Driver 读取 Device 是否接受上一步所选择的特性
  7. Driver 读取/写入 Device 的其它设置位,MMIO 主要需要实现以下设定:
    1. 选择一个 Virtqueue,通常选择 id=0 的
    2. 检查这个队列是否是空闲的
    3. 检查这个队列支持的最大描述符数量
    4. 为 Descriptor Table,Available Ring,Used Ring 分配物理内存
    5. 告知 Device 要使用的描述符数量
    6. 分别告知 Device:Descriptor Table,Available Ring,Used Ring 的物理地址
    7. 告知 Device 这个队列已经准备完毕了
  8. Driver 告知 Device 初始化结束

I/O 事件

在传统的针对 block 设备的 I/O 事件中,我们一次使用 3 个描述符发送请求,包括以下内容:

  1. 描述结构体,包括请求类型,请求 sector 编号,优先级
  2. 需要读写的内存
  3. I/O事件状态位

具体实现可以参考资料 4 的代码和文档 3.2.1

至于回收 I/O 事件,可以采用 busy-waiting 读取状态位或要求 Device 发出硬件中断等方式实现。

我采用的是 busy-waiting 方式(当然是 waiting+yield),并且屏蔽了硬件中断避免频繁陷入内核态影响程序运行效率。

善后工作

善后工作就很简单了,无非就是改改 syscall 接口,更改 ide.c 这样的事情。其它部分都是不需要动的。(当然是页表项权限相关的东西也是肯定要改的)。

参考资料

  1. 0020 virtio-blk简易驱动 - 知乎
  2. 通过MMIO的方式实现VIRTIO-BLK设备 - 知乎
  3. 通过MMIO的方式实现VIRTIO-BLK设备(二) - 知乎
  4. xv6-riscv/virtio_disk.c at riscv · mit-pdos/xv6-riscv · GitHub
  5. docs.oasis-open.org/virtio/virtio/v1.0/cs04/virtio-v1.0-cs04.pdf

Lab 6 Shell

在完成 Lab 5 以后,整个移植任务可以说就已经基本结束了。Lab 6 的移植非常简单,因为大多是用户层面的内容,不涉及底层。可能唯一需要注意的是 spawnl 函数利用了 mips 的传参性质,而 riscv 中不能这样做。另外再注意修改修改有关对 Trapframe 的修改就可以了。

–74d3de5299e5899a31057eff35dd93c6–

–b58e6fe6fbbe7ec05008e2c6864397d2–


评论
  目录