文章目录
引入
在前面的内容中,我们已经讲过:
当多个任务同时访问同一份共享资源时,如果不加控制,就可能出现数据错乱的问题,这就是线程安全问题。
进一步分析可以发现:
出现线程安全问题的根本原因在于,任务对共享资源的访问过程,并不是一个原子操作。
任务在执行操作的过程,随时都可能被切换打断(包括任务与中断),最终导致了线程安全问题。
所以:
若想解决线程安全问题,根本思路就是让原本对共享资源的非原子性操作,变为一个原子操作。
那么在FreeRTOS当中,如何让一个操作变为原子操作呢?
很简单:
只需要保证任务在执行共享资源访问操作时,不被打断就可以了。
在FreeRTOS当中,已经提供了这样的机制,这就是临界区(Critical Section):
- 通过某种机制,使一段代码在执行期间不被打断干扰的行为,我们称之为:进入临界区
- 而被保护起来、要求连续执行、不允许被打断的那段代码,就称为临界区代码。
- 临界区代码执行完毕后,还需要还原系统行为,回归正常调度,这就是:退出临界区。
在具体学习临界区的实现方式之前,在这里我们就要先搞清楚一点:
FreeRTOS的所谓临界区,其本质的原理都是:保证任务在临界区内不会被打断!
换句话说:
只要一段代码进入临界区,就必须完整执行完毕,中途不能被打断。
临界区正是基于这样的原理,来实现对共享资源的原子性操作。
暂停调度器
在 FreeRTOS 中,**(接近)**实现临界区的第一种方式是:暂停调度器。
相关的一对API如下:
c
void vTaskSuspendAll( void ); // 暂停任务调度器
BaseType_t xTaskResumeAll( void ); // 恢复任务调度器
这一组 API 的核心思想非常简单:
既然任务切换是调度器触发的,那我直接把调度器停掉!
通过暂停调度器,阻止任务切换,从而保证当前任务能够连续执行。
这一对函数是FreeRTOS 内核自带的调度控制函数,不受任何宏配置开关控制,始终是存在的。
澄清一个误区
在继续深入之前,需要先澄清一个非常容易产生误解的点。
这两个函数的名字,很容易让人误以为它们类似于:
- 挂起所有任务
- 恢复挂起所有任务
难道说,执行这个函数就是把所有其他任务"挂起来"?通过这种方式来实现禁止切换?
当然不是,如果是这样,未免太麻烦了。
实际上:
这两个函数,它们操作的对象不是"任务",而是"调度器"。
也就是说:
- vTaskSuspendAll:暂停调度器,暂停系统中一切任务调度相关的行为。
- xTaskResumeAll:恢复调度器,恢复调度器的调度行为。
进一步来说:
这两个 API 并不会直接改变任何任务的状态。
任务原本是什么状态:
- 就绪态
- 运行态
- 阻塞态
- 挂起态
在调用这两个函数前后,都不会发生改变。
它们真正做的事情只有一个:让"任务切换机制"暂时失效/恢复。
可以用一句更本质的话来理解:
vTaskSuspend() 是冻结某个任务,暂停某个任务。
vTaskSuspendAll() 则是"冻结调度器","暂停调度"。
vTaskSuspendAll()源码
当然,如果只是讲上述内容,告诉你这样一句结论:vTaskSuspendAll() 是"冻结调度器","暂停调度"。
还是太抽象了,你也根本不理解到底发生了什么事情。
若想真正理解它的行为,最好、最有效的办法还是直接看源码。
这个函数做了什么事情呢?
其实非常简单:
它的核心作用只有一行代码:

说白了,这个函数只做了一件事情:
把uxSchedulerSuspended这个调度器暂停标志,加1,从原本的默认值0,改为了1。
从而表示:暂停调度器。
接下来需要重点关注的是:当 uxSchedulerSuspended 不为 0 时,内核的行为会发生哪些变化?
下面逐一进行分析。
调度器暂停对任务切换的直接影响
调度器暂停后,最直接的影响就是:
任务上下文切换不会再发生。
其具体表现为:
当前正在运行的任务,一旦获得 CPU,就会持续占用 CPU,不会被调度器切换出去。
即使在此期间,有更高优先级任务就绪,也不会立即切换任务。
当然时间片轮转机制就更不会生效了。
从内核实现角度来看:
FreeRTOS 中,真正完成"选择下一个运行任务"的核心函数是:
c
vTaskSwitchContext()
该函数的职责是:
从就绪列表中选择优先级最高的任务,并更新当前运行任务指针。
而一旦任务调度器被暂停,那么任务上下文切换就被禁止了。此时该函数的处理源码如下所示:
一句话总结:
调度器暂停,本质上就是"禁止 vTaskSwitchContext() 的执行",从而彻底阻断任务上下文切换。
调度器暂停标志对全局Tick计数的影响
在前面的学习中,我们已经讲过全局系统Tick计数,也就是全局变量xTickCount。
每当一个新Tick到来后,内核就会调用函数xTaskIncrementTick(),完成对全局计数的累加,并且处理一些事项。
在正常情况下,在任务调度器没有被暂停的情况下,它主要完成以下事项:
- 全局 Tick 计数加 1
- 检查延时阻塞列表
- 将已到期的任务移出阻塞态,并移入就绪列表
- 根据情况决定是否触发任务切换
此函数的部分源码如下图所示:

但这一切行为的前提是:
uxSchedulerSuspended == 0,即任务调度器处于正常工作状态。
一旦调度器被暂停(uxSchedulerSuspended > 0),xTaskIncrementTick() 的行为就会发生变化。
此时,该函数不再执行完整的 Tick 处理流程,而是只保留最基本的时间推进功能。
源码如下:

