FreeRTOS 里有两类名字很像、但含义完全不同的“挂起”:
| 名称 | API | 含义 |
|---|---|---|
| 挂起某个任务 | vTaskSuspend() | 让指定任务进入挂起态,直到被 vTaskResume() 恢复 |
| 挂起调度器 | vTaskSuspendAll() | 暂停任务调度,让当前任务继续运行,但不中断 ISR |
本文讲的是第二种:调度器的挂起和恢复。
为什么需要挂起调度器
临界区保护可以阻止任务切换,也可以屏蔽 FreeRTOS 管理范围内的中断。它的保护能力很强,但代价是中断响应会被推迟。
如果我们只想防止任务之间互相抢资源,而这个资源不会被中断服务函数访问,就没有必要关中断。这时可以挂起调度器:
- 当前任务可以继续执行。
- 其他任务暂时不能切换进来。
- 中断仍然可以正常响应。
这就是调度器挂起和临界区最大的区别。
基本 API
调度器挂起和恢复相关的 API 有两个:
| API | 作用 |
|---|---|
vTaskSuspendAll() | 挂起任务调度器 |
xTaskResumeAll() | 恢复任务调度器 |
典型使用格式如下:
vTaskSuspendAll();
{
/* 只需要防止任务切换的代码 */
}
xTaskResumeAll();
这段代码执行期间,当前任务不会因为调度器切换而被其他任务打断。但是外部中断仍然可以进来,ISR 也仍然会执行。
它和临界区的区别
课堂总结里有一句很关键的话:挂起任务调度器,只是让任务无法调度,但中断正常。
可以这样对比:
| 对比项 | 临界区保护 | 挂起调度器 |
|---|---|---|
| 是否影响任务切换 | 会 | 会 |
| 是否关闭中断 | 会屏蔽 FreeRTOS 管理范围内的中断 | 不会 |
| ISR 是否能响应 | 部分 ISR 会被推迟 | 可以正常响应 |
| 适用场景 | 任务和 ISR 都可能访问的共享资源,或严格时序代码 | 只在任务之间共享的资源 |
| 实时性影响 | 更大 | 更小 |
所以,挂起调度器可以理解成一种“只防任务、不防中断”的轻量保护方式。
适合保护什么
挂起调度器适合保护任务之间的临界区,例如:
- 多个任务访问同一个普通全局缓冲区。
- 当前任务需要连续更新一组状态变量。
- 某段代码不希望被其他任务看到中间状态。
- 这段资源不会在任何 ISR 中被访问。
例如:
static uint8_t tx_buffer[128];
static size_t tx_len;
void update_tx_buffer(const uint8_t *data, size_t len)
{
vTaskSuspendAll();
{
if (len > sizeof(tx_buffer))
{
len = sizeof(tx_buffer);
}
memcpy(tx_buffer, data, len);
tx_len = len;
}
xTaskResumeAll();
}
这段代码的目标是让其他任务看不到 tx_buffer 已经更新、但 tx_len 还没更新的中间状态。
但如果某个串口发送完成中断也会访问 tx_buffer 或 tx_len,这段保护就不够了。因为挂起调度器不会阻止 ISR 进入。此时应该改用临界区,或者重新设计任务和中断之间的同步方式。
内部发生了什么
调度器挂起时,FreeRTOS 内部会维护一个变量,课堂总结中提到的是 uxSchedulerSuspended。
调用一次 vTaskSuspendAll():
uxSchedulerSuspended 加 1
调用一次 xTaskResumeAll():
uxSchedulerSuspended 减 1
只有当这个值重新减到 0 时,调度器才真正恢复。所以它和临界区一样,也支持嵌套。
vTaskSuspendAll();
{
/* 第一层调度器挂起 */
vTaskSuspendAll();
{
/* 第二层调度器挂起 */
}
xTaskResumeAll();
/* 这里调度器仍然没有真正恢复 */
}
xTaskResumeAll();
这种设计能避免内层函数提前恢复调度器。
调度器挂起期间,任务就绪了怎么办
这是理解 vTaskSuspendAll() 的关键。
调度器挂起以后,中断仍然可以运行,系统节拍也可能继续发生。某些任务可能因为时间到、队列收到数据、信号量被释放而变成就绪态。
但是调度器还处于挂起状态,这些任务不能马上切换运行。FreeRTOS 会先把它们记录到等待处理的就绪列表中,也就是课堂总结里提到的 xPendingReadyList。
当 xTaskResumeAll() 让 uxSchedulerSuspended 回到 0 时,FreeRTOS 会集中处理这些延迟调度事件:
- 将
xPendingReadyList中的任务移动到对应优先级的就绪列表。 - 处理调度器挂起期间积累的系统节拍。
- 如果恢复后发现有更高优先级任务已经就绪,就触发一次任务切换。
所以,挂起调度器不是丢掉调度事件,而是把调度事件延后处理。
挂起调度器期间不要调用阻塞 API
调度器已经暂停时,不要调用会阻塞当前任务的 API,例如:
vTaskDelay(pdMS_TO_TICKS(10));
xQueueReceive(queue, &data, portMAX_DELAY);
xSemaphoreTake(semaphore, portMAX_DELAY);
原因很直接:当前任务一旦阻塞,就需要调度器切换到其他任务运行;但调度器又正被挂起,这会让系统进入很尴尬的状态。
因此,挂起调度器保护的代码也应该短小、确定、不会等待。
什么时候不用它
下面这些情况不适合只挂起调度器:
- 共享资源会被 ISR 访问。
- 代码必须连中断都不能插入。
- 保护区里要等待队列、信号量或延时。
- 保护时间很长,可能影响高优先级任务的实时性。
如果资源会被 ISR 访问,要用临界区或专门的 FromISR 同步 API。如果只是任务之间较长时间共享资源,很多时候互斥量比挂起调度器更合适,因为互斥量能表达所有权,也能让高优先级任务在等待时触发优先级继承。
一个选择顺序
实际写代码时,可以按这个顺序判断:
| 问题 | 更合适的方式 |
|---|---|
| 资源会被 ISR 访问吗 | 临界区或 ISR 专用同步 API |
| 只是任务之间短时间共享吗 | 挂起调度器可以考虑 |
| 保护时间比较长吗 | 优先考虑互斥量、队列、事件组等同步机制 |
| 代码会阻塞吗 | 不要使用挂起调度器,也不要放进临界区 |
小结
调度器挂起和恢复解决的是任务之间的资源争夺问题。
它的特点可以概括为四句话:
vTaskSuspendAll()挂起调度器,xTaskResumeAll()恢复调度器。- 它只阻止任务切换,不关闭中断。
- 它支持嵌套,内部通过
uxSchedulerSuspended计数。 - 挂起期间就绪的任务会先进入
xPendingReadyList,恢复调度器时再统一处理。
如果说临界区是“任务和中断都别来打断我”,那么挂起调度器就是“任务先别切走,中断可以照常来”。两者的边界分清了,FreeRTOS 里的资源保护就清楚很多。









