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,我们就可以创建任务了。创建任务时,内核会:

  1. 分配一块内存给TCB。
  2. 分配一块内存给任务栈。
  3. 初始化栈,把任务函数的入口地址、参数等压栈。
  4. 把TCB加入就绪链表。

销毁任务时,内核把TCB从链表中移除,释放内存。嗯,这里要注意:销毁任务前,一定要确保任务不再使用任何共享资源,否则会造成资源泄漏。

我曾经在一个项目中,销毁任务时忘了释放它占用的信号量,结果其他任务永远等不到那个信号量,系统死锁了。从那以后,我写了个“任务清理函数”,在销毁前自动释放所有资源。

2.6 小结

这一章我们讲了任务的基础概念。任务不是函数,它是独立运行的实体。任务状态机让内核知道每个任务在干嘛。优先级决定了谁先跑。TCB是任务的核心数据结构,所有调度都围绕它展开。

下一章,我们会深入调度器的实现,看看内核到底是怎么“抢”CPU的。到时候你会看到,TCB里的栈指针是怎么在切换时发挥作用的。

嗯,今天就到这里。动手写个简单的TCB结构体,再创建两个任务试试看。相信我,亲手写一遍,比看十遍都管用。