rcore

ch1

_start() 函数

_start() 函数是程序的入口函数,为了实现一个完全不依赖 rust 标准库的操作系统内核,
首先移除了 rust 标准库依赖,随后我们需要为编译器提供入口函数 _start()

1
2
3
extern "C" fn _start() {
loop{};
}

实现 exit() 系统调用

为编译器提供了入口函数之后,编译器就可以编译程序了。但是程序还没有一个正常的退出方式,
我们需要为内核实现 exit() 系统调用,这样程序就可以正常退出了。使用 ecall 指令可以访问
操作系统提供的系统调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
fn syscall(id: usize, args: [usize; 3]) -> isize {
let mut ret;
unsafe {
core::arch::asm!(
"ecall",
inlateout("x10") args[0] => ret,
in("x11") args[1],
in("x12") args[2],
in("x17") id,
);
}
ret
}

实现 write() 系统调用

为了能够在控制台上输出信息,我们需要实现 write() 系统调用。实现完
write() 系统调用之后,我们就可以为微内核实现控制太打印信息的相关函数
和宏了。

实现 shutdown()

为了实现模拟器(系统)的关机,我们需要调用 sbi 提供的 shutdown() 系统调用。
sbiRISC-V 的一种底层规范,用于实现操作系统和硬件之间的通信。同应用程序
访问操作系统的系统调用一样,操作系统通过 ecall 指令访问 sbi 提供的

程序内存布局和清空 bss

没什么好说的了,按照需要设置程序的内存布局(个人认为应该说是操作系统的内存布局吧),
然后在内核初始化时需要清空 bss 段。关于 bss 段,程序分配的全局变量储存在 bss 段,
他们的初值被设定为0。(亦是为什么要清空 bss 段)

ch2

加载批处理程序到内核

link_app.S 将应用程序链接到内核,目前我对此汇编的理解,就是将应用程序加载进内核。

AppManager 初始化的时候,从内核中读取 num_app 以及每个 app 的起始地址。

load_app() 函数将应用程序加载到内存中,具体的,加载到内存约定的应用程序地址处,
这里是 0x80400000

实现特权级切换

RISC-V 共有三种特权级,分别是 M 级、S 级和 U 级。执行用户程序时,CPU 处于 U 级,
但当用户需要访问操作系统的资源(如系统调用),需要切换到 S 级。结束时,再切换回 U 级。
M 是机器模式,是最高特权级,用于实现操作系统的启动和关机,同时 M 级别也是最高特权级,
可以执行一切指令。

特权级的切换主要的认为是保存和恢复上下文。

ch3

多道应用程序加载

由于需要实现多道程序执行,我们需要将待执行的应用程序加载到内存,这个为每个应用程序设置
固定的大小,即 0x20000 字节。然后将应用程序加载到对应的位置即可。

__switch() 函数

__swicth() 函数实现上下文切换,上下文切换的时候需要保存当前的寄存器状态,
然后恢复下一个任务的寄存器状态。

具体的说,保存当前任务上下文的 ra sp s0-s11 寄存器,然后恢复下一个任务的寄存器状态。
在实现的时候,遵循 RISC-V 的函数调用约定,使用 a0a1 寄存器传递函数调用参数,
即当前任务和下一个任务的 TaskContext 指针。

管理多道程序的执行

大部分程序的执行流程都是小部分有效指令的和大部分的 I\O 指令,在等待 I\O 时, CPU
处于空闲状态,这显然与我们压榨 CPU 的理念不同,所有我们希望,当一个程序在等待 I\O
操作时,应该将 CPU 的控制权交给另一个程序,这样就可以充分利用 CPU 的计算能力。
为此我们需要实现 sys_yield() 。同样的,当一个程序运行结束时,我们也希望它交出
CPU 的控制权,让其他程序执行,这需要实现 sys_exit() 。这两个函数的实现过程基本
一致,首先是修改当前程序控制块的描述的程序状态,然后切换到下一个程序执行。

分时多任务系统

这里主要实现时钟中断相关和抢占式调度,有了时钟中断,还可以实现更复杂的调度算法。

ch4

开启 MMU 使能

默认情况下,RISC-V 是没有开启 MMU 的,这导致了所有的程序都运行在同一个地址空间,
我们使用物理地址直接访问内存。开启 MMU 之后,我们可以使用虚拟地址访问内存,并且
可以更好的管理内存访问。

通过修改 satp 寄存器,具体的说,我们需要设置 satp 寄存器的 MODE 字段为 8

地址格式(从高位到低位)

  1. 物理地址:44位页框号,12位页内偏移
  2. 虚拟地址:27位页号,12位页内偏移

注:虚拟页面和物理页框都按 4k 对齐,故页内偏移实际上一致,在地址转换时只需要
找到页号和页框号的映射即可。

页表格式(从高位到低位)

10位保留位(必须为0),44位页框号,2位 RSW (保留位,必须为0) 以及8位标志位。

标志位包括:

  • D :Dirty,表示页面是否被修改过。

  • A :Accessed,表示页面是否被访问过。

  • G :Global,表示页面是否是全局页面。

  • U :User,表示页面是否是用户页面。

  • X :Executable,表示页面是否可执行。

  • W :Writable,表示页面是否可写。

  • R :Readable,表示页面是否可读。

  • V :Valid,表示页面是否有效。

物理页的分配与回收

采用栈式物理页帧管理策略,使用 currentend 指示从未分配过的物理页框的页框号,
并使用 recycled 数组记录已分配过并回收的页框号。

分配的逻辑是,首先在 recycled 中查找是否有已回收的页框号,如果有则直接返回,否则
检测 currentend 是否相等,如果相等则代表无页框可用,直接 panic ,否则返回 current
并将 current 加1。

回收则比较简单,直接将页框号加入 recycled 数组即可。

物理内存的访问

物理页帧的访问,使用 into() 将物理页框号直接转化成物理地址,根据物理地址访问内存即可。
尽管我们在程序中得到的物理地址会被 MMU 视作虚拟地址,但是由于这里虚拟页号到物理页框号
的映射是恒等隐射,所有虚拟地址和物理地址一样。