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 视作虚拟地址,但是由于这里虚拟页号到物理页框号
的映射是恒等隐射,所有虚拟地址和物理地址一样。

Struct

Unit-like Struct

没有任何字段的结构体。如:

struct UnitLikeStruct;

Struct Update Syntax

可以利用已有结构体创建新的结构体。如:

struct Point {
    x: i32,
    y: i32,
    z: i31,
}

let p1 = Point { x: 0, y: 0, z: 0 };
let p2 = Point { x: 1, ..p1 };

类型详解

newtype 的使用

rust 中无法为外部类型实现外部特征,想要做到这一点,可以利用 newtype 将外部类型
包装成新的类型。如:

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

类型别名

类型别名类似C语言中 typedef ,可以简化复杂的类型名称,方便使用。如:

type Result<T> = std::result::Result<T, std::io::Error>;
0%