本系列文章代码已放在 Github 项目 Preemptive-rs 上。
这次带来新的系列,手把手教大家从零开始设计和搭建一个最简单的嵌入式实时抢占式内核,看到这个标题先别先入为主认为学习它很难或者认为没有它实际用处,实际上,我们要设计的这个内核非常精简易懂,既囊括了几章讲过的知识,可以作为一次概括性的复习,同时可以深入学习 Cotex-M3
的内核操作状态和核心寄存器的知识点。
为什么要学习可抢占式内核
不管在我们们自学时或是读书时上《操作系统》课程时,内核总是重点中的重点,在日常与 Linux 打交道的时候,我们也经常会在 Kernel 态和 Userlang 间来回切换,而到了实时嵌入式领域,我们可能会用到 ucos 或 freertos,但是我们不一定清楚这些操作系统到底做了什么事情,以及如何在我们之前使用的裸机环境之上提供内核与用户空间分离,权限控制以及多线程切换。学习内核最基本原理可以让我们在使用这些系统时更清楚每时每刻到底在发生什么事情,知道性能调优该如何下手。
什么是可抢占式内核
抢占式内核对应的是非抢占式内核,他们都负责管理全局硬件及内存资源,调度多个进程运行。他们的区别在于内核是否有权利强行夺回用户进程的执行权,这也是抢占一词的定义。
当内核是不可抢占的时候,用户进程有义务在一定执行时间后返回以让内核调度其他进程,如果某个进程执行时间过长就会阻塞整个系统,这时如果有任何外部中断到来都会进入 pending 状态,结果会带来极大的响应延迟。
可抢占式内核上的进程完全不需要关心内核及其他进程是如何运作的,它甚至可以陷入死循环,内核也有能力将足够的时间分片分给其他进程。
图中的 task4 是 Linux 中一个高优先级的进程,它可以在时间分片外抢占其他进程,在嵌入式系统中扮演这个角色的一般是驱动程序。
为什么内核可以抢占正在执行的进程
这个问题的关键是中断(interrupt),不管是嵌入式架构还是 x86 桌面架构在这一点上的原理都是一样的,CPU(或 MCU) 都会提供一系列硬件中断,这些中断的优先级总是高于一切进程(包括 Kernel 进程),那么内核便可以通过中断,比如设定定时器定时引发中断,然后在中断处理时将执行权交给 Kernel 进程便完成了抢占的过程。也有的抢占是来自于硬件中断,比如网卡硬件接收到网络数据包,这时硬件触发数据包到达中断,内核就会将执行权交给网卡驱动(运行在 Kernel)。
最终实现的效果 (画大饼时间)
例子里有两个进程,分别负责计算斐波那契额数列以及质数判定然后打印结果到串口,他们各自的工作进程都是死循环,但是内核会让他们平分计算资源。
const TASK_NUM: usize = 2;
const TASK_STACK_SIZE: usize = 100;
static mut TASK_STACKS: [[usize; TASK_STACK_SIZE]; TASK_NUM] = [[0; TASK_STACK_SIZE]; TASK_NUM];
#[no_mangle]
fn main() -> ! {
// Initialization omitted
// initialize task stack
let mut process_task1 =
unsafe { Process::new(TASK_STACKS[0].last_mut().unwrap() as *mut usize, task1) };
let mut process_task2 =
unsafe { Process::new(TASK_STACKS[1].last_mut().unwrap() as *mut usize, task2) };
writeln!(USART, "Kernel started!");
// main dispatcher loop
loop {
writeln!(USART, "nExecuting task1!");
process_task1.switch_to_task();
writeln!(USART, "nExecuting task2!");
process_task2.switch_to_task();
}
}
fn task1() -> ! {
for n in 0.. {
writeln!(USART, "fib({})={}", n, fib(n));
}
}
fn task2() -> ! {
for n in 0.. {
writeln!(USART, "is_prime({})={}", n, is_prime(n));
}
}
串口输出:
Kernel started!
Executing task1!
task1: fib(0)=1
task1: fib(1)=1
task1: fib(2)=2
Executing task2!
task2: is_prime(1)=true
task2: is_prime(2)=true
task2: is_prime(3)=true
Executing task1!
task1: fib(3)=3
task1: fib(4)=5
task1: fib(5)=8
Executing task2!
task2: is_prime(4)=false
task2: is_prime(5)=true
task2: is_prime(6)=false
...
Limitations
我们实现的内核将只可以运行在 STM32F103
单片机上,因为实现它的作用主要是为了学习,为了方便不会加入 HAL 来泛化适用范围,但是需要泛化的部分都是集中在时钟初始化和串口初始化上,内核部分在 Cortex-M3
甚至 Cortex-M4
上都是通用的。
另外,这个内核为了实现简单,暂时没有使用 MPU 内存保护单元和特权模式,这些是一个实用的系统必须具备的特性,用来保护 Kernel 内存不会被 Userland 进程污染。而且这个内核没有使用高级的线程调度算法,只是定时切换时间分片,因为这部分内容算法比较复杂,却和嵌入式内核的原理关系不大,有兴趣的读者可以阅读拓展内容。