RCore-始初龙

总体架构

锯齿螈多道程序操作系统 -- Multiprog OS总体结构

始初龙协作式多道程序操作系统 -- CoopOS总体结构

背景知识

多道程序操作系统:内存中尽量同时驻留多个应用,这样处理器的利用率就会提高。但只有一个程序执行完毕后或主动放弃执行,处理器才能执行另外一个程序。 aka. 支持 多道程序 或 协作式多任务 的协作式操作系统

代码

overview

在 批处理系统 的基础上,多道程序的协作式操作系统的区别主要在于:

  • 全部应用都会直接放在内存load_apps(),无需运行时再加载(batch 时需要加载到APP_BASE_ADDRESS)。

  • 每个应用都有自己的内核/用户栈

1
2
3
4
5
6
7
static KERNEL_STACK: [KernelStack; MAX_APP_NUM] = [KernelStack {
data: [0; KERNEL_STACK_SIZE],
}; MAX_APP_NUM];

static USER_STACK: [UserStack; MAX_APP_NUM] = [UserStack {
data: [0; USER_STACK_SIZE],
}; MAX_APP_NUM];
  • 原来batch.rs的大部分加载逻辑放在了loader.rs

为什么不需要修改 trap 文件夹的代码?我认为是因为 trap 只用于单个app的内核/用户态的切换,而 多个程序的切换 对于 单个 app 是透明的。事实上,多个程序间的切换的逻辑放在task文件夹下。

rust

需要写一些不熟的 rust 语法知识

lazy_static

为什么需要 lazy_static!?在 Rust 中,普通的 static 变量必须在编译时初始化,且初始值必须是常量表达式。然而,有些场景下我们需要在运行时初始化静态变量(例如,初始化一个复杂的结构体、动态分配内存、调用函数等)。这时,lazy_static! 就派上用场了。

task

本操作系统的重点是:怎么在不同的 app 之间切换?yield的过程?

Just yield, baby.

启动

比较好奇从系统启动到开始运行 app 发生了什么,什么时候发生 app 的切换?怎么切换?

  • 进入 rust_main:看第一章的笔记
  • 进入 trap 模块的init():设置 trap 处理入口,详情看第二章笔记

加载apps

我们会将他们加载到:

1
2
3
4
/// Get base address of app i.
fn get_base_i(app_id: usize) -> usize {
APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT
}

将 app 的二进制文件加载到内存中:src to dst

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pub fn load_apps() {
extern "C" {
fn _num_app();
}
let num_app_ptr = _num_app as usize as *const usize;
let num_app = get_num_app();
let app_start = unsafe { core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1) };
// load apps
for i in 0..num_app {
let base_i = get_base_i(i);
// clear region
(base_i..base_i + APP_SIZE_LIMIT)
.for_each(|addr| unsafe { (addr as *mut u8).write_volatile(0) });
// load app from data section to memory
let src = unsafe {
core::slice::from_raw_parts(app_start[i] as *const u8, app_start[i + 1] - app_start[i])
};
let dst = unsafe { core::slice::from_raw_parts_mut(base_i as *mut u8, src.len()) };
dst.copy_from_slice(src);
}

unsafe {
asm!("fence.i");
}
}

这部分与前一章的内容略有不同:我们需要加载全部的app。至于怎么从脚本得到汇编代码,从汇编代码中得到 app数量和起止位置等,和前一章基本一样。

进入第一个 app 的用户态

我们需要先关注TaskManager创建初始化时干了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
lazy_static! {
/// Global variable: TASK_MANAGER
pub static ref TASK_MANAGER: TaskManager = {
let num_app = get_num_app();
let mut tasks = [TaskControlBlock {
task_cx: TaskContext::zero_init(),
task_status: TaskStatus::UnInit,
}; MAX_APP_NUM];
for (i, task) in tasks.iter_mut().enumerate() {
task.task_cx = TaskContext::goto_restore(init_app_cx(i));
task.task_status = TaskStatus::Ready;
}
TaskManager {
num_app,
inner: unsafe {
UPSafeCell::new(TaskManagerInner {
tasks,
current_task: 0,
})
},
}
};
}

