任务管理基础:任务状态机、TCB、优先级与任务创建删除

各位同学,今天我们正式进入RTOS的核心——任务管理。说实话,任务管理就是整个实时操作系统的灵魂。你想想看,一个系统里跑着十几个、几十个任务,它们怎么切换?谁先跑谁后跑?跑着跑着卡住了怎么办?这些问题的答案,都藏在我们今天要讲的内容里。

我个人习惯把任务管理比作一个公司里的员工调度。每个任务就是一个员工,有各自的岗位(优先级),有各自的状态(在干活、在等资源、被老板叫停了),还有各自的档案(TCB)。嗯,这个比喻虽然简单,但很贴切。

任务状态机:就绪/运行/阻塞/挂起

先说说任务的状态。RTOS里的任务不是一直跑着的,它会在几个状态之间跳来跳去。我见过不少新手,代码写得好好的,但任务就是跑不起来,最后发现是状态搞混了。

常见的任务状态有四种:

  • 就绪(Ready):任务啥都准备好了,就等CPU来调度它。说白了就是「我准备好了,随时可以干活」。
  • 运行(Running):任务正在占用CPU,代码正在执行。注意,同一时刻只有一个任务能处于运行状态(单核CPU下)。
  • 阻塞(Blocked):任务在等某个事件,比如等一个信号量、等消息队列里有数据、或者调用了延时函数。这时候任务不参与CPU竞争。
  • 挂起(Suspended):任务被强制暂停了,通常是由其他任务或中断调用了挂起函数。它比阻塞更「被动」一些。

我在项目中遇到过一个问题:一个传感器采集任务,明明数据已经准备好了,但任务就是不动。查了半天,发现是任务在等一个永远不会来的信号量,进入了阻塞状态。嗯,这种死等的问题,在RTOS里特别常见。

状态转换的关键点:

  • 就绪→运行:调度器选中该任务
  • 运行→就绪:时间片用完,或被更高优先级任务抢占
  • 运行→阻塞:任务主动等待资源或延时
  • 阻塞→就绪:等待的事件发生
  • 运行→挂起:被其他任务或中断挂起
  • 挂起→就绪:被恢复(解挂)

我的小技巧:调试时,我习惯在任务切换的地方加一个GPIO翻转,用示波器看波形。哪个任务跑了多久、切换频率如何,一目了然。比打印日志靠谱多了。

任务控制块(TCB)——任务的「身份证」

每个任务都有一个TCB。你可以把它理解成任务的档案袋,里面装着任务的所有信息。调度器就是靠TCB来管理任务的。

TCB里通常包含这些内容:

字段说明
任务栈指针指向任务自己的栈空间,保存局部变量和函数调用信息
任务状态当前处于就绪/运行/阻塞/挂起中的哪个状态
优先级任务的优先级数值
任务入口函数任务开始执行的函数地址
任务名称调试用的字符串标识
延时计数器用于延时功能的计时
事件等待列表任务在等待哪些事件

说白了,TCB就是RTOS内核里最重要的数据结构。没有它,调度器就是个瞎子。

我曾经在一个项目里,因为TCB的栈指针初始化错了,导致任务一运行就崩。查了两天才发现,是栈顶地址算错了8个字节。嗯,这种低级错误,犯过一次就再也不会忘了。

任务优先级——谁先跑谁后跑

优先级是RTOS里最敏感的话题。优先级设高了,低优先级任务可能永远跑不了(这叫「饥饿」)。优先级设低了,实时性要求高的任务可能错过deadline。

常见的优先级策略有两种:

  • 静态优先级:任务创建时定死,运行期间不变。简单可靠,适合确定性强的系统。
  • 动态优先级:运行期间可以调整。灵活,但容易出问题。

我个人习惯用静态优先级,除非有特别强的理由。你想想看,动态优先级调整一旦写错,任务调度就乱套了,调试起来非常痛苦。

避坑指南:我曾经在一个项目中,把两个任务的优先级设成了相同的值。结果发现,一个任务跑完了另一个才跑,完全不是预期的「轮流跑」。后来才知道,同优先级任务默认是时间片轮转,但时间片设得太短,上下文切换开销太大。嗯,优先级相同的情况下,时间片设置要格外小心。

任务创建与删除

终于到实操环节了。任务创建和删除,是RTOS里最基础的操作。

先看一个任务创建的典型代码:

// 任务栈空间
#define TASK_STACK_SIZE 512
StackType_t taskStack[TASK_STACK_SIZE];

// 任务控制块
StaticTask_t taskTCB;

// 任务函数
void MyTask(void *param) {
    while(1) {
        // 任务代码
        vTaskDelay(100);
    }
}

// 创建任务
TaskHandle_t xTaskHandle = NULL;
xTaskHandle = xTaskCreateStatic(
    MyTask,           // 任务函数
    "My Task",        // 任务名称
    TASK_STACK_SIZE,  // 栈大小
    NULL,             // 参数
    2,                // 优先级(数值越小优先级越高?注意不同RTOS定义不同)
    taskStack,        // 栈空间
    &taskTCB          // TCB
);

if(xTaskHandle != NULL) {
    // 创建成功
    vTaskStartScheduler(); // 启动调度器
}

注意看,创建任务时需要指定栈空间、优先级、任务函数。这些参数一旦确定,任务的生命周期就开始了。

删除任务更简单:

// 删除自己
vTaskDelete(NULL);

// 删除其他任务
vTaskDelete(xTaskHandle);

但这里有个坑:删除任务时,如果任务持有某些资源(比如锁、信号量),这些资源不会被自动释放。我曾经因为这个原因,导致系统里出现了一个「幽灵资源」,其他任务永远等不到它。嗯,所以删除任务前,一定要确保资源已经释放干净。

我的建议:能不删除任务就别删除。很多嵌入式系统里,任务创建后一直运行到系统关机。动态创建和删除任务,容易引入内存碎片和资源泄漏问题。除非你的系统内存非常充裕,否则老老实实用静态任务。

总结一下

今天的内容,说白了就是三件事:

  1. 任务状态机:就绪、运行、阻塞、挂起,四个状态来回切换。搞清楚每个状态的触发条件,调试时能省一半时间。
  2. TCB:任务的身份证,调度器靠它干活。初始化TCB时,栈指针和优先级最容易出错。
  3. 任务创建与删除:创建时注意栈大小和优先级,删除时注意资源释放。

下一章我们会讲任务调度算法,到时候你会看到,今天学的这些状态和TCB,是怎么被调度器玩出花样的。嗯,敬请期待。