FreeRTOS(7)——调度器的挂起和恢复

FreeRTOS 里有两类名字很像、但含义完全不同的“挂起”:

名称API含义
挂起某个任务vTaskSuspend()让指定任务进入挂起态,直到被 vTaskResume() 恢复
挂起调度器vTaskSuspendAll()暂停任务调度,让当前任务继续运行,但不中断 ISR

本文讲的是第二种:调度器的挂起和恢复。

为什么需要挂起调度器

临界区保护可以阻止任务切换,也可以屏蔽 FreeRTOS 管理范围内的中断。它的保护能力很强,但代价是中断响应会被推迟。

如果我们只想防止任务之间互相抢资源,而这个资源不会被中断服务函数访问,就没有必要关中断。这时可以挂起调度器:

  • 当前任务可以继续执行。
  • 其他任务暂时不能切换进来。
  • 中断仍然可以正常响应。

这就是调度器挂起和临界区最大的区别。

基本 API

调度器挂起和恢复相关的 API 有两个:

API作用
vTaskSuspendAll()挂起任务调度器
xTaskResumeAll()恢复任务调度器

典型使用格式如下:

vTaskSuspendAll();
{
   /* 只需要防止任务切换的代码 */
}
xTaskResumeAll();

这段代码执行期间,当前任务不会因为调度器切换而被其他任务打断。但是外部中断仍然可以进来,ISR 也仍然会执行。

它和临界区的区别

课堂总结里有一句很关键的话:挂起任务调度器,只是让任务无法调度,但中断正常。

可以这样对比:

对比项临界区保护挂起调度器
是否影响任务切换
是否关闭中断会屏蔽 FreeRTOS 管理范围内的中断不会
ISR 是否能响应部分 ISR 会被推迟可以正常响应
适用场景任务和 ISR 都可能访问的共享资源,或严格时序代码只在任务之间共享的资源
实时性影响更大更小

所以,挂起调度器可以理解成一种“只防任务、不防中断”的轻量保护方式。

适合保护什么

挂起调度器适合保护任务之间的临界区,例如:

  1. 多个任务访问同一个普通全局缓冲区。
  2. 当前任务需要连续更新一组状态变量。
  3. 某段代码不希望被其他任务看到中间状态。
  4. 这段资源不会在任何 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_buffertx_len,这段保护就不够了。因为挂起调度器不会阻止 ISR 进入。此时应该改用临界区,或者重新设计任务和中断之间的同步方式。

内部发生了什么

调度器挂起时,FreeRTOS 内部会维护一个变量,课堂总结中提到的是 uxSchedulerSuspended

调用一次 vTaskSuspendAll()

uxSchedulerSuspended 加 1

调用一次 xTaskResumeAll()

uxSchedulerSuspended 减 1

只有当这个值重新减到 0 时,调度器才真正恢复。所以它和临界区一样,也支持嵌套。

vTaskSuspendAll();
{
   /* 第一层调度器挂起 */

   vTaskSuspendAll();
  {
       /* 第二层调度器挂起 */
  }
   xTaskResumeAll();

   /* 这里调度器仍然没有真正恢复 */
}
xTaskResumeAll();

这种设计能避免内层函数提前恢复调度器。

调度器挂起期间,任务就绪了怎么办

这是理解 vTaskSuspendAll() 的关键。

调度器挂起以后,中断仍然可以运行,系统节拍也可能继续发生。某些任务可能因为时间到、队列收到数据、信号量被释放而变成就绪态。

但是调度器还处于挂起状态,这些任务不能马上切换运行。FreeRTOS 会先把它们记录到等待处理的就绪列表中,也就是课堂总结里提到的 xPendingReadyList

xTaskResumeAll()uxSchedulerSuspended 回到 0 时,FreeRTOS 会集中处理这些延迟调度事件:

  1. xPendingReadyList 中的任务移动到对应优先级的就绪列表。
  2. 处理调度器挂起期间积累的系统节拍。
  3. 如果恢复后发现有更高优先级任务已经就绪,就触发一次任务切换。

所以,挂起调度器不是丢掉调度事件,而是把调度事件延后处理。

挂起调度器期间不要调用阻塞 API

调度器已经暂停时,不要调用会阻塞当前任务的 API,例如:

vTaskDelay(pdMS_TO_TICKS(10));
xQueueReceive(queue, &data, portMAX_DELAY);
xSemaphoreTake(semaphore, portMAX_DELAY);

原因很直接:当前任务一旦阻塞,就需要调度器切换到其他任务运行;但调度器又正被挂起,这会让系统进入很尴尬的状态。

因此,挂起调度器保护的代码也应该短小、确定、不会等待。

什么时候不用它

下面这些情况不适合只挂起调度器:

  1. 共享资源会被 ISR 访问。
  2. 代码必须连中断都不能插入。
  3. 保护区里要等待队列、信号量或延时。
  4. 保护时间很长,可能影响高优先级任务的实时性。

如果资源会被 ISR 访问,要用临界区或专门的 FromISR 同步 API。如果只是任务之间较长时间共享资源,很多时候互斥量比挂起调度器更合适,因为互斥量能表达所有权,也能让高优先级任务在等待时触发优先级继承。

一个选择顺序

实际写代码时,可以按这个顺序判断:

问题更合适的方式
资源会被 ISR 访问吗临界区或 ISR 专用同步 API
只是任务之间短时间共享吗挂起调度器可以考虑
保护时间比较长吗优先考虑互斥量、队列、事件组等同步机制
代码会阻塞吗不要使用挂起调度器,也不要放进临界区

小结

调度器挂起和恢复解决的是任务之间的资源争夺问题。

它的特点可以概括为四句话:

  1. vTaskSuspendAll() 挂起调度器,xTaskResumeAll() 恢复调度器。
  2. 它只阻止任务切换,不关闭中断。
  3. 它支持嵌套,内部通过 uxSchedulerSuspended 计数。
  4. 挂起期间就绪的任务会先进入 xPendingReadyList,恢复调度器时再统一处理。

如果说临界区是“任务和中断都别来打断我”,那么挂起调度器就是“任务先别切走,中断可以照常来”。两者的边界分清了,FreeRTOS 里的资源保护就清楚很多。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