可以看到,它设置好了每个 app 的上下文,并将状态都设为ready,并开始 task0,我们再深入进去:app的上下文是怎么初始化的:goto_restore(init_app_cx(i))发生了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// in loader.rs
pub fn init_app_cx(app_id: usize) -> usize {
KERNEL_STACK[app_id].push_context(TrapContext::app_init_context(
get_base_i(app_id),
USER_STACK[app_id].get_sp(),
))
}

// in trap/context.rs
impl TrapContext {
/// set stack pointer to x_2 reg (sp)
pub fn set_sp(&mut self, sp: usize) {
self.x[2] = sp;
}
/// init app context
pub fn app_init_context(entry: usize, sp: usize) -> Self {
let mut sstatus = sstatus::read(); // CSR sstatus
sstatus.set_spp(SPP::User); //previous privilege mode: user mode
let mut cx = Self {
x: [0; 32],
sstatus,
sepc: entry, // entry point of app
};
cx.set_sp(sp); // app's user stack pointer
cx // return initial Trap Context of app
}
}

可以看到,在init_app_cx中,我们根据该 app 的 base address 来设置其 TrapContext 的中入口寄存器sepc,根据该 app 的用户栈的地址来设置其 TrapContext 中的栈顶寄存器x2,并将该构造好的 TrapContext 推入内核栈,返回指向该上下文的指针(见push_context)。

goto_restore接收了该指针,并构造对应的 TaskContext:将返回地址ra设置为__restore,将sp设置为 TrapContext 的地址,初始化s0..s11

至此,完成对所有 task 的初始化。我们再看看run_first_task是怎么启动第一个 app 的:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn run_first_task(&self) -> ! {
let mut inner = self.inner.exclusive_access();
let task0 = &mut inner.tasks[0];
task0.task_status = TaskStatus::Running;
let next_task_cx_ptr = &task0.task_cx as *const TaskContext;
drop(inner);
let mut _unused = TaskContext::zero_init();
// before this, we should drop local variables that must be dropped manually
unsafe {
__switch(&mut _unused as *mut TaskContext, next_task_cx_ptr);
}
panic!("unreachable in run_first_task!");
}

TaskManagerAppManager一样,是全局变量。我们需要先获得互斥的访问权。我们构造了一个不存在的 task:_unused,并切换到 task0,由于我们之前已经设置好入口、trap 的上下文等,可以顺利开始 task0

switch

当一个 app 调用 yield 时,进入内核态,TaskManager会根据各个应用的状态来调度(run_next_task()),然后调用 __switch 来切换应用。

值得注意的是,__switch只会在内核态被调用,所以实际上它切换的是不同应用的内核栈空间,在进入新的内核栈后,再通过该内核栈上保存的值来到达新的应用。

其实到这里基本可以猜出我们是怎么换 task 的了:

  • A应用发出切换请求sys_yield 或者sys_exit
  • 进入 trap:保存A用户栈,切换A内核栈,A用户态的上下文放在A内核栈。执行 syscall,最终调用run_next_task
  • 使用__switch,交换 TaskContext,将新应用的 TaskContext 放入现在的寄存器(rasps0..s11),实际上等于切换到B内核栈
  • 回忆之前ra中放的是__restore的地址,sp放的是B内核栈中保存的B用户态的上下文TrapContext,效果是:run_next_task结束后,使用ra跳转到__restore,后者使用sp指向的内容,恢复到B用户态

switch

下一个操作系统:使用时钟让时间暂停!


RCore-始初龙
https://pactheman123.github.io/2025/01/24/RCore-始初龙/
作者
Xiaopac
发布于
2025年1月24日
许可协议