具体来说,此时函数的行为是:
- 全局 Tick 计数仍然会加 1,系统时间继续向前推进
- 不再检查延时阻塞列表,即不会判断任务是否到期
- 不会将到期任务移入就绪列表,任务仍然停留在阻塞态
- 不会触发任务调度,即不会发生任务切换
也就是说,在调度器暂停期间,Tick 仍然在不断产生,时间还在继续推进,但由 Tick 引发的阻塞态任务状态切换不再进行了。
为了记录这段时间内累计的 Tick 数,内核使用了变量 xPendedTicks(暂停时期Tick计数)。
在调度器暂停期间,每当一个 Tick 到来时,不再执行完整的处理流程,而是简单地执行:
c
xPendedTicks++
可以这样理解这一过程:
- 正常情况下,Tick 到来后会立即推动任务状态变化,并可能引发调度;
- 而在调度器暂停期间,Tick 只被"记录下来",并不会立即产生任何调度效果。
因此,会出现一种现象:
某些延时阻塞任务在时间上已经"到期",但状态上仍然停留在阻塞态。
这是因为:
时间在推进,但时间对任务状态产生的影响被延迟了,调度器被暂停了。
但是需要注意:
这些关于延时阻塞任务的处理不会丢失,而是会在后续调用 xTaskResumeAll() 时统一处理。
换句话说,调度器暂停期间,并不是"不处理",而是"暂不处理"。
通过这一条执行路径,我们还可以得到一个更本质的结论:
调度器暂停后,所有与任务调度相关的行为,并不会消失,而是被整体延后,统一在恢复调度器时再处理。
做一个类比就是:
公司周末不上班,并不是把需要做的工作都"消灭"了。
而是将事务暂时积压,等到下一个工作日再统一处理。
所以:
若一个处于延时阻塞状态的任务,一个处于延时阻塞列表中的任务
在任务调度暂停期间到达"苏醒时间",那么它实际不会发生任何变化,仍然会继续待在延时阻塞列表中,继续处在延时阻塞状态。
直到任务调度器恢复,它才会回到就绪态。
调度器暂停标志对事件阻塞的影响(了解)
在前面的分析中,我们已经从**"Tick驱动路径"**出发,说明了:
当调度器被暂停时,Tick 仍然正常推进,但由 Tick 引发的延时阻塞任务状态变化会被延迟处理。
但是在FreeRTOS当中,处于阻塞状态的任务,并不只有延时阻塞。
还有一种很常见的阻塞方式:
事件阻塞。
由于事件阻塞涉及到后面的知识点,所以这里只简单提一下,尽量不超出大家目前的知识范畴。
所谓事件阻塞,可以简单理解为:任务在等待某个"外部条件"成立或者"外部事件"发生。
例如,任务 A 需要等待其他任务发送通知。
如果这个事件始终没有发生,那么任务 A 会一直处于阻塞态;只有当事件真正发生时,任务 A 才会解除阻塞。
这里有一个关键区别:
延时阻塞依赖时间推进,而事件阻塞依赖事件本身是否发生。
处于事件阻塞的任务,会被放入对应的事件列表当中(具体是什么事件列表,后面详谈)。
那么问题来了:
如果在任务调度器被暂停的期间,事件发生了,会如何进行处理呢?
事件一旦发生,内核会调用函数 xTaskRemoveFromEventList() 来进行任务状态的处理。
常规的处理就是直接唤醒任务,将任务移入就绪列表,转变成就绪态:

但如果此时调度器处于暂停状态,情况就不同了:

如果调度器暂停期间,事件发生了,任务就不会回到就绪态了,而是被移入挂起就绪列表。
也就是我们之前见过的一个列表:

挂起就绪列表,只是一个临时列表,它用于存储那些:
"在调度器暂停期间,事件发生,满足执行条件,进入就绪态的任务"。
为什么这么设计呢?
很简单:
在任务调度器暂停期间,如果让任务也能从阻塞态回到就绪态,即便不调度它上CPU,但这种操作还是破坏了调度系统结构本身。
这就好比:
公司的财务周末没上班,休息了,我偷偷往公司的账上记一笔。
显然是不合理。
从这个角度出发:
- 挂起就绪列表就是一个"临时账本",先记一笔
- 等到 xTaskResumeAll() 函数调用,再把"临时账本"上的数据抄回就绪列表这个"真正的账本"(这个事情由财务亲自干)。
也就是说:
- 挂起就绪列表,相当于一个"临时记录区"
- 等调度器恢复(调用
xTaskResumeAll())后 - 再把这些任务统一转移到真正的就绪列表中
总之,这条线还是告诉我们一个任务调度器暂停的本质作用:
调度器暂停后,所有与任务调度相关的行为,并不会消失,而是被整体延后,统一在恢复调度器时再处理。
只不过:
- 延时阻塞的任务如果到期,内核会先记录 临时挂起Tick计数,恢复调度器后再统一处理。
- 事件阻塞的任务如果满足条件,内核会先将它们放入挂起就绪列表,恢复调度器后再统一处理。
任务调度暂停对挂起任务的影响
如果一个任务处于挂起状态,那么任务调度器暂停对它有影响吗?
这个简单,直接查看挂起相关函数的源码即可:

挂起函数中,没有找到任何有关调度器暂停的判断。
那么恢复挂起呢?

恢复挂起函数中,也没有找到任何有关调度器暂停的判断。
所以,任务调度器暂停对挂起状态实际上是不起作用的:
- 即便任务调度器暂停,仍然可以将一个任务挂起,让这个任务进入挂起列表。
- 即便任务调度器暂停,仍然可以将一个挂起的任务,恢复挂起,让这个任务回到就绪列表。
但是注意,即便一个挂起任务恢复挂起,回到就绪态,但在任务调度器暂停期间,也不会上CPU执行。
vTaskSuspendAll()执行后的系统行为
调度器暂停后,所有与任务调度相关的行为,并不会消失,而是被整体延后,统一在恢复调度器时再处理。
也就是说,当执行 vTaskSuspendAll() 之后,系统进入一种**"半冻结"状态**:
- 当前正在运行的任务会继续执行
- 不会再发生任务切换(上下文切换被禁止)
- 即使有更高优先级任务就绪,也不会抢占当前任务。
- 中断仍然可以正常响应,只是暂停调度器,而没有禁用中断。
- 但由 Tick 驱动的任务状态变化(如延时到期)不会立即生效,延时阻塞状态的任务不会回到就绪态。
- 事件仍然可以正常发生,但被唤醒的任务不会立即被立刻加入就绪列表,而是进入挂起就绪列表。
- 挂起状态相关的事项仍然可以正常处理,但并没有实际意义,也不推荐在任务调度器暂停期间做挂起相关操作。
vTaskSuspendAll()函数总结
通过源码可以看出,vTaskSuspendAll() 的本质可以归纳为以下几点:
- vTaskSuspendAll() 并不是"停止系统运行",而只是"暂停任务调度"。
- 系统 Tick 仍然正常产生,全局时间持续推进。
- 但所有与任务调度相关的行为被暂时冻结。
- 调度器暂停后,所有"与调度相关的行为"都会被延迟处理,而不会丢失。
- 延时阻塞任务到期 → 不立即转入就绪态,而是通过
xPendedTicks记录 - 事件触发 → 不直接进入就绪列表,而是进入挂起就绪列表
- 任务切换 → 不立即发生
- 本质上:不是"不处理",而是"暂不处理,后续统一处理"
- 延时阻塞任务到期 → 不立即转入就绪态,而是通过
- 内核通过"临时记录机制"保证系统状态的正确性。
xPendedTicks:记录暂停期间累计的 Tick- 挂起就绪列表:记录本应进入就绪态但被延迟处理的任务
- 即使调度器暂停,系统状态也不会丢失或紊乱。
- xTaskResumeAll() 是整个机制的"收口点" ,恢复调度器时会统一处理:
- 累计的
xPendedTicks - 延时到期任务
- 挂起就绪列表中的任务
- 并在必要时触发一次任务调度
- 可以理解为:暂停期间积压的所有调度行为,在这里一次性补执行。
- 累计的
一句话总结:
vTaskSuspendAll() 的本质,不是停止系统,而是将任务调度行为整体延后,并在恢复时统一处理。
xTaskResumeAll()函数
前面我们讲了:
vTaskSuspendAll() 的作用,本质上就是把调度器暂停标志 uxSchedulerSuspended 加 1,使系统进入"暂停调度"的状态。
那么很自然就会有一个问题:
调度器暂停之后,靠谁来恢复?
答案就是:
xTaskResumeAll()
这个函数的作用可以概括为一句话:
恢复调度器,并把暂停期间积压的那些"本该处理但暂时没处理"的调度相关事务,统一补处理掉。
也就是说:
vTaskSuspendAll()负责"先暂停"xTaskResumeAll()负责"再恢复,并收尾"
这两个函数通常是成对使用的。
函数的行为
xTaskResumeAll() 做了什么?
这个函数整体上主要完成 4 件事情:
- 将调度器暂停计数减 1
- 如果计数已经恢复为 0,说明调度器真正恢复
- 统一处理暂停期间积压的任务状态变化
- 根据情况决定是否触发一次任务切换
它的核心逻辑,其实就是围绕下面这句展开的:

也就是说:
vTaskSuspendAll() 是对调度暂停标志加 1,xTaskResumeAll() 就是对调度暂停标志减 1。
嵌套计数器
很容易就可以发现:
只有当调度暂停标志最终减回到 0 时,才表示:
调度器真正从"暂停状态"恢复到了"可调度状态"
所以,这里也能看出一个细节:
调度器暂停不是简单的布尔标志,而是一个可嵌套计数器。
例如:
c
vTaskSuspendAll();
vTaskSuspendAll();
此时 uxSchedulerSuspended = 2
必须调用两次:
c
xTaskResumeAll();
xTaskResumeAll();
才能真正恢复调度器。
暂停调度器这一对API,必须成对出现使用。而且暂停几次,就必须恢复几次。
函数核心功能
具体来说,恢复调度器,会完整以下操作。
第一,处理挂起就绪列表中的任务,统一将它们转移到就绪列表。
让它们真正进入就绪态。
源码部分如下所示:

除此之外,它还会补处理暂停期间积压的 Tick。
将 xPendedTicks 记录的Tick,进行逐个调用 xTaskIncrementTick()函数。
也就是将记录的Tick,逐个Tick的完成:
- 全局Tick计数加 1
- 检查延时就绪列表,将应当苏醒的任务加入就绪列表,回归就绪态。
源码部分如下所示:

xTaskResumeAll() 的核心作用就是:
把暂停期间积压的"时间变化"和"就绪变化",统一补处理。
函数返回值
xTaskResumeAll() 的返回值具有重要含义:
- 若恢复调度器后发生了任务切换 → 返回 pdTRUE;
- 若未发生任务切换 → 返回 pdFALSE。
这个返回值,本质上是在回答一个问题:
在调度器恢复的这一刻,是否有"更高优先级任务"需要立即运行。
在调度器暂停期间,系统中可能已经积累了一些变化,例如:
- 任务延时到期
- 事件唤醒任务(进入 xPendingReadyList)
当调用 xTaskResumeAll() 时,内核会统一处理这些变化,并重新构建就绪列表。
如果此时存在优先级高于当前任务的任务,那么系统需要立即进行任务切换,函数返回 pdTRUE;否则不发生切换,返回 pdFALSE。
因此可以总结为:
xTaskResumeAll() 的返回值,本质就是"是否需要立即进行一次任务切换"。
但是注意:
这个返回值只是告诉你,是否需要任务切换,真正想让任务发生切换这个行为,还需要手动调用函数。
一般来说,该函数的返回值可以接收并处理,标准形式如下:
c
BaseType_t xYieldRequired;
vTaskSuspendAll();
/* 临界区代码 */
xYieldRequired = xTaskResumeAll();
if (xYieldRequired == pdTRUE) {
taskYIELD(); // 主动触发调度,此时有高优先级任务就绪,尽快让它上CPU
}
通过这种方式,可以在恢复调度器后,立即触发一次任务调度,从而让高优先级任务尽快获得 CPU。
当然就算不接收函数的返回值,不主动触发调度,在下一个Tick到来时,系统还是会自动调度切换任务。
很明显,前者必然切换响应速度更快。
两者方式之间,最大会有接近 1个Tick 的响应时间差距。
Tips:简单介绍一下taskYIELD()函数。
调用 portYIELD() 后,系统会触发一次任务调度请求,使当前任务让出 CPU,由调度器重新选择任务运行。
具体表现为:
- 当前任务主动放弃 CPU 的使用权
- 系统进行一次调度
- 选择当前就绪状态中最高优先级的任务运行
根据系统情况,结果可能不同:
- 若存在更高优先级任务 → 切换到更高优先级任务执行
- 若存在同优先级任务 → 进行时间片轮转,切换到下一个任务
- 若没有其他就绪任务 → 当前任务继续执行(看起来像没有变化)
可以一句话理解为:
taskYIELD() 只是让调度器"现在就重新选一次任务",但不保证当前任务一定被切走。
代码示例
在前面,我们让多个任务,使用同一串口打印调试信息,就会出现线程安全问题。
所以我们可以这一对API来实现"禁止任务调度"的受保护区域,这样串口打印调试数据就不会再乱了。
还可以参考下面代码示例:
c
#include "stm32f10x.h" // STM32F10x 标准外设库头文件
#include "FreeRTOS.h" // FreeRTOS 核心头文件
#include "task.h" // FreeRTOS 任务相关 API
#include "DebugUSART1.h" // 调试串口模块(printf1)
// -------------------- 任务1 --------------------
void task1(void *arg) {
uint8_t testFlag = 0;
while (1) {
vTaskDelay(5000);
printf1("task1 running\r\n");
// 只测试一次
if (testFlag == 0) {
testFlag = 1;
printf1("\r\n");
printf1("task1 -> call vTaskSuspendAll()\r\n");
// 暂停调度器
vTaskSuspendAll();
// for循环忙等待延时
for (uint32_t i = 0; i < 8000000; i++);
printf1("task1 -> still running 1\r\n");
// for循环忙等待延时
for (uint32_t i = 0; i < 8000000; i++);
printf1("task1 -> still running 2\r\n");
// for循环忙等待延时
for (uint32_t i = 0; i < 8000000; i++);
printf1("task1 -> still running 3\r\n");
// for循环忙等待延时
for (uint32_t i = 0; i < 8000000; i++);
printf1("task1 -> call xTaskResumeAll()\r\n");
printf1("\r\n");
// 恢复调度器
xTaskResumeAll();
}
}
}
// -------------------- 任务2 --------------------
void task2(void *arg) {
while (1) {
vTaskDelay(3000);
printf1("task2 running\r\n");
}
}
// -------------------- main函数 --------------------
int main(void) {
// 1. 初始化调试串口
DebugUSART1_Init();
printf1("system start\r\n");
// 2. 创建两个同优先级任务
xTaskCreate(task1,
"Task1",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
NULL);
xTaskCreate(task2,
"Task2",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
NULL);
printf1("tasks created\r\n");
// 3. 启动调度器
vTaskStartScheduler();
while (1) {
}
}
还有一个很好的例子就是,假如你需要在任务当中创建新任务,又不希望被打断(尤其被创建的任务优先级还更高的情况)。
也可以临时暂停调度器。
代码示例如下:
c
#include "stm32f10x.h" // STM32F10x 标准外设库头文件
#include "FreeRTOS.h" // FreeRTOS 核心头文件
#include "task.h" // FreeRTOS 任务相关 API
#include "DebugUSART1.h" // 调试串口模块(printf1)
// -------------------- 任务1 --------------------
void task1(void *arg) {
while (1) {
printf1("task1\r\n");
}
}
// -------------------- 任务2 --------------------
void task2(void *arg) {
while (1) {
printf1("task2\r\n");
}
}
// -------------------- 开始任务 --------------------
void beginTask(void *arg) {
TaskHandle_t hand1 = NULL;
TaskHandle_t hand2 = NULL;
// 创建两个优先级更高的任务
xTaskCreate(task1,
"Task1",
configMINIMAL_STACK_SIZE,
NULL,
3,
&hand1);
xTaskCreate(task2,
"Task2",
configMINIMAL_STACK_SIZE,
NULL,
3,
&hand2);
while (1) {
printf1("beginTask\r\n");
}
}
// -------------------- main函数 --------------------
int main(void) {
// 1. 初始化调试串口
DebugUSART1_Init();
// 2. 创建开始任务
xTaskCreate(beginTask,
"beginTask",
configMINIMAL_STACK_SIZE,
NULL,
2,
NULL);
// 3. 启动调度器
vTaskStartScheduler();
while (1) {
}
}
由于被创建的任务优先级反而更高,所以:
beginTask 在创建完 task1 后,task1任务就绪,优先级更高,抢占 beginTask。
随后:
beginTask再也没有机会执行。
task2也不会被创建。
整个系统就只有task1在执行。
为了解决这个问题,可以在创建任务的过程中暂停调度器,还可以在创建任务结束后删除此任务。
参考代码如下:
c
void beginTask(void *arg) {
TaskHandle_t hand1 = NULL;
TaskHandle_t hand2 = NULL;
// 暂停调度器,防止创建高优先级任务后立即发生切换
vTaskSuspendAll();
// 创建任务1
xTaskCreate(task1,
"Task1",
configMINIMAL_STACK_SIZE,
NULL,
3,
&hand1);
// 创建任务2
xTaskCreate(task2,
"Task2",
configMINIMAL_STACK_SIZE,
NULL,
3,
&hand2);
// 恢复调度器
xTaskResumeAll();
// 启动任务完成后,删除自身
vTaskDelete(NULL);
}
如此,beginTask就可以顺利完成任务的创建,随后被删除。
不能使用vTaskDelay等延时函数
FreeRTOS 提供的延时类函数,都不应该在调度器暂停期间使用。
因为延时函数的本质都是:
让当前任务进入阻塞态,主动放弃CPU,然后等待到期后再恢复就绪态,重新参与调度。
这类操作本身就依赖调度器的正常工作。
而 vTaskSuspendAll() 的作用恰恰是:
暂停调度器,禁止正常的任务调度流程。
所以两者在设计目标上就是冲突的。
对于 vTaskDelay() 来说,这一点从源码里其实已经能直接看出来:

