rCore-N 的多核改造
在前文中,我们描述了用户态中断的机制和功能。然而基于单核环境的 rCore-Tutorial v3.5 改造的 rCore-N 并不能发挥出用户态中断的优势,所以我们需要对其进行多核改造。
多核启动
rustsbi-qemu 的多核启动
对于 SMP 架构,多核启动的一种简单方法是先启动核 0,在核 0 完成 OS 的启动后,通过某种方式“唤醒”其余的核。在 rustsbi-qemu 中,所有核在上电后都直接启动,执行到 rust_main
中的 mp_hook
函数。在 mp_hook
中,编号非 0 的核都会陷入循环,等待“M 态软件中断”来跳出循环,执行 sbi 层的初始化配置后跳转 OS。
rCore-N 的多核启动
启动栈
sbi 层完成配置后,首先进入一段汇编进行启动栈的配置,再跳转 rust 编写的内核初始化函数 rust_main
。我们使用 4 核的 SMP 架构,每个核有 64KiB 的启动栈,即在 .bss.stack
段共有 256KiB 的空间用于 OS 的启动。此段根据核的 id,地址从低到高进行分配。
初始化
OS 的初始化分为三个部分:核 0 进行整体的初始化、唤醒其他核、其他核自身的初始化。
整体初始化阶段,OS 需要初始化内存、PLIC 和共用的串口外设。在完成初始化后,核 0 通过调用 sbi 提供的接口,向另外三个核发送 IPI,通过 SBI 转为 M 态软件中断来唤醒它们。
其他核被唤醒后,进入相同的内核初始化函数,并根据其 id 进入核自身的初始化部分,设置自己的寄存器并配置对应自己的 PLIC 上下文。
简略的内核初始化函数如下:
#![allow(unused)] fn main() { pub fn rust_main(hart_id: usize) -> ! { if hart_id == 0 { // 全局初始化 核0初始化 // 唤醒其他核 for i in 1..CPU_NUM { let mask: usize = 1 << i; send_ipi(&mask as *const _ as usize); } } else { // 其他核初始化 } // 开始运行 } }
进程调度
调度队列
出于实现和维护的简易性与简单使用场景的考虑,我们采用了单队列调度的方式。所有核共用一个“进程池”,其中只有一个进程调度队列;每个核有一个自己正持有并运行的进程。
#![allow(unused)] fn main() { pub struct TaskPool { pub scheduler: TaskManager, // 共享的调度队列 ... } struct ProcessorInner { current: Option<Arc<TaskControlBlock>>, // 该核持有的进程 ... } }
内核开始运行后,进入一个死循环,不断尝试从共享的调度队列中取出一个进程开始运行,如果没有取出则进入下一次循环。即多核不断抢同一个队列中的资源,进程可能频繁地在核之间轮转,因而会有亲和性的问题发生。但在进程数恰为核数时,进程几乎与核绑定,便可以避免不亲和的问题。
死锁问题
在进程退出时,需要将属于自己的子进程交付不会退出的 0 号进程 initproc
来避免资源浪费。这要求同时持有进程自身和 initproc
的锁,在多核情境下会与 waitpid
等需要进程锁的函数发生冲突,甚至产生死锁。
目前 rCore-N 中采用一个名为 WAIT_LOCK
的大锁来解决此问题。所有需要获取多个进程锁的函数,都首先尝试获取这个锁,获取不到则自旋等待。以此方式来确保同一时间只有一处需要多个进程的锁,解决死锁问题。
进程表
为支持通过 pid
寻找对应进程并进行进程间通信等操作,增加了一个 <pid, task>
的映射表。此表在进程创建时增加相应的项,在进程资源回收时先行尝试释放。