bug现象:
key任务:
默认任务:
此时两个任务的优先级相同,抢占式调度,时间片轮转,空闲任务让步。
但是会出现一个问题,key任务在发送完队列之后不会立即跳转到默认任务的队列接收函数后的printf,而是会在printf("Send Successfully")打印一半之后再跳转到默认任务的printf里,但是此时默认任务的printf也无法打印了。具体跳转步骤如下图:
原因解析:
步骤“1”执行队列发送函数的时候,会唤醒等待队列数据接收的任务,如果任务优先级比当前任务高,那么会立刻抢占cpu,但是现在由于两个任务的优先级是平级的关系,只能在时间片轮转的时候进行任务调度,让默认任务优先运行(等待队列接收的任务)。
由于不是立即跳转,导致步骤“2”的printf进行了一半,跳转到了步骤“3”的printf,由于printf是不可重入函数,所以步骤“3”的printf无法打印。
运行顺序为:
步骤“1” -> 步骤“2”一半 ->步骤“3” ->步骤“3”所在任务运行一个时间片(等待队列接收)->任务调度回到步骤“2”将printf剩下的一半打印完
解决方法:
1 将key任务优先级抬高
当key任务发送队列唤醒默认任务时,不会在下一个时间片切换任务,而是等key任务主动放弃CPU资源的时候再进行任务调度(先看优先级,再是队列接收),从而保证printf()不会被多个任务同时调用,保证默认任务中步骤“3”printf的打印正常进行。
2 将默认任务优先级抬高
与方法1同理,将默认任务优先级抬高后可以在发送队列结束后立即跳转(默认任务被唤醒,优先级比key任务高,抢占CPU)到默认任务的打印函数(步骤“3”),在默认任务运行到阻塞(等待队列接收)主动出让CPU资源后再跳转到步骤“2”运行printf,这种方法同样保证了printf同时只被一个任务调用。
3 不更改任务优先级,更改调度方式(不常用,了解即可)
关于调度方式:
这个设置的更改即从“时间片轮转”变为“非时间片轮转”,让任务调度只能通过优先级抢占和主动出让CPU资源来进行任务切换。
由于key任务和默认任务优先级平级,当取消时间片轮转后,自然不会出现步骤“2”printf打印一半就被时间片调度跳转到另一个任务的情况,此时没有更高优先级的任务,只有key任务主动出让CPU资源才能切换任务。也保证了printf()函数同时只被一个任务调用。
知识点补充:
立即触发调度的操作/函数:(deepseek)
关于xQueueSend()或xSemaphoreGive()的调度:(deepseek)
1. 默认行为:有条件触发调度
这两个函数在以下场景中可能触发立即调度:
存在等待的高优先级任务:若调用
xQueueSend()
或xSemaphoreGive()
时,有更高优先级的任务正在等待队列数据或信号量,则调度器会立即切换任务(抢占式调度生效)。例如:// 任务 A(低优先级)发送数据到队列
xQueueSend(xQueue, &data, 0);
// 任务 B(高优先级)正在阻塞等待该队列
// 此时任务 A 会立即被抢占,任务 B 开始运行
配置为抢占式调度:若 FreeRTOS 的调度策略设置为抢占式(默认),则高优先级任务就绪时会立即触发切换。
2. 不触发调度的场景
以下情况不会立即触发调度:
无等待任务或优先级不满足:若没有任务在等待队列/信号量,或等待的任务优先级不高于当前任务,则当前任务继续运行,调度器不会切换。
在中断服务程序(ISR)中未显式请求调度:在 ISR 中调用
xQueueSendFromISR()
或xSemaphoreGiveFromISR()
时,需手动调用portYIELD_FROM_ISR()
才会触发调度。
3. 与时间片轮转调度的关系
若系统启用了时间片轮转(configUSE_TIME_SLICING = 1
),即使没有更高优先级任务,同优先级任务也会在时间片用完时切换。但 xQueueSend()
或 xSemaphoreGive()
本身不会强制切换,需依赖时间片中断。
总结
场景 |
是否触发调度 |
说明 |
发送数据/信号量唤醒更高优先级任务 |
立即触发 |
抢占式调度生效,任务切换 |
发送数据/信号量唤醒同或低优先级任务 |
不触发 |
当前任务继续运行 |
在 ISR 中未手动调用 |
不触发 |
需显式请求调度 |
无任务等待队列/信号量 |
不触发 |
无任务需切换 |
补充:显式调度与隐式调度(deepseek)
1. 显式触发调度(Explicit Scheduling)
含义:
开发者主动调用特定函数来强制触发任务调度,明确要求系统立即切换到其他就绪任务。
特点:
直接控制调度时机,代码中可见明确的调度指令。
适用于需要手动让出CPU的场景,例如协作式多任务或优化关键代码段的执行顺序。
FreeRTOS 中的显式触发函数:
taskYIELD()
:立即触发调度器重新选择任务,即使当前任务的时间片未用完。void vTaskFunction(void *pvParameters) { while (1) { // 执行某些操作后主动让出CPU taskYIELD(); // 显式触发调度 } }
2. 隐式触发调度(Implicit Scheduling)
含义:
调度由系统事件或API函数的副作用自动触发,开发者无需显式调用调度函数。
特点:
调度时机由系统管理,与任务状态变化(如阻塞、资源释放)或中断事件绑定。
更符合抢占式调度机制,提升系统自动化程度。
FreeRTOS 中的隐式触发场景:
任务阻塞操作:调用
vTaskDelay()
、xQueueReceive()
等函数时,任务进入阻塞状态,调度器自动切换任务。vTaskDelay(pdMS_TO_TICKS(100)); // 隐式触发调度(任务阻塞)
资源释放唤醒高优先级任务:例如,在
xSemaphoreGive()
或xQueueSend()
中,若释放资源后存在更高优先级任务就绪,调度器自动抢占当前任务。xSemaphoreGive(xSemaphore); // 隐式触发调度(若唤醒高优先级任务)
中断服务程序(ISR)中的调度请求:在中断中调用
xQueueSendFromISR()
或xSemaphoreGiveFromISR()
后,通过portYIELD_FROM_ISR()
隐式触发调度(需结合中断上下文)。
对比总结
特性 |
显式触发调度 |
隐式触发调度 |
触发方式 |
开发者主动调用函数(如 |
系统自动触发(如任务阻塞、资源释放、中断) |
控制权 |
完全由开发者控制 |
由系统事件或API逻辑控制 |
典型场景 |
协作式任务切换、优化执行顺序 |
抢占式调度、事件驱动任务切换 |
代码可见性 |
显式代码指令 |
隐含在API或系统行为中 |
不可重入函数
顾名思义:不可重入函数就是不可以在它还没有返回就再次被调用。
不可重入原因:
由于函数内部使用了全局变量、静态变量、或调用了不可重入的函数等,导致函数在执行过程中可能被中断,并在中断后继续执行时出现数据错误或不可预料的后果。以下是不可重入函数的几个特点:
使用全局变量或静态变量:如果函数内部使用了全局变量或静态变量,那么在多任务环境下,多个任务同时调用该函数时,可能会对这些共享变量进行并发修改,导致数据不一致。
调用不可重入函数:如果函数内部调用了其他不可重入函数,那么这些被调用的函数也可能因为上述原因导致整个调用链不可重入。
使用动态内存分配:函数体内调用了
malloc()
或者free()
函数,由于这些函数维护内部的链表,且这个过程不是原子的,因此在多任务环境下可能导致内存管理出现问题。使用标准I/O函数:标准I/O函数通常使用全局数据结构,因此在多任务环境下也可能导致不可重入问题。