所以,在任务调度器暂停期间,调用延时函数的后果就是:
当前任务会进入阻塞态,但不会立即发生任务切换。
比如下列代码:
c
void task1(void *arg) {
while (1) {
printf1("task1 start\r\n");
// 暂停任务调度器
vTaskSuspendAll();
printf1("Delay start\r\n");
// 延时5s
vTaskDelay(5000);
printf1("Delay end\r\n");
// 恢复任务调度器
xTaskResumeAll();
printf1("task1 end\r\n");
}
}
执行完vTaskDelay(5000);函数调用后,并不会立刻让任务进入阻塞,反而会一直运行。
直接打印:
task1 start
Delay start
Delay end
待到调度器恢复,发现task1需要延时阻塞5s,此时才开始真正延时。
于是5s过后,任务1又会打印:
task1 end
Delay start
Delay end
看起来延时的行为还在,但被延后了,这显然也是不符合我们预期的。
因此:
凡是会让当前任务阻塞、等待时间到达的延时函数,都不应放在 vTaskSuspendAll() 和 xTaskResumeAll() 之间使用。
包括:
vTaskDelay()vTaskDelayUntil()
等FreeRTOS提供的延时函数,都不能在任务调度暂停期间使用。
那么我自己写一个for循环忙等待延时,可不可以用呢?
用是可以用,但不推荐!
在任务调度器暂停期间加延时,是一个可以实现,但是没有意义的操作!
暂停调度器的时间不应该过长
暂停调度器的时间不应该过长。
这是因为:
vTaskSuspendAll() 暂停的只是任务调度,不是整个系统时钟,也不是所有内核活动。
在这段期间内:
- 当前任务会一直占用 CPU 持续运行
- 其他任务即使已经满足运行条件,也无法及时获得 CPU
- 所有与调度相关的处理,都会被不断积压,等恢复调度器后再统一处理
因此:
如果只是短时间的暂停调度器,避免任务执行共享资源时被打断,这是合理的设计。
但如果长时间暂停调度器,势必导致整个系统的实时性,确定性都被破坏。
所以:
暂停调度器,只适合用于保护极短的关键代码片段,其中不应该执行延时、长循环,甚至阻塞等待等操作。
周末放假两天,积压两天的工作,周一还是可以处理的,但如果放假一年,公司就倒闭了。
注意:暂停调度器不算真正的临界区
严格来说,暂停调度器,并不算真正意义上的临界区。
它可以在系统明确只有任务切换,而没有中断抢占任务时,**"冒充"**一下临界区。
这是因为 vTaskSuspendAll() 的作用只是:禁止调度,禁止任务之间的切换。
但它并没有禁止中断。
这就意味着,在调度器被暂停期间:
- 虽然当前任务不会被其他任务打断,
- 但仍然可能被中断打断。
如果中断中也访问了同一份共享资源,依然可能发生线程安全问题。
因此,从严格意义上讲:
vTaskSuspendAll() 只能保证"任务级别"的原子操作,不能保证"系统级别"的原子性。
也就是说:
- 它只能防止"任务与任务之间"的竞争,导致数据出现问题。
- 但无法防止"任务与中断之间"的竞争。
所以可以得到一个非常重要的结论:
暂停调度器,只是弱化版的临界区,而不是真正的临界区。
当然:
能不能用暂停调度器,取决于你的资源是否会被中断访问;只要涉及中断,就不能把它当作临界区使用。
什么样才算真正的临界区呢?
很简单,临时禁用中断,就是真正意义上的实现"系统级别原子操作",是真正的临界区。
日常口语描述下,我们常说的"临界区"也是指"禁用中断"级别的临界区。
使用建议
什么时候可以使用"暂停调度器"这种级别的弱化版临界区呢?
它适用的场景其实是非常有限的,毕竟它是弱化版。
一般来说,只有在满足下面条件时,才适合使用:
第一,共享资源只会被"任务访问",不会被中断访问。
也就是说:
这份共享资源,只存在于任务之间的竞争,
中断中不会去读写它。
在这种情况下,只需要防止任务切换即可,即便中断"插一脚"也不会对共享资源产生影响。
此时使用暂停调度器就是安全的。
第二,被保护的代码执行时间非常短
暂停调度器期间:
所有任务调度都会被延后,如果这段时间过长,就会影响系统实时性。
因此:
只适合保护很短的一段代码。
因此不能在保护代码中执行延时、等待等操作。
临界区
在 FreeRTOS 当中,一个真正意义上能被称为临界区的机制,应当同时满足两个条件:
- 能防止任务切换
- 还能防止中断打断
FreeRTOS已经给我们提供了这样的一组 API,如下:
c
taskENTER_CRITICAL(); // 进入临界区,不会被中断和任务打断
taskEXIT_CRITICAL(); // 退出临界区
这一组 API 的核心思想也非常简单:
禁用中断,关闭中断,来保证当前代码执行期间不被打断。
也就是说,只要中断被关闭,就能够同时实现两大功能:
- 不会被中断打断
- 不会发生任务切换
于是你马上就会提出一个疑问:
关闭中断后,不会被中断打断,这很正常,很好理解。但为什么同时也不会发生任务切换呢?
为了解释这个问题:
我们还需要先了解一下,FreeRTOS任务切换的过程,究竟是怎么进行的。
FreeRTOS任务切换的流程
在前面的课程中,我们已经对 FreeRTOS 的任务切换有了初步了解:
- 任务被切换时,会将当前上下文信息保存到自身的任务栈中
- 当任务再次运行时,从任务栈中恢复上下文信息,继续执行
但是,这里还存在两个关键问题:
- 任务是在什么时候发生切换的?
- 任务切换的具体执行过程是怎样的?比如会调用哪些函数?
这些内容在前面并没有展开。
这一小节,我们就来把这一部分补完整。
首先要明确一个非常关键的结论:
FreeRTOS 的任务切换,并不是"随时发生的",也不是在哪个函数中完成的,而是必然在ISR中断完成的。
这也是临界区禁止中断后,就可以禁止任务切换的根本原因!
FreeRTOS 任务切换流程,可以直接分为两大步:
- 触发任务切换请求,也就是触发任务调度器进行调度。
- 在 PendSV 中断服务函数(PendSV_Handler)中完成任务切换。
我们先不讲调度如何触发,因为有多种方式,不妨先来看一下 PendSV_Handler 是如何实现任务切换的。
PendSV_Handler中断处理函数
在 FreeRTOS 的移植过程中,我们通过宏定义:
#define xPortPendSVHandler PendSV_Handler
将 PendSV_Handler 中断服务函数交由 FreeRTOS 内核接管。
因此可以明确一点:
FreeRTOS 中的任务切换,并不是在任务调用某个函数中直接完成的,而是统一在 PendSV_Handler 中断中完成。
也就是说:
任务本身只能"触发调度","请求一次调度"
但真正的任务切换,也就是真正的"上下文切换动作",一定发生在 PendSV_Handler 中。
这也是 FreeRTOS 的一项强制设计:
所有任务切换,都必须在中断环境中完成,而这个中断就是 PendSV。
这样设计的目的很明确:
将任务切换这一复杂且关键的操作,统一放在一个固定入口中执行,从而保证切换过程的可控性与一致性。
总结一句话:
PendSV_Handler 就是 FreeRTOS 任务上下文切换的唯一执行现场。
上下文切换流程
内核接管的 PendSV_Handler 函数处理,其源码大多都是汇编代码。
但从整体逻辑上看,它的执行可以概括为三步:
- 先保存当前任务的上下文信息(入栈)。
- 当前正在运行的任务进入 PendSV 中断后
- 会将当前任务(也就是pxCurrentTCB指针指向的任务)需要保存的CPU内核寄存器压入当前任务栈中
- 包括当前任务的任务栈栈顶等信息也都会保存起来。
- 这就是任务上下文切换过程中的上下文保存。
- 调用函数vTaskSwitchContext(),判断是否需要切换任务。
- 该函数的作用是:
- 根据当前系统状态(优先级、就绪列表等),判断是否需要切换任务,并选择下一个要运行的任务。
- 也就是说:PendSV 只是提供"切换的执行环境",只做"上下文的保存与恢复",是否真的发生任务切换,是由调度器决定的。
- 恢复上下文信息(出栈)。
- 根据 pxCurrentTCB 指向的任务,将它的上下文信息,从任务栈中恢复,恢复 CPU 的执行现场。
- 完成后,中断执行完毕,退出中断。
- 注意:pxCurrentTCB 指向的任务可能是新任务,也完全可以还是旧任务,具体是哪一个任务要看优先级和就绪列表的状态。
- 所以:如果调度器选择了新的任务,那么就完成了任务切换,如果仍然是原任务,那么实际上只是"进了一次 PendSV,又回来"。
总结一句话:
PendSV 负责"执行切换",vTaskSwitchContext 决定"要不要切换、切换到谁"。
扩展/补充了解:为什么选择PendSV这个中断来切换任务
这牵扯到FreeRTOS的内核底层设计问题,而且这是一个非常巧妙的设计,非常推进大家阅读学习一下。
我们一步步得来回答这个问题:
第一,我们首先解释一下为什么要用中断来完成任务切换。
原因很简单:
- 如果在普通任务环境下进行任务切换,这是非常不合理的。因为:
- 任务处于执行状态下,当前代码本身也在用CPU寄存器
- 那怎么完成任务上下文的保存呢?
- 一边动态运行,一边记录静态的状态,显然不可能。
- 所以,任务切换必然不可能在任务环境下完成。
- 使用中断切换任务还有一个天然的好处:
- 进入中断时,任务已经处于冻结状态,硬件自动保存任务执行的上下文。
- 在这种状态下,进行任务的切换是非常合理且安全的。
总之,只有在中断上下文中,任务切换才是"安全且可控的"。
第二个问题,为什么非要选择PendSV这个中断来切换任务呢?
首先,我们要明确的一些前提是:
FreeRTOS明确要求,STM32集成FreeRTOS,需要设置使用优先级分组4。
如下图所示:

