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,就不得不提栈。每个任务都有自己的栈空间。栈用来存局部变量、函数调用返回地址、寄存器现场等。

我见过最典型的错误就是栈溢出。任务里定义了一个大数组,或者递归调用太深,直接把栈给撑爆了。后果是什么?程序跑飞、死机、或者出现各种诡异现象。

怎么避免?两个办法:

  1. 静态分析:估算任务的最大栈使用量,留出余量(一般多给30%-50%)
  2. 运行时检测:在栈底填充一个特殊值(比如0xDEADBEEF),定期检查这个值有没有被覆盖

我的习惯:每个任务创建时,我都会在栈底写一个“哨兵值”。然后在空闲任务里定期检查。一旦发现哨兵被改写了,立刻打印告警。这个方法帮我抓到了不下10次栈溢出bug。

2.6 小结

这一章我们聊了任务的基本概念、四种状态、状态转换、TCB结构,还有栈的管理。这些东西看着简单,但它们是整个RTOS调度的基石。你想想看,没有任务,调度器调度谁?没有状态,调度器怎么知道谁可以跑?没有TCB,调度器怎么切换?

下一章,我们会深入调度策略——优先级抢占、时间片轮转、还有那个让人头疼的优先级反转问题。到时候我会分享一个我实际项目中遇到的优先级反转案例,保证让你印象深刻。

好,今天就到这里。记住:理解任务,是理解RTOS的第一步