阅前须知/免责声明
本文用于记录 OS 挑战任务——将 MOS 操作系统移植到 RISC-V 架构。
本文仅供学习交流使用,不得用于商业用途。本文中的所有内容均为原创,如需转载请联系本人。
本文不是移植报告或指导书,仅是移植过程的记录,每个 Lab 的内容都是我自己参考众多资料后摸索出来的,可能有错误,不保证正确性或最优解。
并且本文中的内容可能不是最新的,因为我在移植过程中也在不断地学习和尝试,所以可能会有一些错误或不完善的地方,欢迎指正。
本文中提到的实现不代表我最终的实现,特别是前几个 Lab,很可能在之后的 Lab 中被修改或替换。
最终移植后的源代码将在挑战性任务答辩结束后开源,敬请期待。
Lab 0 环境配置
按照实验教程所述,本实验推荐使用常见 Linux 发行版作为开发环境,虽然 Mac 可能也可以,但是在本地安装那么庞大的环境,特别是还有可能出错,实在是不太推荐。如果有想尝试本地安装的,建议 Windows 用户使用 WSL2,Mac 用户使用 Docker。这里我选择了 Github Codespaces,学生一个月免费 180 CPUhours,足够使用了(后来发现可能不太够用,所以还是很推荐 Docker)。
交叉编译器
- 首先使用
sudo su
提权到 root 用户,这里可能要求输入密码; - 执行
apt update
更新索引; - 执行
apt install gcc-riscv64-unknown-elf
安装实验所需要的交叉编译器。
QEMU
- 执行
git clone https://github.com/qemu/qemu.git --recursive
从 Github 克隆 QEMU 源代码; - 执行
cd qemu
切换到克隆好的仓库下; - 执行
mkdir build
随后cd build
新建一个build
目录并切换过去; - 尝试执行
../configure --target-list=riscv32-softmmu
,此时可能因为没有相关依赖而失败,按要求尝试安装所需依赖。实测只需要下面两种:ninja
: 使用apt install ninja-build
安装;pixman-1
: 使用apt install libpixman-1-dev
安装。
- 执行
make
编译(执行make install
可以直接把qemu-system-riscv32
装在/usr/local/bin
下,可以直接使用)
一点 Tips:
如果遇到一些不知如何安装的依赖项,推荐 command-not-found.com 和 Google 进行搜索。
另外如果克隆源代码时不使用 --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 |
- 更改交叉编译器,并删掉不支持的编译选项,然后编译选项添加
-march=rv32gc
和-mabi=ilp32
,链接选项添加-melf32lriscv
(参考资料 1); - 删掉不支持的伪指令,并更改关闭中断的汇编指令(参考资料 2, 5);
- 删掉不支持的伪指令;
- 更改寄存器名字和序号对应表;
- 链接脚本可以参考实验指导书;
- 暂时注释掉用到的汇编指令。
完成上述操作,你应该能成功通过编译。
在这一步中,你可能遇到 #include_next
的相关错误 -ffreestanding
可能对你有所帮助。
-ffreestanding
:Do not assume that standard C libraries and "main" exist.
启动内核
如果直接运行上面 make
出来的内核会有一个非常严重的问题。我们注意到 OpenSBI
引导后,默认会跳转到 0x80200000
,但是通过 objdump
得知,我们编译出的 mos
在 0x80200000
位置处是 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)。
这里我们可以创建一个头文件用于储存所有 OpenSBI
的 ecall
调用(并不是都一定要实现,用不到的可以暂时不实现)。
#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 dbg
(Makefile
也要记得配置哦~),然后按下F5
进行调试Tips: 如果想要给
.S
文件添加断点,可以在调试栏中手动输入标签名或机器地址
参考资料
- RISC-V嵌入式开发入门篇1:RISC-V GCC工具链的介绍_半斗米的博客-CSDN博客
- RISC-V 指令概况 - 计算机组成原理(2021年)
- Lab 1: RV64 内核引导 - 知乎
- riscv-sbi-doc/riscv-sbi.adoc at master · riscv-non-isa/riscv-sbi-doc · GitHub
- PolarFire® SoC MSS Technical Reference Manual
- 浙江大学22年秋操作系统实验
Lab2 MMU 设置和内存管理
关于这一单元要实现的内容,实验书中写的非常详细。然而,关于具体如何实现^^
这里给几个重要的参考提示吧。
- 关于
sfence.vma
,是一个用于刷新fence
的指令,大致可以类比mips
中的刷新tlb
相关指令。其主要需要使用的地方如下:(摘至riscv-privileged-20211203.pdf
) riscv
在S
态的物理内存开始于0x80000000
。riscv
的获取总内存只能在M
态进行。qemu-system-riscv32
默认内存是64MB
。- 关于特权位,首先要改宏,其次原来
PTE_D
的地方都应该改为PTE_W
。另外还要注意设置PTE_R
和PTE_X
。并且还要注意他们的相互关系。本Lab
我们无需关心其它特权位。 _do_tlb_refill
相关函数其实是需要的,但是并非在这个lab
中,而且写法肯定也完全不同,表达的含义也略有差别,passive_alloc
确实可以考虑留下来。- 由于在
Lab2
中我们只有内核态,而且即便建立了SV32
虚拟内存映射,实质上还是一个等值映射(即虚拟地址和对应物理地址相等),所以直接使用物理内存也是可以的。但是我还是强烈建议在Lab2
建立虚拟地址映射,这也是为后续的用户态的加入做好准备。 - 另外善用
qemu
的指令也很重要。在模拟时,按下Ctrl+A
随后按下C
可以看到(qemu)
的标志出现,表示此时你可以输入qemu
支持的指令。可以用help
查看帮助。一般来说,最常见的是info mem
查看虚拟内存映射,info registers
查看寄存器。需要注意在输入指令模式下模拟不会停止(除非输入指令stop
),因此建议搭配gdb
使用。 - 更多技术细节可以参考浙大 OS 实验 Lab4。
参考资料
- 10.4 自制操作系统: risc-v 虚拟内存系统_sv39_richard.dai的博客-CSDN博客
- GitHub - ZJU-SEC/os22fall-stu: https://zju-sec.github.io/os22fall-stu/
- RISC-V Technical Specifications - Home - RISC-V International
Lab3 异常处理和进程管理
这一个 Lab
的主要任务是完成异常处理和进程管理,进程管理的难度不大,改动较少,异常处理内容就比较多(这是因为 riscv
和 mips
异常处理相关的寄存器是完全不一样的)。
异常处理 1 - 时钟中断
本部分建议参考浙大 OS 实验 Lab2
。
在 riscv
中设置时钟中断分为三步:
- 调整
sstatus
和sie
寄存器的值,使得 CPU 允许中断发生。 - 使用
rdtime
指令获取当前时间 (CPU周期数)。 - 调用
OpenSBI
的接口设置下一次中断发生的时间。
在这一个子任务中,可以暂时注释掉有关保存上下文到 TrapFrame
的代码,简单进行一些直接压栈即可,另外可以将 schedule
改为输出一条消息后立即 sret
,不进行进程管理,方便查看时钟中断是否实现正确。另外也要注意调整调整时钟发生的频率,验证时钟中断发生频率是否合理。
进程管理
主要任务如下:
- 更改页表映射的权限位,包括但不限于
base_pgdir
以及elf_load_seg
。 - 在为进程初始化虚拟内存的时候,应该将内核虚拟地址全部映射到进程的页表中,这样做才能在不切换页表的条件下进行
S
态异常处理和系统调用。这也是为什么我推荐在上一个Lab
中做好内核的虚拟地址映射。 - 在进入异常处理的时候进行上下文保存。保存在
KSTACKTOP
中。合理利用sscratch
寄存器会对这个过程有所帮助。 - 初始化进程的
TrapFrame
的时候需要根据需要初始化,使得sret
后sp
和sstatus
等寄存器都有合理值。 - 正确实现
env_pop_tf
,这一步需要进行切换页表操作,以及为恢复上下午做准备(设置合理的栈帧)。切换页表是立即生效的,所以理论上你不需要刷新fence
。但是可能出现的问题是如果asid
出现了循环利用,这时就会出现satp
中的页表与fence
中的不同,所以需要考虑在删除旧页表的时候,或者初始化新页表时刷新fence
。
正确完成后应该能通过测试 3.1
,3.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 需要解决,在
riscv32IC
中0x0000
是未知指令,真正的nop
指令是addi zero, zero, 0
,编码为0x0001
。所以你需要自行更改测试点的entry.S
关于调试
在这一个 Lab
中,我们的系统引入了用户态程序,然而用户态程序可能有多个,他们会使用同样的虚拟地址(尽管物理地址不同,但是 gdb
无权观测物理地址),而且不会在内核启动的同时载入。因此需要调试用户程序时,需要在内核启动并加载对应虚拟地址后,手动加载用户程序符号表,然后再添加断点。参考资料 1,可以使用 add-symbol-file <filename> <address>
,address
对于我们来说通常应该是 0x400000
.
参考资料
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
。
Fork
和 Copy-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 的统一化标准,实现指定的接口,完成驱动与设备之间的通讯。
基本术语:
- Driver: 驱动,又称 Guest,实现在虚拟机中,为前端,也就是我们要实现的部分
- Device: 设备,又称 Host,实现在虚拟机监控器中,为后端,由 QEMU 实现
- Virtio Queue:是 Virtio 的数据传输的载体,为核心部分,也是接下来要重点关注的
Virtio Queue
Virtio Queue 是 Virtio 中数据传输的载体,一对设备和驱动之间可以有多个 Virtio Queue。Virtio Queue 主要包括三个部分:
- Descriptor Table
描述符表,描述符结构体应包含
addr
(地址),len
(长度),flags
(标志),next
(指针)四个部分。addr
: 想要共享的内存的起始地址,应当是虚拟机物理地址len
: 想要共享的内存的长度flags
: 记录该描述符是否为 Device 可写的,是否有下一个描述符next
: 指向该描述符链的下一个描述符;通常一次数据传输中,一个描述符并不够用,所以需要将多个链连在一起使用 - Available Ring
可用描述符队列,该队列主要维护可用描述符链头的 id,该队列的队尾由 Driver 维护,Driver 向队尾写入新的描述符链向 Device 传递请求,Device 从队头取出描述符链进行处理。
- Used Ring
已用描述符队列,和上面正好相反,用于维护已用描述符链头的 id,队尾由 Device 维护,Device 会将从 Available Ring 中取出的,已经完成的请求放回 Used Ring 队列中,Driver 从队头取出描述符链进行回收。
实际使用的描述符个数以及通知方式应当在设备初始化阶段协商。
这部分源代码可以参考 /usr/include/linux/virtio_ring.h
设备初始化
参考官方文档 3.1 和 4.2.3,一共有 8 步:
- 重置设备
- Guest OS 声明已经发现 Device
- Guest OS 声明可以驱动 Device
- Driver 读取 Device 所支持的功能/特性说明,并选择一个子集使用
- Driver 告知 Device 将会使用/实现的功能/特性
- Driver 读取 Device 是否接受上一步所选择的特性
- Driver 读取/写入 Device 的其它设置位,MMIO 主要需要实现以下设定:
- 选择一个 Virtqueue,通常选择 id=0 的
- 检查这个队列是否是空闲的
- 检查这个队列支持的最大描述符数量
- 为 Descriptor Table,Available Ring,Used Ring 分配物理内存
- 告知 Device 要使用的描述符数量
- 分别告知 Device:Descriptor Table,Available Ring,Used Ring 的物理地址
- 告知 Device 这个队列已经准备完毕了
- Driver 告知 Device 初始化结束
I/O 事件
在传统的针对 block 设备的 I/O 事件中,我们一次使用 3 个描述符发送请求,包括以下内容:
- 描述结构体,包括请求类型,请求 sector 编号,优先级
- 需要读写的内存
- I/O事件状态位
具体实现可以参考资料 4 的代码和文档 3.2.1
至于回收 I/O 事件,可以采用 busy-waiting 读取状态位或要求 Device 发出硬件中断等方式实现。
我采用的是 busy-waiting 方式(当然是 waiting+yield),并且屏蔽了硬件中断避免频繁陷入内核态影响程序运行效率。
善后工作
善后工作就很简单了,无非就是改改 syscall 接口,更改 ide.c 这样的事情。其它部分都是不需要动的。(当然是页表项权限相关的东西也是肯定要改的)。
参考资料
- 0020 virtio-blk简易驱动 - 知乎
- 通过MMIO的方式实现VIRTIO-BLK设备 - 知乎
- 通过MMIO的方式实现VIRTIO-BLK设备(二) - 知乎
- xv6-riscv/virtio_disk.c at riscv · mit-pdos/xv6-riscv · GitHub
- 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–