2. 任务与调度基础:任务的概念、状态模型与TCB
大家好,欢迎来到实时系统调度这一章。今天咱们聊聊最基础的东西——任务。说实话,很多初学者一上来就盯着调度算法看,结果越看越糊涂。为什么?因为连任务本身都没搞明白。我个人习惯是,先把地基打牢,再往上盖楼。
2.1 任务到底是什么?
任务(Task),也叫线程(Thread)。在RTOS里,它就是一段独立执行的代码。你可以把它想象成一个“小工人”,有自己的活要干,有自己的工具(栈空间),有自己的进度(程序计数器)。
我遇到过不少刚入行的朋友,把任务和函数搞混。函数是被调用的,调用完了就返回。任务不一样,任务是个“永动机”——它通常是个无限循环,一直在那里跑。你想想看,一个嵌入式系统上电后,总不能跑完一个函数就关机吧?
任务 vs 函数:核心区别
- 任务:独立执行流,有自己的栈和寄存器上下文,由调度器管理
- 函数:被调用执行,共享调用者的栈,执行完返回
举个例子,一个简单的LED闪烁任务:
void led_task(void *arg) {
while(1) {
GPIO_Toggle(LED_PIN);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
看到没?这就是个典型的任务。它不返回,一直在那里“闪啊闪”。
2.2 任务的状态模型
任务不是一直都能跑的。它有几个状态,就像人一样——有时候在工作,有时候在等别人,有时候在睡觉。RTOS里常见的状态有四个:就绪、运行、阻塞、挂起。
| 状态 | 含义 | 典型场景 |
|---|---|---|
| 就绪(Ready) | 任务准备好了,就等CPU | 刚创建、时间片到了、被唤醒 |
| 运行(Running) | 正在占用CPU执行 | 调度器选中了它 |
| 阻塞(Blocked) | 在等某个事件或资源 | 等信号量、等队列、调用延时 |
| 挂起(Suspended) | 被强制暂停,不参与调度 | 调试时手动挂起、低功耗模式 |
这里有个关键点:同一时刻,只有一个任务能处于运行状态(单核CPU)。其他任务要么在就绪队列里排队,要么在阻塞队列里等着。
我的经验之谈:调试任务状态时,我最常用的方法就是打印每个任务的当前状态。有一次我发现一个任务一直处于“就绪”但就是没运行,查了半天才发现是优先级设得太低了,被其他任务“饿死”了。嗯,优先级反转也是从这里埋下的隐患。
2.3 状态转换:任务的生命周期
任务的状态不是固定的,它会随着系统运行而切换。我画个简单的转换关系给你看:
- 创建 → 就绪:任务创建完成后,自动进入就绪队列
- 就绪 → 运行:调度器选中它,分配CPU
- 运行 → 就绪:时间片用完,或者被更高优先级任务抢占
- 运行 → 阻塞:任务主动等待资源(比如等一个消息队列)
- 阻塞 → 就绪:等待的条件满足了(消息到了、延时到了)
- 运行/就绪/阻塞 → 挂起:被外部调用强制挂起
- 挂起 → 就绪:被恢复(resume)
为什么会这样设计?说白了,就是为了让CPU别闲着。一个任务在等I/O的时候,CPU可以跑去干别的活。这就是RTOS提高效率的核心思想。
注意:我曾经踩过一个坑——在中断服务函数里调用了阻塞API(比如等待信号量)。结果整个系统直接卡死。为什么?因为中断上下文里不能做任务切换。记住:中断里别阻塞,阻塞里别中断。
2.4 任务控制块(TCB)
TCB,全称Task Control Block。它是RTOS用来管理任务的数据结构。每个任务都有一个TCB,里面存着任务的所有“家底”。
你可以把TCB想象成任务的“身份证+档案袋”。调度器就是靠它来知道:这个任务现在在干嘛、优先级多少、栈用到了哪里。
一个典型的TCB结构体长这样:
typedef struct tcb {
uint32_t *stack_ptr; // 栈指针(当前栈顶位置)
uint32_t *stack_base; // 栈底地址
uint32_t stack_size; // 栈大小(字节)
uint8_t priority; // 任务优先级
uint8_t state; // 任务状态(就绪/运行/阻塞/挂起)
char name[16]; // 任务名字(方便调试)
void *msg_queue; // 消息队列指针(可选)
uint32_t delay_ticks; // 延时剩余时间(阻塞时用)
struct tcb *next; // 链表指针(用于就绪队列等)
} TCB_t;
这里面有几个关键字段:
- stack_ptr / stack_base:栈管理。每个任务有自己的栈,互不干扰。我建议栈大小至少给256字节,太小了容易溢出。
- priority:优先级。数值越小优先级越高(不同RTOS可能相反,注意看文档)。
- state:当前状态。调度器根据这个决定要不要把它放进就绪队列。
- delay_ticks:延时用的。任务调用vTaskDelay时,这个值会递减,减到0就回到就绪状态。
核心要点:TCB是RTOS的“心脏”。你写的每个任务,内核都会给它分配一个TCB。调度器遍历TCB链表,找到优先级最高且就绪的任务,然后切换过去。就这么简单,也这么关键。
2.5 任务栈:看不见的战场
说到TCB,就不得不提栈。每个任务都有自己的栈空间。栈用来存局部变量、函数调用返回地址、寄存器现场等。
我见过最典型的错误就是栈溢出。任务里定义了一个大数组,或者递归调用太深,直接把栈给撑爆了。后果是什么?程序跑飞、死机、或者出现各种诡异现象。
怎么避免?两个办法:
- 静态分析:估算任务的最大栈使用量,留出余量(一般多给30%-50%)
- 运行时检测:在栈底填充一个特殊值(比如0xDEADBEEF),定期检查这个值有没有被覆盖
我的习惯:每个任务创建时,我都会在栈底写一个“哨兵值”。然后在空闲任务里定期检查。一旦发现哨兵被改写了,立刻打印告警。这个方法帮我抓到了不下10次栈溢出bug。
2.6 小结
这一章我们聊了任务的基本概念、四种状态、状态转换、TCB结构,还有栈的管理。这些东西看着简单,但它们是整个RTOS调度的基石。你想想看,没有任务,调度器调度谁?没有状态,调度器怎么知道谁可以跑?没有TCB,调度器怎么切换?
下一章,我们会深入调度策略——优先级抢占、时间片轮转、还有那个让人头疼的优先级反转问题。到时候我会分享一个我实际项目中遇到的优先级反转案例,保证让你印象深刻。
好,今天就到这里。记住:理解任务,是理解RTOS的第一步。