也就是说,整个STM32中断系统:
没有子优先级,全部4位优先级位寄存器都用于设置抢占优先级,抢占优先级的取值范围是:0 ~ 15
当然,中断的优先级数值越大,优先级越低。(这和FreeRTOS的任务优先级是恰好相反的)
并且:
FreeRTOS启动后,任务成为了"主程序执行",所有中断都可以打断任务的执行。
现在来回答问题:
为什么非要选择PendSV这个中断来切换任务呢?
原因很简单,PendSV中断的优先级最低,默认是15,源码设置如下所示:

这里有两个中断:PendSV和Systick中断。
在内核优先级寄存器中,它们的存储位置如下:
| 31~24 | 23 ~ 16 | 15 ~ 8 | 7 ~ 0 | ↑ ↑ SysTick PendSV
并且是高4位生效。
所以在FreeRTOS当中,PendSV和Systick中断的优先级都是抢占优先级15,属于优先级最低的任务。
这样设计的好处是:
它们执行的时候,一定是在"所有更高优先级中断都执行完"之后,它们一定不会打断其他任务的执行!
这样做就使得中断过程中执行的任务切换,可以更加安全的完成。
举个例子:
系统中 USART 中断正在执行:
c
uint8_t data = USART_ReceiveData();
xQueueSendFromISR(...); // 触发调度
如果 PendSV 优先级不低,它会立刻抢占当前 USART 中断,在中断还没执行完的时候就开始任务切换。
这样会导致中断逻辑被打断,数据可能没处理完,系统状态也可能被破坏,这是不安全的。
而在 FreeRTOS 中,PendSV 被设置为最低优先级。
当在中断中触发调度时,PendSV 不会立刻执行,而是等当前 USART 中断执行完并退出之后,再执行任务切换。
关键点在于:任务切换发生在中断执行完成之后,而不是中断执行过程中。
总结一句话:PendSV 设为最低优先级,是为了保证任务切换不会打断正在执行的中断。
扩展/补充了解:任务的第一次执行
既然任务切换是通过中断完成的,那么任务第一次上CPU是怎么完成的呢?
首先,任务第一次上CPU也是通过中断来完成的。
既然任务第一次上CPU,也需要依赖中断来完成,于是就自然存在一个问题:
任务都还没有开始执行,任务栈中怎么会保存任务的上下文信息呢?(中断结束后,需要恢复上下文执行任务)
原因很简单:
第一次运行的任务,其"上下文"并不是上一次运行保存出来的,而是FreeRTOS"伪造出来的"。
FreeRTOS在创建任务时,会在该任务的任务栈中,提前构造好了一个"假的中断现场、一个假的任务执行上下文"。
因此,这个"伪造的上下文",本质上是在模拟一种场景:
就好像这个任务曾经运行过一次,被中断打断,现在需要被恢复执行。
换句话说:
FreeRTOS在创建任务时,已经提前帮你把"任务第一次运行时所需要的CPU寄存器状态、以及其他信息",全部压入了任务栈中。
然后中断结束,上下文恢复后,CPU就会从任务的入口函数开始执行。
如此,就完成了任务的第一次执行。
但具体到细节:
- FreeRTOS内核调度器启动后的第一个任务执行,真正意义上的第一个任务执行。
- 后续其他任务的第一次执行。
它们还是有区别的。
其中:
- SVC 中断是任务调度的入口,它负责来启动系统的第一个任务,将整个系统从MSP模式转换成PSP模式。
- PendSV 是任务调度的"切换器",由它来负责系统第二个任务开始的,所有任务的切换与执行。
举一个例子:
ABC三个优先级一致的任务,首先执行任务C。
于是由 SVC 中断来启动任务C,这是整个系统的第一个任务执行,由SVC中断完成。
随后AB任务的第一次执行,包括随后的ABC三者时间片轮转,都由PendSV中断完成任务的切换。
小Tips:
- 任务栈的栈基地址是任务栈的最低地址处,且整个任务生命周期内保持不变。
- 但任务栈的栈顶地址,在任务创建后就不再是任务栈的最高地址了,而是已经向低地址偏移,指向"伪造上下文"所在的位置。
好了,我们讲完了任务上下文切换的细节,那么我们再回过头来看一下,任务调度是如何触发的。
任务主动触发调度:taskYIELD()
在前面的学习中,我们已经接触过一种主动触发任务调度的方式:
c
taskYIELD();
它由某个任务调用,主动让出CPU,主动触发一次任务调度。
最终的结果就是触发 PendSV 中断,在 PendSV_Handler 中根据任务调度机制来决定谁上CPU。
随后完成必要的上下文切换流程。
最常见的触发调度方式:Tick中断(重点)
在 FreeRTOS 中,任务调度的触发方式有多种,但最常见、也是最核心的一种方式,就是Tick 中断。
什么是Tick中断
在 FreeRTOS 中,Tick 中断可以这样理解:
Tick 中断,是一个按照固定时间间隔、周期性触发的系统定时中断。
很显然,这种周期性的、时间强相关的中断,必然需要定时器外设的参与。
在FreeRTOS中,Tick 中断实际上就是由最简单的定时器------**系统嘀嗒定时器(SysTick)**触发的。
它会按照设定好的周期,不断重复触发中断。
Tick 中断的产生频率,由如下宏决定:
c
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
所以对于这个宏的理解,我们进一步加深了:
这个宏配置了系统每秒会产生多少次 Tick,每秒划分为多少个时间片,也是每秒会产生多少次Tick中断。
换一个更本质的理解方式:
Tick 是 FreeRTOS 的最小时间单位。
系统时间被划分为一个个离散的 Tick,每到一个 Tick,系统时间就向前推进一步。
每一个新的 Tick 到来时,都会触发一次 Tick 中断。
Tips:
SysTick系统嘀嗒定时器产生的Tick中断,其中断处理也被FreeRTOS接管了。
这就是我们在移植FreeRTOS时,使用的下面配置宏:
c
#define xPortSysTickHandler SysTick_Handler
关于SysTick系统嘀嗒定时器中断,我们后续定时器课程还会再详谈它,这里你了解一下即可。
Tick中断做了什么事情呢?
Tick中断做的事情,我们太熟悉了。
实际上它就做了这三件事情:
- 系统全局Tick计数 + 1
- 遍历延时阻塞列表,查看是否有任务需要唤醒
- 若需要切换任务,则触发一次系统调度。即触发PendSV中断。

