总体架构
背景知识 多道程序操作系统 :内存中尽量同时驻留多个应用,这样处理器的利用率就会提高。但只有一个程序执行完毕后或主动放弃执行,处理器才能执行另外一个程序。 aka. 支持 多道程序 或 协作式多任务 的协作式操作系统
代码 overview 在 批处理系统 的基础上,多道程序的协作式操作系统的区别主要在于:
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 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 ) }; for i in 0 ..num_app { let base_i = get_base_i (i); (base_i..base_i + APP_SIZE_LIMIT) .for_each(|addr| unsafe { (addr as *mut u8 ).write_volatile (0 ) }); 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! { 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 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 (), )) }impl TrapContext { pub fn set_sp (&mut self , sp: usize ) { self .x[2 ] = sp; } pub fn app_init_context (entry: usize , sp: usize ) -> Self { let mut sstatus = sstatus::read (); sstatus.set_spp (SPP::User); let mut cx = Self { x: [0 ; 32 ], sstatus, sepc: entry, }; cx.set_sp (sp); cx } }
可以看到,在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 (); unsafe { __switch(&mut _unused as *mut TaskContext, next_task_cx_ptr); } panic! ("unreachable in run_first_task!" ); }
TaskManager
同AppManager
一样,是全局变量。我们需要先获得互斥的访问权。我们构造了一个不存在的 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 放入现在的寄存器(ra
、sp
、s0..s11
),实际上等于切换到B内核栈
回忆之前ra
中放的是__restore
的地址,sp
放的是B内核栈中保存的B用户态的上下文TrapContext
,效果是:run_next_task
结束后,使用ra
跳转到__restore
,后者使用sp
指向的内容,恢复到B用户态
下一个操作系统:使用时钟让时间暂停!