Why does FreeRTOS need to lock when switching threads?

Hi everyone, I’m using an STM32F4xx (Cortex-M4, Armv7-M architecture).

While reading the Armv7-M Architecture Reference Manual on page 530, I came across the following section:

Use of SVCall and PendSV to avoid critical code regions
Context switching typically requires the processor to execute a critical region of code with interrupts disabled, to avoid context corruption of key data structures during the switch. This can be a severe constraint on system design and deterministic performance. Armv7-M can support context switching with no critical region, meaning the processor does not have to disable interrupts.
An Armv7-M usage model to avoid critical regions is:
• Configure both SVCall and PendSV with the same, lowest exception priority.
• Use SVCall for Supervisor Calls from threads.
• Use PendSV to handle context-critical work offloaded from the exception handlers, including work that might otherwise be handled by the SVCall handler.

Because SVCall and PendSV have the same execution priority they cannot preempt each other, therefore one must process to completion before the other starts. SVCall and PendSV exceptions are always enabled, meaning each executes at some point, when the processor has handled all other exceptions.
In addition, the associated exception handlers do not have to check whether they are returning to a process on exit with this usage model, as the PendSV
exception will occur when returning to a process.
This usage model always sets PendSV to pending to issue a context switch request. However, a system can use both SVCall and PendSV exceptions for context switching because they do not interfere with each other.

From this, I understand that it should be possible to perform thread switching without disabling interrupts.

However, in the FreeRTOS Cortex-M4 port.c file, I found the following code:

void xPortPendSVHandler( void )
{
    ......
        "   mov r0, %0   \n" //%0 is configMAX_SYSCALL_INTERRUPT_PRIORITY        
        "   msr basepri, r0                     \n"
        "   dsb                                 \n"
        "   isb                                 \n"
        "   bl vTaskSwitchContext               \n"
        "   mov r0, #0                          \n"
        "   msr basepri, r0                     \n"
    ....::"i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY )
}

It seems that FreeRTOS disables interrupts that require the use of the xxxxx_FromISR APIs by setting the BASEPRI register before calling vTaskSwitchContext.

I’m wondering if it’s possible to perform task switching with interrupts fully enabled. If so, how can this be achieved while avoiding race conditions or data consistency issues?

Thank you for your help!


---------------------------------------------------------------------------

UPDATE 1:
Hi RAC, richard-damon, and rtel,

Thank you for your replies. From your explanations, I now understand that FreeRTOS needs to disable interrupts that use xxxFromISR to ensure the integrity of thread-related data structures (which I understand primarily refers to the TCB structure).

However, according to ARM’s description:

“Use PendSV to handle context-critical work offloaded from the exception handlers, including work that might otherwise be handled by the SVCall handler.”

I am wondering if it would be possible for xxxFromISR to avoid directly accessing these data structures. For instance, when calling vTaskNotifyGiveFromISR within an interrupt, could it simply submit a “request” into a “queue” instead? All actual work (including accessing and modifying the TCB structure) could then be deferred and handled later in the PendSV handler.

Thank you for your insights!

This is a good question, thanks for bringing it up!

I believe there is a potential problem during the call to vTaskSwitchContext() as some functions ending on …FromISR may concurrently access the task lists that vTaskSwitchContext() consults with, ending in possible data corruption.

An empirical test would be extremly easy to implement, of course; simply remove the interrupt disable sequence and let a previously running system loop forever. :wink: Needless to say, that test system needs to be sufficiently complex and non deterministic.

I suppose it might make more sense if vTaskSwitchContext() internally used a critical section in the local areas that need that sort of protection. I suppose the one issue is that depending on the port, it might change the sort of critical section that needs to be used, as while many do it inside a “Pend” ISR like described for ARM, so can do this also at task level directily (and without nested ISRs, the ISR Critical section is a no-op).

Short answers written on my phone:

The lock prevents higher priority tasks accessing the same data structures, and the switch context function requires consistency across its call, not just sections of it.

While this might be theoretically possible, it is not the best design as it actually boils down to have one interrupt of lowest priority to do everything.