这也解释了一个问题:
引入FreeRTOS后,Systick系统嘀嗒定时器,被用于产生FreeRTOS系统节拍,产生Tick中断。
既然Systick系统嘀嗒定时器已经被占用了,那我们就不要再用了。
之前我们实现的,基于Systick系统嘀嗒定时器的延时函数,就不能再使用了。
其他触发任务调度的场景
看到这里,我们已经知道:
所谓触发任务调度,本质上就是触发PendSV中断,在中断中完成任务调度。
除了上面讲的两个场景外:
- 任务进入阻塞态,也会触发中断,进行任务调度。
- 某个任务被唤醒(时间到期、事件发生),也会触发中断,进行任务调度。
- ...
taskENTER_CRITICAL()临界区
FreeRTOS 中另一种构建临界区的方式 ------ taskENTER_CRITICAL()
除了通过挂起调度器(vTaskSuspendAll / xTaskResumeAll)构建"调度级临界区"之外,
FreeRTOS 还提供了更常见的一种临界区机制:
taskENTER_CRITICAL() 与 taskEXIT_CRITICAL()。
这是一对专门用于临界资源保护的函数。
c
taskENTER_CRITICAL();
...
taskEXIT_CRITICAL();
调用 taskENTER_CRITICAL() 表示进入临界区,
调用 taskEXIT_CRITICAL() 表示退出临界区。
这两个函数必须成对使用,用于包裹需要被保护的代码段。
当进入临界区后:
当前任务不会被任务调度器切换出去。
示例代码如下:
c
#include "stm32f10x.h" // STM32F10x 标准外设库头文件
#include "FreeRTOS.h" // FreeRTOS 核心头文件
#include "task.h" // FreeRTOS 任务相关 API
#include "DebugUSART1.h" // 调试串口模块(printf1)
// -------------------- 任务1 --------------------
void task1(void * arg) {
while (1) {
printf1("task1\r\n");
}
}
// -------------------- 任务2 --------------------
void task2(void * arg) {
while (1) {
printf1("task2\r\n");
}
}
// -------------------- 开始任务 --------------------
void beginTask(void * arg) {
TaskHandle_t hand1 = NULL;
TaskHandle_t hand2 = NULL;
taskENTER_CRITICAL();
xTaskCreate(task1,
"Task1",
configMINIMAL_STACK_SIZE,
NULL,
3,
&hand1);
xTaskCreate(task2,
"Task2",
configMINIMAL_STACK_SIZE,
NULL,
3,
&hand2);
taskEXIT_CRITICAL();
while (1) {
printf1("xTaskResumeAll\r\n");
}
}
// -------------------- main函数 --------------------
int main(void) {
DebugUSART1_Init();
xTaskCreate(beginTask,
"beginTask",
configMINIMAL_STACK_SIZE,
NULL,
2,
NULL);
vTaskStartScheduler();
while (1) {
}
}
在这段代码中,
任务创建过程被临界区包裹,
在退出临界区之前,不会发生任务调度。
挂起调度器 与 taskENTER_CRITICAL 的区别
虽然这两种方式都可以"保护代码",
但它们的本质机制不同。
vTaskSuspendAll / xTaskResumeAll 的特点
它们的作用是暂停任务调度器。
调用 vTaskSuspendAll() 后:
- 不发生任务切换;
- 但中断仍然可以发生;
- 中断服务函数仍然会执行。
如果在中断中:有更高优先级任务被唤醒进入就绪态,
那么在恢复调度器时,可能立即发生任务切换。
因此可以总结为:
挂起调度器 = 只禁止任务调度,不禁止中断。
taskENTER_CRITICAL 的特点
taskENTER_CRITICAL() 不仅:
禁止任务调度;
还会:屏蔽部分中断。
具体屏蔽哪些中断,取决于:
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 191
在 Cortex-M3 架构下:
NVIC 使用高 4 位作为中断的有效优先级。
191 对应 0xB0 → 1011 1111,只取高四位,就是1011,即十进制优先级 11。
由于 Cortex-M 中:
数值越小,优先级越高。
因此:
进入临界区后,
会屏蔽优先级数值为 11~15 的中断,
不会屏蔽优先级 0~10 的中断。
可以理解为:
只屏蔽"低优先级中断",
高优先级中断仍然可以响应。
当然,屏蔽了中断,自然就屏蔽了任务调度。
如何选择使用哪种方式
可以这样理解:
如果希望:
保护"短时间关键代码",并且不希望被中断打断,
可以使用:taskENTER_CRITICAL() / taskEXIT_CRITICAL()
如果希望:允许中断响应,但不允许任务切换。
可以使用:
vTaskSuspendAll() / xTaskResumeAll()
为什么不建议长时间进入临界区
无论使用哪种方式,都不建议在临界区中执行长时间代码。
原因如下:
- 第一,实时性会下降。
- 如果长时间关闭调度或屏蔽中断,
- 高优先级任务和中断将无法及时响应。
- 第二,系统抖动增加。
- 延迟的中断会在退出临界区后集中执行,
- 可能导致系统瞬时负载增大。
- 第三,可能引发看门狗复位。
- 若长时间屏蔽中断,某些周期性中断无法执行,
- 可能触发系统异常。
因此临界区应尽量短小,只保护必要的共享数据操作,避免在临界区中:
- 执行 printf
- 调用 vTaskDelay
- 进行复杂逻辑计算
- 执行阻塞操作
总结一下,taskENTER_CRITICAL临界区:
- 禁止调度 + 屏蔽部分中断,以此来实现一个更加安全的临界区
- 适合极短关键代码保护
总结一下,vTaskSuspendAll临界区:
- 只禁止调度,允许中断响应
- 适合需要保证连续执行但又不想影响中断的场景
合理选择机制,是实时系统设计的重要能力之一。
中断和任务间的屏蔽问题
中断一定能打断任务吗?临界区是如何真正屏蔽中断的?
我们之前讲过一句话:
在 FreeRTOS 中,ISR 的优先级天然高于任务优先级,因此中断可以随时打断任何任务的执行。
这句话是正确的。
但随后我们又看到:
taskENTER_CRITICAL() 可以屏蔽部分中断;
这看起来似乎有些矛盾,其实并不矛盾。关键在于:
中断是否能打断任务,取决于"CPU当前是否允许这个等级的中断执行"。
而 FreeRTOS 的临界区机制,本质上是操作了 Cortex-M 的中断屏蔽寄存器。
回顾:STM32 中的中断屏蔽寄存器
在 Cortex-M 内核中,常见的中断屏蔽寄存器有三个:
- BASEPRI
- PRIMASK
- FAULTMASK
它们的作用分别不同:
一、BASEPRI
BASEPRI 用于屏蔽"低优先级中断"。
当设置 BASEPRI = X 时:
优先级数值 ≥ X 的中断将被屏蔽。
注意:
Cortex-M 中优先级数值越小,优先级越高。
因此:
设置一个 BASEPRI 值,本质是设定一个"屏蔽阈值"。
高于这个阈值(数值更小)的中断仍然可以触发。
这正是 FreeRTOS 使用的核心机制。
二、PRIMASK
PRIMASK 只有一位:
- PRIMASK = 1 → 屏蔽所有普通 IRQ
- PRIMASK = 0 → 允许普通 IRQ
即:
它会屏蔽所有可屏蔽中断,
但不会屏蔽:
- NMI
- HardFault
这两个系统级别中断
三、FAULTMASK
FAULTMASK = 1 时:
- 屏蔽所有可屏蔽中断
- 甚至屏蔽 HardFault
仍然不会屏蔽 NMI。
这个寄存器极少在普通应用中使用。
关于 HardFault 与 NMI
HardFault:
当系统发生严重异常时触发,例如:
- 访问非法地址
- 执行非法指令
- 总线错误
- 使用错误
默认处理函数通常是死循环:
while(1);
NMI(Non-Maskable Interrupt):
最高优先级中断,
不能被任何寄存器屏蔽。
常见触发原因:
- 看门狗异常
- 时钟失锁
- 外部硬件异常
FreeRTOS 是如何实现临界区的?
理解了这些寄存器之后,
我们就可以回答:
taskENTER_CRITICAL() 是如何屏蔽中断的,以及taskEXIT_CRITICAL() 如果退出临界区,解除屏蔽的。
taskENTER_CRITICAL 屏蔽中断的实现原理:
设置 BASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY。
也就是说:
屏蔽优先级数值 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断。
由于 Cortex-M 采用"数值越小优先级越高"的规则,因此:
- 数值较大的(低优先级中断) → 被屏蔽
- 数值较小的(高优先级中断) → 仍然可以响应
换句话说:
FreeRTOS 并没有关闭所有中断,而是只屏蔽"可调用系统 API 的那一类中断"。
这样设计的目的,是在保证临界区安全的同时:
仍然允许极高优先级的中断(如紧急中断)继续执行,从而保证系统实时性。
另外,taskENTER_CRITICAL() 还做了一件重要的事情:
维护临界区嵌套计数(uxCriticalNesting)
也就是说:
- 每调用一次 taskENTER_CRITICAL → 计数 +1
- 支持"临界区嵌套"使用
taskEXIT_CRITICAL 解除中断屏蔽的过程:
taskEXIT_CRITICAL() 并不会每次调用都立即恢复中断,而是:
- 先将嵌套计数减 1
- 只有当计数减到 0 时,才真正恢复中断
最终的恢复动作是:
c
BASEPRI = 0
这表示:
取消中断屏蔽阈值,恢复所有可屏蔽中断的响应能力。
核心总结
taskENTER_CRITICAL 的本质:
都是通过修改 Cortex-M 的 BASEPRI 寄存器,来控制中断响应范围。
它们并不是在"操作任务",而是在操作"CPU 的中断屏蔽级别"。
因此:
中断是否能打断任务,最终取决于当前的中断屏蔽状态,而不是任务优先级本身。
临界区的退出
与 xTaskResumeAll() 不同,taskEXIT_CRITICAL() 几乎不做"内核层面的处理"。
它只做一件事: 先嵌套计数-1,然后在嵌套计数归零时,把 BASEPRI 清零,恢复中断响应。
之后发生的事情,其实不是它"主动做的",而是硬件自动触发的:
- 之前被屏蔽的中断(如 Tick)开始执行
- 如果这些中断触发了调度(如 PendSV)
- 才会进一步发生任务切换
所以可以这样对比总结:
xTaskResumeAll():主动处理一堆内核事务(补调度、处理延迟列表等)taskEXIT_CRITICAL():只恢复中断,后续一切交给中断机制自然推进
一句话总结:
调度器恢复是"内核主动收尾",而临界区退出只是"打开中断开关"。
临界区嵌套
taskENTER_CRITICAL() 的嵌套机制
在 FreeRTOS 中,taskENTER_CRITICAL() 支持嵌套调用。
也就是说:
可以在一个临界区内部再次进入临界区,而不会破坏中断屏蔽状态。
其本质实现方式是:
通过内部计数器(临界区嵌套计数)来维护嵌套深度。
具体机制如下:
- 每调用一次 taskENTER_CRITICAL() → 计数器递增;
- 每调用一次 taskEXIT_CRITICAL() → 计数器递减;
- 只有当计数器递减至 0 时,才真正恢复中断。
这意味着:
只有"最后一次退出"临界区时,才会重新开启中断。
嵌套示例
c
taskENTER_CRITICAL(); // 第一次进入,计数器 = 1,屏蔽中断
// ... 临界区代码1 ...
taskENTER_CRITICAL(); // 第二次进入,计数器 = 2,中断仍然屏蔽
// ... 临界区代码2 ...
taskEXIT_CRITICAL(); // 计数器 = 1,中断仍然保持屏蔽
taskEXIT_CRITICAL(); // 计数器 = 0,恢复中断
可以看到:
中断只在"最外层临界区退出"时恢复。
为什么需要嵌套支持
在实际工程中,
某些函数内部可能已经使用了临界区,
而上层调用函数也可能使用了临界区。
如果不支持嵌套,
则会导致:
- 中断提前被恢复;
- 临界区保护失效;
- 出现数据竞争风险。
因此:
嵌套计数机制是保证临界区安全性的关键。
底层实现说明(Cortex-M)
在 Cortex-M 端口中:
FreeRTOS 使用 BASEPRI 寄存器来屏蔽部分中断。
taskENTER_CRITICAL():
- 提高 BASEPRI 屏蔽级别;
- 同时增加嵌套计数。
taskEXIT_CRITICAL():
- 减少嵌套计数;
- 当计数归零时恢复 BASEPRI。
注意事项
必须保证:
每一个 taskENTER_CRITICAL()都有一个对应的 taskEXIT_CRITICAL()
否则:
计数器无法归零,中断将永久处于屏蔽状态,系统将失去实时响应能力。
小结
taskENTER_CRITICAL 的嵌套机制依赖:
内部计数器维护临界区层级。
其原则是:
进入几次,就必须退出几次。只有最外层退出时,中断才会真正恢复。
重要的提醒
在我们创建使用多任务FreeRTOS系统时,(默认配置下)如果希望禁用中断的临界区功能生效,应该遵循以下原则:
- 用户自定义中断的优先级不要高于阈值(11),即中断优先级数值应满足:大于或等于11。
- 推荐设置为 11 或 12。
- 这样这些中断才能被 BASEPRI 正常屏蔽,从而保证临界区有效。
- 如果优先级设置得更高(数值更小,例如 0~10),则不会被屏蔽,临界区将失去保护作用。
- 自定义中断优先级设置为 11 或 12 已经足够使用,这是因为:
- 它们仍然属于"较高优先级中断",响应速度已经很快(毕竟这个优先级已经高于任务调度的中断优先级了)。
- 同时又处于 FreeRTOS 可管理范围内(可被 BASEPRI 屏蔽),可以安全调用 FromISR API。
注意:
只有优先级数值 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断,才允许调用 FreeRTOS API(FromISR 版本)。
原因也很简单:
FreeRTOS 内核在操作关键数据结构(如就绪列表、延时列表等)时,大量依赖临界区进行保护。
而临界区的本质,是通过 BASEPRI 屏蔽"一部分中断"。
如果你定义的中断优先级过高(数值更小),不受 BASEPRI 控制,那么就会出现这种情况:
内核正在修改链表 → 你这个高优先级中断突然打断 → 又调用了 FreeRTOS API → 再次操作这些链表
结果就是:
内核数据结构被破坏,系统直接异常甚至崩溃。
所以,必须牢记一个结论:
用户自定义中断的优先级,不要高于 configMAX_SYSCALL_INTERRUPT_PRIORITY 对应的阈值。
也就是说:
中断优先级数值必须 ≥ 该阈值(例如 ≥ 11)。