2、任务管理基础:任务的概念、任务状态机、任务优先级、任务控制块设计
各位同学,欢迎来到第二章。
上一章我们聊了RTOS到底是什么,以及它凭什么能“同时”干那么多事。这一章,咱们要动真格的了——直接切入RTOS的心脏:任务管理。
我个人习惯把任务管理比作一个公司的运营。CEO(内核)不关心每个员工具体怎么敲代码,它只关心谁在干活、谁在等资源、谁被开除了。嗯,这个比喻你记着,后面会反复用到。
2.1 任务到底是什么?
说白了,任务就是一段独立的、无限循环的代码。它有自己的“灵魂”——也就是上下文(CPU寄存器、栈指针等)。
在裸机编程里,你只有一个main函数在跑。但在RTOS里,你可以有多个main函数同时“感觉上”在跑。每个这样的“main函数”,就是一个任务。
核心定义:任务是RTOS调度的最小单位。它拥有独立的栈空间和优先级,由内核统一管理。
我在项目中遇到过新手问:“任务是不是就是函数?” 嗯,有点像,但不一样。函数是被调用的,调用完就结束了。任务呢?它是个死循环,永远不返回。你想想看,如果任务返回了,那内核就不知道它该干嘛了,系统可能就崩了。
// 一个典型的任务函数
void my_task(void *arg) {
while (1) {
// 干点活
vTaskDelay(100); // 主动让出CPU
}
}
2.2 任务状态机:任务的“生老病死”
一个任务从创建到销毁,会经历几种状态。理解这个状态机,是理解RTOS调度的第一步。
RTOS里,任务通常有四种状态:就绪、运行、阻塞、挂起。
| 状态 | 含义 | 典型场景 |
|---|---|---|
| 就绪 (Ready) | 任务已经准备好,随时可以运行,但CPU正被别人占着。 | 刚创建完,或者从阻塞中醒来。 |
| 运行 (Running) | 任务正在占用CPU,代码正在被执行。 | 调度器选中了你,把CPU交给你了。 |
| 阻塞 (Blocked) | 任务在等某件事发生(比如等一个信号量、等延时结束)。 | 调用vTaskDelay(),或者等待队列消息。 |
| 挂起 (Suspended) | 任务被“雪藏”了,调度器完全忽略它,除非有人把它解挂。 | 调用vTaskSuspend()。 |
为什么会这样设计?你想想看,如果所有任务都在抢CPU,那系统就乱套了。阻塞状态让任务可以“主动休息”,把CPU让给更需要的任务。挂起状态则像是一个“暂停键”,调试的时候特别好用。
避坑指南:我曾经在一个项目里,把所有任务都设成了相同的优先级,结果一个任务里的死循环把整个系统卡死了。后来我养成了一个习惯:高优先级任务里绝对不要做长时间的计算,否则低优先级任务永远没机会跑。
2.3 任务优先级:谁先跑,谁后跑?
优先级是RTOS调度决策的核心依据。每个任务都有一个数字代表优先级,数字越小优先级越高(或者反过来,取决于具体RTOS实现)。
我个人习惯用0作为最高优先级,这样符合直觉——数字越小越“高级”。
调度规则其实很简单:
- 抢占式调度:高优先级任务一旦就绪,立刻抢走CPU。低优先级任务只能乖乖让路。
- 时间片轮转:同优先级任务之间,大家轮流用CPU,每人分一个时间片。
嗯,这里要注意:优先级不是越高越好。优先级太高,低优先级任务可能永远得不到CPU,这叫“饥饿”。我在做物联网网关时,把网络协议栈任务的优先级设得比UI任务高,结果UI卡得不行。后来调低了网络任务的优先级,用户体验才正常。
警告:不要滥用高优先级。高优先级任务应该只处理紧急、短小的事情。如果高优先级任务里做了大计算,整个系统的实时性会崩塌。
2.4 任务控制块(TCB):任务的“身份证”
内核怎么管理这么多任务?靠的就是任务控制块(Task Control Block, 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
} tcb_t;
你看,TCB里最重要的就是栈指针。任务切换时,内核把当前CPU寄存器的值保存到当前任务的栈里,然后从下一个任务的栈里恢复。这就是“上下文切换”的本质。
我记得第一次自己写TCB时,忘了保存栈指针,结果任务切换后程序直接跑飞。查了两天才发现,原来栈指针没更新。从那以后,我每次写TCB都会反复确认:栈指针,栈指针,栈指针——重要的事情说三遍。
设计要点:TCB通常放在一个链表中。内核遍历这个链表,找到最高优先级的就绪任务,然后切换过去。链表操作要快,否则会影响调度性能。
2.5 任务创建与销毁
有了TCB,我们就可以创建任务了。创建任务时,内核会:
- 分配一块内存给TCB。
- 分配一块内存给任务栈。
- 初始化栈,把任务函数的入口地址、参数等压栈。
- 把TCB加入就绪链表。
销毁任务时,内核把TCB从链表中移除,释放内存。嗯,这里要注意:销毁任务前,一定要确保任务不再使用任何共享资源,否则会造成资源泄漏。
我曾经在一个项目中,销毁任务时忘了释放它占用的信号量,结果其他任务永远等不到那个信号量,系统死锁了。从那以后,我写了个“任务清理函数”,在销毁前自动释放所有资源。
2.6 小结
这一章我们讲了任务的基础概念。任务不是函数,它是独立运行的实体。任务状态机让内核知道每个任务在干嘛。优先级决定了谁先跑。TCB是任务的核心数据结构,所有调度都围绕它展开。
下一章,我们会深入调度器的实现,看看内核到底是怎么“抢”CPU的。到时候你会看到,TCB里的栈指针是怎么在切换时发挥作用的。
嗯,今天就到这里。动手写个简单的TCB结构体,再创建两个任务试试看。相信我,亲手写一遍,比看十遍都管用。