最近学习FreeRTOS源码时,看到vTaskSuspendAll()函数中,在++uxSchedulerSuspended前后,没有调用taskENTER_CRITICAL()和taskEXIT_CRITICAL()关闭中断保护。++uxSchedulerSuspended毫无疑问不是原子操作,看着好像有问题(当然,大神写的代码肯定没问题),网上搜索了一些相关文章,FreeRTOS中vTaskSuspendAll()和xTaskResumeAll()介绍 - 知乎,但是笔者看了还是不太清晰,不甚明了,所以就自己分析了一下。
先说明一下vTaskSuspendAll()的作用,如果一个临界区执行时间过长而不适合通过关闭中断(vTaskEnterCritical)的方式来保护临界区的话,可以通过挂起调度器来保护临界区。系统维护一个全局变量uxSchedulerSuspended的计数值,当其大于0的时候禁止调度,等于0的时候表示允许调度。如果调度器挂起话,当前正在执行的Task会一直继续执行,内核不再调度(意味着当前任务不会被切换出去),直到该任务调用了xTaskResumeAll()函数。
vTaskSuspendAll()对应汇编代码
我们知道,C语言编译后变为汇编指令,汇编指令和最终的机器码一一对应,也就可以把汇编代码和CPU指令执行过程对应起来。所以分析vTaskSuspendAll()中++uxSchedulerSuspended的处理,和可能被打断的情况,最好将其转换为汇编代码分析。在risc架构中,不论arm或者risc-v,对应内存中的变量修改,汇编的操作大致可以写为(不是真正的汇编代码,汇编伪代码):
load rx, addr //从地址addr加载数据到寄存器rx
add rx,rx,1 //寄存器rx的值+1
store rx,addr //将寄存器rx的值写回到地址addr 的内存中
总结起来,基本流程:1. Load--从内存加载数据到寄存器;2.add--寄存器数值+1;3.store--将+1后的寄存器值写回内存。
vTaskSuspendAll()函数源码如下,uxSchedulerSuspended根据32位或者64位cpu,为volatile uint32_t和uint64_t类型:
下面是arm cortex-m3和risc-v rv32对应的vTaskSuspendAll()函数反汇编代码:
可以看到,也是符合上面的Load -> Add -> Store流程的。
vTaskSuspendAll()被打断的几种情况分析
vTaskSuspendAll()函数没有关中断,也就是上面3个流程任意时刻都可能被中断,进而可能切换任务,假设两个任务TaskA和TaskB,代码如下:
void TaskA( void *pvParameters )
{
while(1)
{
...
vTaskSuspendAll();
//do something critical
...
xTaskResumeAll();
...
}
}
void TaskB( void *pvParameters )
{
while(1)
{
...
vTaskSuspendAll();
//do something critical
...
xTaskResumeAll();
...
}
}
代码先关闭调度器,之后进入零界区,做一些不希望被其它任务打断的工作(可以被中断打断,中断不访问该零界区),之后恢复调度器。假设TaskA运行,在调用vTaskSuspendAll()时被打断,可以分以下几种情况:
1. 在load之前触发中断切换任务,显然这样没有影响,因为零界区的工作还未开始。
2. 在store之后触发中断,中断处理中检查到uxSchedulerSuspended不为0,调度器被挂起,不会切换任务也没有影响。
3. 在load之后和store之前被打断(可能在add之前也可能add之后),此时TaskA load到的值一定是0(如果load到1,说明此前TaskB已经关闭了调度器(store执行完),在TaskB打开调度器,uxSchedulerSuspended变回0之前,不可能切换到任务TaskA;当然还有可能TaskA嵌套调用vTaskSuspendAll(),但这种情况产生中段也不会切换任务没有影响),此时进入中断处理,由于TaskA还没有完成Store,中断中load到的uxSchedulerSuspended依然为0,然后可能切换到任务TaskB,TaskB中load到的uxSchedulerSuspended依然为0,可以继续运行并通过store将uxSchedulerSuspended改为1,此时如果切换回任务TaskA将会出现++uxSchedulerSuspended两次,uxSchedulerSuspended的值为1的情况有点问题,但实际上这种情况不可能发生,应为在TaskB调用xTaskResumeAll()把uxSchedulerSuspended改为0之前,不可能切换任务,也就不可能回到TaskA。也就是等在次回到TaskA时uxSchedulerSuspended一定为0,那么接着执行add和store uxSchedulerSuspended为1并没有错误。
总结vTaskSuspendAll函数修改uxSchedulerSuspended完成之前被打断,此时T内存中的uxSchedulerSuspended为0,任务TaskA还没有开始执行零界区的代码,此时即使切换任务到TaskB,并且TaskB将uxSchedulerSuspended改为1,但是等切换回任务TaskA时,内存中uxSchedulerSuspended肯定还是0,TaskA将继续store或者add后store 1到uxSchedulerSuspended依然是正确的,接着TaskA继续执行临界区代码,整个运行过程保证了对uxSchedulerSuspended的正确修改,也保证临界区代码不会被其它任务打断。所以vTaskSuspendAll()中不关中断,也没有影响。