第三章 实时操作系统(RTOS)基础:任务、调度器与核心机制
各位同学,欢迎来到第三章。从这一章开始,我们正式进入 RTOS 的世界。很多工程师刚接触 RTOS 时,会觉得它很神秘——不就是个“小系统”吗?其实不然。RTOS 的核心思想,说白了就是“在正确的时间,做正确的事”。我做了这么多年嵌入式,见过太多因为 RTOS 使用不当导致的“玄学”问题,其实根源都在于对这几个基础概念理解不透彻。
好,我们直接切入正题。这一章,我们只讲五件事:任务、调度器、上下文切换、临界区、任务状态。把这五件事搞明白,RTOS 的大门就算被你踹开了。
3.1 任务:RTOS 的“基本工作单元”
任务(Task),在有些 RTOS 里也叫线程(Thread)。它是什么?它就是一个无限循环的函数,加上它自己的栈空间和任务控制块(TCB)。
我个人习惯把任务想象成一个“独立的小人”。每个小人都有自己的“脑子”(栈,用来记临时数据)和“身份证”(TCB,用来记录任务优先级、状态等信息)。这些小人轮流在 CPU 这个“舞台”上表演。
看一个典型的任务代码结构:
void my_task(void *param)
{
// 初始化硬件或外设
init_sensor();
while (1)
{
// 任务主体:等待事件、处理数据、输出结果
wait_for_event();
process_data();
send_result();
// 主动让出 CPU,或者等待下一次触发
task_delay(100); // 单位通常是毫秒
}
}
注意看,任务函数里是一个 while(1) 死循环。为什么?因为任务一旦返回,RTOS 就会认为它“跑完了”,然后把它干掉。所以,任务函数永远不能返回。这是新手最容易犯的错误——我见过有人直接在任务里写 return,结果任务跑一次就消失了,系统直接挂掉。
3.2 调度器:谁来决定下一个谁上场?
调度器(Scheduler)是 RTOS 的“大脑”。它的职责很简单:从所有就绪的任务中,选一个出来运行。
调度器常用的策略有两种:
| 调度策略 | 核心规则 | 典型场景 |
|---|---|---|
| 优先级抢占式调度 | 高优先级任务就绪时,立即抢占低优先级任务 | 大多数实时控制系统 |
| 时间片轮转调度 | 同优先级任务轮流运行固定时间片 | 非实时或弱实时系统 |
我个人最常用的是优先级抢占式调度。为什么?因为实时性要求高的场景,你必须保证紧急任务能“插队”。比如一个电机控制任务,优先级必须高于一个串口打印任务。否则电机堵转了,CPU 还在那打印日志,你说急不急人?
但这里有个陷阱——优先级反转。低优先级任务拿着高优先级任务需要的资源,导致高优先级任务被阻塞。嗯,这个问题我们后面讲互斥量时会详细说,这里先留个印象。
3.3 上下文切换:RTOS 的“换人”功夫
上下文切换(Context Switch),就是 CPU 从执行一个任务切换到执行另一个任务的过程。这个过程是 RTOS 最核心、也最消耗时间的操作之一。
切换时发生了什么?说白了就是“保存现场,恢复现场”:
- 保存当前任务的寄存器(R0-R15、PSR、PC、LR 等)到它的栈中
- 保存当前任务的栈指针到它的 TCB 中
- 从新任务的 TCB 中取出栈指针
- 从新任务的栈中恢复寄存器
- 跳转到新任务继续执行
这个过程通常由硬件触发(PendSV 异常)或软件主动调用(任务切换函数)完成。在 Cortex-M 系列 MCU 上,硬件已经帮我们做了大部分工作,但软件部分仍然需要精心优化。
3.4 临界区:保护共享资源的“安全屋”
临界区(Critical Section),就是一段不能被中断或任务切换打断的代码。为什么需要它?因为多个任务可能同时访问同一个全局变量或外设寄存器,如果不加保护,数据就会乱掉。
我举个例子。假设两个任务都在修改一个全局变量 counter++。这个操作在 C 语言里是一行,但在汇编里可能是三条指令:读、加、写。如果任务 A 刚读完,任务 B 就插进来把值改了,然后任务 A 再写回去——那任务 B 的修改就被覆盖了。这就是经典的“读-改-写”问题。
临界区的保护方式通常有两种:
| 保护方式 | 原理 | 开销 | 适用场景 |
|---|---|---|---|
| 关中断 | 进入临界区前关全局中断,退出时开中断 | 低(几微秒) | 临界区代码极短(< 10 条指令) |
| 使用互斥量/信号量 | 任务获取锁才能进入临界区 | 中(几十微秒) | 临界区代码较长,或可能阻塞 |
我个人建议:能用互斥量就别关中断。关中断虽然快,但会影响系统的实时响应。你想想看,如果你关中断关了 100 微秒,那这 100 微秒里所有中断都进不来——包括那个紧急的电机过流保护中断。后果?可能电机就烧了。
3.5 任务状态:就绪、运行、阻塞、挂起
一个任务在它的生命周期里,会经历不同的状态。理解这些状态,是理解 RTOS 行为的基础。
常见的任务状态有四种:
- 就绪(Ready):任务已经准备好运行,只等调度器选中它。说白了就是“排队等 CPU”。
- 运行(Running):任务正在占用 CPU 执行代码。同一时刻,只有一个任务处于运行态。
- 阻塞(Blocked):任务在等待某个事件(如延时到期、信号量可用、消息队列有数据)。此时任务不参与调度。
- 挂起(Suspended):任务被显式暂停,通常由其他任务或中断调用挂起函数触发。需要显式恢复才能继续。
我画了一张状态转换图,帮你理清它们之间的关系:
你看这张图,任务状态的转换其实很清晰。我特别想强调一点:阻塞态和挂起态的区别。很多新手搞混这两个状态。
- 阻塞是任务主动等待某个条件(比如延时、信号量),条件满足后自动回到就绪态。
- 挂起是任务被被动暂停(由其他任务或中断调用挂起函数),必须由别人显式恢复。
举个例子。你写了一个串口接收任务,它调用 queue_receive() 等待数据。如果队列为空,任务就进入阻塞态。等数据来了,它自动回到就绪态。这是阻塞。而挂起呢?比如你有一个调试任务,平时不需要运行,你可以把它挂起。等需要调试时,再恢复它。
3.6 本章小结
好,我们来快速回顾一下这一章的核心内容:
- 任务是 RTOS 的基本工作单元,有自己的栈和 TCB,永远不能返回。
- 调度器负责从就绪任务中选一个运行,优先级抢占式调度是最常用的策略。
- 上下文切换是保存/恢复任务现场的过程,其耗时直接影响系统实时性。
- 临界区保护共享资源不被并发访问,原则是“短、平、快”。
- 任务状态包括就绪、运行、阻塞、挂起,理解状态转换是掌握 RTOS 行为的关键。