2、内核架构基础:内核态与用户态、中断上下文与任务上下文、临界区与资源保护、可抢占内核设计
好,咱们正式开始聊内核架构。这一章是地基中的地基。你想想看,一个实时操作系统,它凭什么能“实时”?凭什么能“安全”?凭什么一个任务崩了不会把整个系统拖下水?答案就在这四大块里:特权级隔离、上下文切换、临界区保护、以及抢占机制。
我个人习惯,讲内核先从“分家”讲起。也就是内核态和用户态。
2.1 内核态与用户态:谁说了算?
说白了,这就是CPU的“两副面孔”。
内核态,也叫特权模式。在这个模式下,CPU啥都能干——访问所有内存、操作外设寄存器、开关中断、修改系统时钟。这是操作系统的“老巢”。
用户态,也叫非特权模式。任务跑在这个模式下,就像被关在笼子里。它只能访问自己的那点内存,不能碰硬件,不能关中断。一旦越界,CPU直接触发异常,把控制权交还给内核。
为什么要这么折腾?
我在项目中遇到过一件事。有个同事写了一个任务,里面不小心写了个野指针,直接往0x00000000地址写数据。如果系统没有用户态保护,这一下就能把中断向量表给毁了,整个系统当场死机。但有了用户态,CPU会立刻触发“访存异常”,内核可以优雅地杀掉这个任务,其他任务照常运行。
在ARM Cortex-M系列上,其实没有传统意义上的“用户态”。它用的是特权级(Privileged)和非特权级(Unprivileged)。RTOS内核跑在特权级,任务跑在非特权级。切换方式很简单——通过SVC(系统服务调用)指令,任务主动“举手”请求内核帮忙。
// 伪代码:用户态任务请求内核服务
void user_task(void) {
// 用户态代码,不能直接操作硬件
// 想打印日志?必须通过系统调用
__asm volatile("SVC #0"); // 触发SVC异常,进入内核态
// 回来后继续执行用户态代码
}
2.2 中断上下文与任务上下文:谁在干活?
好,现在咱们聊聊“上下文”。
上下文,说白了就是CPU当前的状态——寄存器、栈指针、程序计数器这些东西。操作系统要切换任务,就得先把当前任务的上下文保存起来,再恢复下一个任务的上下文。
但这里有个关键区别:中断上下文和任务上下文是完全不同的两码事。
- 任务上下文:由任务主动让出CPU时保存。比如任务调用了
delay()或者等待信号量。这是“自愿切换”。 - 中断上下文:由硬件中断强制触发。比如定时器到了、串口收到数据了。这是“被动切换”。
你想想看,中断上下文里能调用delay()吗?绝对不能!因为中断上下文没有自己的“任务控制块”,它用的是中断栈。你在中断里睡一觉,醒来发现栈已经乱套了。
vTaskDelay(),结果系统直接死锁。
那怎么区分当前是不是中断上下文?大多数RTOS都提供了查询接口:
// FreeRTOS 示例
if (xPortIsInsideInterrupt()) {
// 当前在中断上下文,只能使用中断安全的API
xQueueSendFromISR(xQueue, &data, &higherPriorityTaskWoken);
} else {
// 当前在任务上下文,可以使用普通API
xQueueSend(xQueue, &data, portMAX_DELAY);
}
我个人习惯,在写驱动时,所有对外接口都做一次上下文检查。如果调用者传错了,直接断言报错。这样能提前暴露问题,而不是等到系统崩溃了才去查。
2.3 临界区与资源保护:别抢我的数据
临界区,就是一段“不能被打断”的代码。比如你在更新一个全局变量,刚读到一半,中断来了,也去读这个变量——那读到的数据就是脏的。
保护临界区,常用的方法有三种:
| 方法 | 原理 | 适用场景 | 缺点 |
|---|---|---|---|
| 关中断 | 进入临界区前关掉所有中断 | 极短的操作(如修改一个变量) | 影响实时性,关中断时间不能太长 |
| 信号量/互斥量 | 任务间通过信号量互斥访问 | 任务与任务之间共享资源 | 不能用于中断上下文 |
| 暂停调度器 | 禁止任务切换,但中断仍然响应 | 需要保护一段较长的代码 | 中断仍可能访问共享资源 |
嗯,这里要注意:关中断是最粗暴但最有效的方法。但关中断的时间必须极短,一般不超过几十微秒。否则你的系统实时性就没了——想想看,你关了中断,定时器中断进不来,系统时钟就停了。
信号量呢?它适合保护“任务与任务”之间的共享资源。但记住:信号量不能在中断中使用。中断里只能用“中断安全的信号量”版本,比如xSemaphoreGiveFromISR()。
// 正确的做法:任务中等待信号量
void task_A(void *param) {
while(1) {
// 等待信号量,会阻塞
xSemaphoreTake(xSemaphore, portMAX_DELAY);
// 访问共享资源
process_shared_data();
xSemaphoreGive(xSemaphore);
}
}
// 中断中只能Give,不能Take
void UART_IRQHandler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
xSemaphoreTake(),结果系统直接卡死。原因是中断里不能阻塞,而xSemaphoreTake()如果信号量不可用,会让当前任务进入阻塞态——但中断里没有“当前任务”这个概念。记住:中断里只能Give,不能Take。
2.4 可抢占内核设计:谁优先级高谁先跑
可抢占内核,是实时操作系统的灵魂。
什么叫可抢占?就是当一个高优先级任务就绪时,它可以立即打断正在运行的低优先级任务,抢走CPU的使用权。
你想想看,如果没有抢占,一个低优先级任务在循环里算圆周率,高优先级任务来了也只能等着——那还叫什么实时系统?
可抢占内核的实现,核心在于每次系统节拍中断(Tick Interrupt)时,检查是否有更高优先级的任务就绪。如果有,就触发一次任务切换。
// 伪代码:系统节拍中断处理
void SysTick_Handler(void) {
// 更新系统时钟
xTaskIncrementTick();
// 检查是否需要切换任务
if (task_switch_needed()) {
// 保存当前任务上下文
save_context(current_task);
// 切换到最高优先级就绪任务
current_task = get_highest_priority_ready_task();
// 恢复新任务上下文
restore_context(current_task);
}
}
但这里有个陷阱:中断里能不能做任务切换? 能,但必须小心。因为中断可能嵌套,如果在中断里切换了任务,返回时应该返回到新任务,而不是被中断的任务。
ARM Cortex-M3/4 提供了一个硬件特性——咬尾中断(Tail-Chaining)和晚到异常(Late-Arriving)。这些硬件机制能自动处理中断嵌套时的任务切换,大大简化了软件设计。
最后,我想说一句:可抢占内核不是万能的。如果两个任务优先级相同,它们会时间片轮转。但如果一个任务一直占着CPU不放,其他同优先级任务就永远没机会跑。所以,任务里要主动让出CPU,比如调用delay()或者等待事件。
while(1);,结果所有同优先级任务全部饿死。记住:实时系统里,每个任务都要有“喘息”的机会。
好,这一章就到这里。下一章咱们聊聊任务管理——怎么创建任务、怎么调度、怎么让任务之间通信。这些东西,才是真正把内核用起来的开始。