Skip to content

1.5 任务与任务生命周期 ​

这节要搞懂:什么是任务,它有哪几种状态。 需要基础:1.4 ⭐ 本章重点,值得多看两遍

一、什么是任务 ​

任务(Task)是 FreeRTOS 中最小的调度单位。每个任务都是一段独立运行的程序代码,拥有自己的栈空间和优先级,由内核决定何时让它运行。

打个比方:一个任务就像餐厅里的一名员工 —— 有自己的岗位(代码)、自己的工位(栈)、自己的重要程度(优先级)。经理(调度器)决定什么时候让谁干活。

二、任务在代码里长什么样 ​

一个任务 = 一个自带 while(1) 的函数:

c
void task_led(void *pvParameters)
{
    while (1) {                              // 注意:永远不能结束
        led_on();                            // 点亮 LED
        vTaskDelay(pdMS_TO_TICKS(500));      // 睡 500 毫秒

        led_off();                           // 熄灭 LED
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

你可以写好几个这样的函数,它们看起来都在"同时"运行。

任务函数不能 return,也不能自然结束例如break。 它必须是个死循环。

真要结束就调用 vTaskDelete(NULL) 主动删掉自己。任务函数跑到结尾"掉出去",程序会直接崩溃。 :::

三、任务的三个组成部分 ​

组成是什么说明
函数体你写的那段代码必须是死循环
栈(Stack)一块专属的内存存局部变量和切换时的临时数据
TCB(任务控制块)内核用的一个结构体每创建一个任务,内核就会分配一个 TCB 来记录其全部信息:它的名字、优先级、当前状态

这三样都是创建任务时自动分配的,你只需要告诉内核"栈要多大"。

四、创建一个任务 ​

c
xTaskCreate(
    task_led,       // 1. 任务函数
    "led",          // 2. 任务名字,调试时用来识别
    2048,           // 3. 栈大小(字节)
    NULL,           // 4. 传给任务的参数,没有就填 NULL
    5,              // 5. 优先级,数字越大越优先
    NULL            // 6. 任务句柄,不需要就填 NULL
);

六个参数,新手只要关心第 3 个和第 5 个:

参数怎么填
栈大小先填 2048,不够再加。太小会崩,太大浪费内存
优先级一般任务填 5,需要快速响应的填大一点(比如按键填 6)

一个跨平台的坑

在 ESP32-S3(ESP-IDF) 里,栈大小的单位是字节,填 2048 就是 2048 字节。

在原版 FreeRTOS 里单位是"字"(4 字节),填 2048 相当于 8192 字节。

本课程全部按字节算,换平台时记得查一下。

五、什么是任务生命周期 ​

任务生命周期指的是任务从被创建到被删除的整个过程中,所经历的各种运行状态及其相互转换关系。

FreeRTOS 的任务有四种状态:

xTaskCreate() 创建任务Ready 就绪排队等 CPURunning 运行中正在占用 CPUBlocked 阻塞自己在等,会自动醒Suspended 挂起被按停,不会自己醒
<!-- Running → Blocked --> <path d="M255,211 L190,211" marker-end="url(#ar)"/> <!-- Blocked → Ready --> <path d="M105,185 L105,88 L255,88" marker-end="url(#ar)"/> <!-- Running → Suspended --> <path d="M445,211 L510,211" marker-end="url(#ar)"/> <!-- Suspended → Ready --> <path d="M595,185 L595,88 L445,88" marker-end="url(#ar)"/> 
调度器选中它被更高优先级抢走vTaskDelay等队列/信号量⏰ 时间到 / 等到了vTaskSuspendvTaskResume 任何时刻,一颗 CPU 核心上只有 一个 任务处于 Running 实线框 = 任务自己能出来  虚线框 = 只能靠别人叫醒

顺着箭头读:任务创建出来先进 Ready(排队);被调度器选中就变 Running(干活);干活时如果调了 vTaskDelay 或去等队列,就进 Blocked(让出 CPU 去等),条件满足后自动回到 Ready 重新排队。

Suspended 是画虚线的那个 —— 只能被别人 vTaskSuspend 按停,也只能被别人 vTaskResume 叫醒,它自己出不来。

六、用排队理解四种状态 ​

状态相当于占着 CPU 吗会自己恢复吗
Running 运行中正在里面✅ 是—
Ready 就绪门口排队,随时能进❌ 否✅ 轮到就进
Blocked 阻塞去旁边坐着等叫号❌ 否✅ 条件满足自动醒
Suspended 挂起被人拉走了❌ 否❌ 不叫不回来

七、阻塞和挂起的区别 ​

新手最容易混,记住一句话:

  • 阻塞:任务自己说"我等一下",时间到了或条件满足了会自动醒
  • 挂起:别人把它按停了,只能靠别人叫醒(调用 vTaskResume)

99% 的情况你用的都是阻塞:

c
vTaskDelay(pdMS_TO_TICKS(500));   // 我睡 500ms,到点自己醒 → 阻塞

八、为什么 vTaskDelay 这么重要 ​

c
// ❌ 错误:傻等,CPU 被你一个人占着,别的任务都别想跑
for (int i = 0; i < 1000000; i++) { }

// ✅ 正确:睡过去,CPU 让给别人
vTaskDelay(pdMS_TO_TICKS(500));

记住这条铁律

每个任务的循环里,必须有一个会"睡"的语句(vTaskDelay,或者等待队列、等待信号量)。

否则这个任务会一直霸占 CPU,其他任务全部饿死,系统还会报错: Task watchdog got triggered

本节要点 ​

  • 任务 = FreeRTOS 最小的调度单位,由代码 + 栈 + TCB 组成
  • 任务函数是死循环,不能返回
  • 创建时先关心两个参数:栈填 2048、优先级填 5
  • 四种状态:运行 / 就绪 / 阻塞 / 挂起
  • 阻塞会自己醒,挂起不会
  • 每个任务循环里必须有会"睡"的语句

动手试试 ​

  1. 创建两个任务,一个每 500ms 打印 "A",一个每 1000ms 打印 "B",观察输出怎么交错。
  2. 把其中一个任务的 vTaskDelay 删掉,看看会发生什么(会报错,就是要你看到)。
  3. 把栈大小从 2048 改成 256,看看程序还能不能跑。