4、任务管理:任务创建、删除、挂起、恢复与任务控制块(TCB)详解

各位好,欢迎来到第四章。

任务管理,说白了就是操作系统怎么管好你手底下的这些“干活的人”——任务。我做了这么多年嵌入式,见过太多系统崩溃,十有八九都是任务管理没搞明白。你想想看,一个实时系统里几十个任务跑来跑去,谁先跑、谁后跑、谁该歇着、谁该干活,这要是乱了套,系统不崩才怪。

这一章,咱们就把任务管理的几个核心操作掰开揉碎了讲。任务创建、删除、挂起、恢复,还有那个藏在背后的数据结构——任务控制块(TCB)。嗯,这些都是基本功,但基本功往往最见功力。

4.1 任务控制块(TCB)——任务的“身份证”

每个任务在系统里都有一个唯一的身份标识,这就是TCB。你可以把它理解成任务的“档案袋”,里面装着这个任务的所有关键信息。

我个人习惯把TCB比作一个“任务户口本”。系统调度器不看任务代码长啥样,它只看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_size设得特别小,结果任务一跑就栈溢出,系统直接死机。排查了两天才找到原因。所以,栈大小一定要留够余量,别抠门。

核心要点:TCB是任务管理的基石。系统调度器不直接操作任务函数,它只操作TCB。你创建任务,本质就是初始化一个TCB;你删除任务,本质就是回收这个TCB。

4.2 任务创建——让任务“活”起来

任务创建,就是把一段代码和一个TCB绑定起来,告诉系统:“嘿,这个任务可以跑了。”

创建任务时,系统会做三件事:

  • 分配并初始化TCB
  • 分配任务栈空间
  • 初始化栈帧,让任务看起来像是“刚被切换出去”的状态

代码示例:

// 任务函数
void task_led(void *arg) {
    while(1) {
        GPIO_Toggle(LED_PIN);
        os_delay(500);  // 延时500ms
    }
}

// 创建任务
TCB_t *tcb_led;
tcb_led = os_task_create(task_led,      // 任务函数指针
                         NULL,          // 参数
                         1024,          // 栈大小
                         5);            // 优先级5

if (tcb_led == NULL) {
    // 创建失败,处理错误
    error_handler();
}

注意看,os_task_create返回的是TCB指针。这个指针很重要,后续所有操作——删除、挂起、恢复——都要靠它来定位任务。

我的经验:创建任务时,优先级别设太高。我曾经一上来就把所有任务优先级设成0(最高),结果高优先级任务把CPU吃死了,低优先级任务永远得不到执行。优先级设计要像分蛋糕,得有个层次。

4.3 任务删除——让任务“安息”

任务删除,就是把任务从系统里移除。系统会回收TCB和栈空间。

但这里有个坑:你不能删除一个正在运行的任务。为什么?你想想看,任务正在执行,突然把自己删了,那栈指针、寄存器状态全乱套了。系统根本不知道下一步该干啥。

// 删除任务
os_task_delete(tcb_led);

就这么一行?对,就这么一行。但背后系统做了很多事:

  • 从就绪队列中移除该任务
  • 释放栈空间
  • 将TCB标记为“空闲”

警告:千万不要在中断服务函数里调用任务删除!我曾经犯过这个错,中断里删了一个任务,结果系统调度器直接崩溃。中断服务函数应该只做最轻量级的工作,任务管理这种重活交给任务自己干。

4.4 任务挂起与恢复——让任务“休眠”和“唤醒”

挂起和恢复,是任务管理中我用得最多的两个操作。挂起不是删除,任务还在,只是暂时不参与调度。恢复就是让它重新回到就绪队列。

挂起一个任务:

os_task_suspend(tcb_led);

恢复一个任务:

os_task_resume(tcb_led);

就这么简单?嗯,接口确实简单,但用法有讲究。

我举个例子。我在做一个数据采集系统时,有个传感器任务只在特定条件下才需要运行。平时挂起它,条件满足时恢复它。这样既省CPU,又不用反复创建删除任务。创建删除是有开销的,挂起恢复几乎没有。

关键区别:

操作 TCB是否保留 栈空间是否保留 是否可恢复 开销
挂起
删除

4.5 任务状态机——任务的一生

一个任务从创建到删除,会经历多个状态。我画个简单的状态图给你看:

  • 就绪态:任务准备好了,等调度器选它
  • 运行态:任务正在执行
  • 挂起态:任务被挂起,不参与调度
  • 阻塞态:任务在等某个事件(比如延时、信号量)
  • 终止态:任务被删除,等待资源回收

任务创建后进入就绪态。调度器选中它,就变成运行态。运行中调用os_delay,就进入阻塞态。延时结束,回到就绪态。被挂起,进入挂起态。恢复后,回到就绪态。被删除,进入终止态。

这个状态流转,说白了就是任务的一生。你把这个搞清楚了,任务管理就掌握了一半。

避坑指南:我曾经在调试一个多任务系统时,发现有个任务莫名其妙不跑了。查了半天,原来是另一个任务不小心把它挂起了,而且忘了恢复。所以,挂起和恢复一定要成对使用,最好在代码里加个计数器,防止重复挂起。

4.6 实战建议——任务管理的最佳实践

最后,我总结几条实战经验,供你参考:

  1. 任务栈大小要留余量。我一般留30%的余量。栈溢出是嵌入式系统最难排查的问题之一。
  2. 优先级不要超过32级。优先级太多,调度器开销会变大。32级以内,调度效率最高。
  3. 挂起比删除好。如果任务需要反复启停,用挂起恢复,别用创建删除。创建删除有内存碎片风险。
  4. TCB指针要保护好。别让多个任务同时操作同一个TCB,加个互斥锁更安全。
  5. 调试时打印任务状态。我习惯在系统里加一个“任务监控”任务,定期打印所有任务的状态、栈使用率。这对排查问题帮助极大。

嗯,这一章就到这里。任务管理是实时操作系统的核心,你把这些操作练熟了,后面学信号量、消息队列、互斥锁就会轻松很多。下一章,咱们聊聊任务间的通信——信号量与互斥锁。