rcore
ch1
_start()
函数
_start()
函数是程序的入口函数,为了实现一个完全不依赖 rust
标准库的操作系统内核,
首先移除了 rust
标准库依赖,随后我们需要为编译器提供入口函数 _start()
。
1 | extern "C" fn _start() { |
实现 exit()
系统调用
为编译器提供了入口函数之后,编译器就可以编译程序了。但是程序还没有一个正常的退出方式,
我们需要为内核实现 exit()
系统调用,这样程序就可以正常退出了。使用 ecall
指令可以访问
操作系统提供的系统调用。
1 | fn syscall(id: usize, args: [usize; 3]) -> isize { |
实现 write()
系统调用
为了能够在控制台上输出信息,我们需要实现 write()
系统调用。实现完write()
系统调用之后,我们就可以为微内核实现控制太打印信息的相关函数
和宏了。
实现 shutdown()
为了实现模拟器(系统)的关机,我们需要调用 sbi
提供的 shutdown()
系统调用。sbi
是 RISC-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
的函数调用约定,使用 a0
和 a1
寄存器传递函数调用参数,
即当前任务和下一个任务的 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
。
地址格式(从高位到低位)
- 物理地址:44位页框号,12位页内偏移
- 虚拟地址: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,表示页面是否有效。
物理页的分配与回收
采用栈式物理页帧管理策略,使用 current
和 end
指示从未分配过的物理页框的页框号,
并使用 recycled
数组记录已分配过并回收的页框号。
分配的逻辑是,首先在 recycled
中查找是否有已回收的页框号,如果有则直接返回,否则
检测 current
和 end
是否相等,如果相等则代表无页框可用,直接 panic
,否则返回 current
,
并将 current
加1。
回收则比较简单,直接将页框号加入 recycled
数组即可。
物理内存的访问
物理页帧的访问,使用 into()
将物理页框号直接转化成物理地址,根据物理地址访问内存即可。
尽管我们在程序中得到的物理地址会被 MMU
视作虚拟地址,但是由于这里虚拟页号到物理页框号
的映射是恒等隐射,所有虚拟地址和物理地址一样。