FreeRTOS任务调度研究

本文深入探讨FreeRTOS的任务调度机制,包括任务上下文切换原理、任务调度过程及TickHandler的作用。介绍了ARM Cortex-A9平台上的移植案例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这篇文章不介绍FreeRTOS移植,只是最近针对多核ARM Cortex系列平台做了移植后的一篇总结研究文章。所以不涉及对FreeRTOS整体的介绍,而只是分析任务调度这一块的机制。对应的Demo参考自CORTEX_A9_Zynq_ZC702。

一、触发任务调度的两种机制

  1. taskYIELD()
    这种机制其实是通过ARM swi 异常触发task context switch。
  2. IRQ handler
    通过tick中断,来检测是否需要进行任务切换。

最终两者任务切换的方式过程都是一样的,以taskYIELD()为例,代码如下:

/******************************************************************************
 * SVC handler is used to start the scheduler.
 *****************************************************************************/
.align 4
.type FreeRTOS_SWI_Handler, %function
FreeRTOS_SWI_Handler:
    /* Save the context of the current task and select a new task to run. */
    portSAVE_CONTEXT
    LDR R0, vTaskSwitchContextConst
    BLX R0
    portRESTORE_CONTEXT

ps.这是一个weak函数,可以自行重定义。

二、任务调度过程

总的来说,任务调度包含三个部分:保存当前任务现场;选择下一个执行的任务;恢复任务现场。

FreeRTOS保护的现场都通过system模式进行访问,针对每个任务,在其创建的时候都会在栈内开辟一段固定大小的空间用来保存之前任务的上下文(包括CPU状态,运行栈,内部寄存器)。

为什么是system mode? 因为system mode和usr mode看到的是一份寄存器内容。如下图所示

这里写图片描述

能反应usr模式下的运行状态,又不会影响其他特权模式。

而且system mode会进到PL1 state

这里写图片描述

2.1 恢复现场(通常也是触发执行第一个task的方式)

.macro portRESTORE_CONTEXT

    /* Set the SP to point to the stack of the task being restored. */
    LDR     R0, pxCurrentTCBConst  // 当前要恢复的任务对应的TCB
    LDR     R1, [R0]
    LDR     SP, [R1]    // TCB的第一个成员(pxTopOfStack),创建任务的时候根据A系列上下文内容计算得到,恢复的时候通过这个SP来发现保存的上下文,注意:这个时候是system mode。

    // 下面就是需要恢复的上下文,和创建任务时保存的顺序相反(push/pop)。
    /* Is there a floating point context to restore?  If the restored
    ulPortTaskHasFPUContext is zero then no. */
    LDR     R0, ulPortTaskHasFPUContextConst
    POP     {R1}
    STR     R1, [R0]
    CMP     R1, #0

    /* Restore the floating point context, if any. */
    POPNE   {R0}
    VPOPNE  {D16-D31}
    VPOPNE  {D0-D15}
    VMSRNE  FPSCR, R0

    /* Restore the critical section nesting depth. */
    LDR     R0, ulCriticalNestingConst
    POP     {R1}
    STR     R1, [R0]

    /* Ensure the priority mask is correct for the critical nesting depth. */
    LDR     R2, ulICCPMRConst
    LDR     R2, [R2]
    CMP     R1, #0
    MOVEQ   R4, #255
    LDRNE   R4, ulMaxAPIPriorityMaskConst
    LDRNE   R4, [R4]
    STR     R4, [R2]

    /* Restore all system mode registers other than the SP (which is already
    being used). */
    POP     {R0-R12, R14}

    /* Return to the task code, loading CPSR on the way. */
    RFEIA   sp!          // 执行完这条指定之后system mode下的sp重新指向了真正的栈顶,然后CPU回到任务lr指定的位置(初始值为任务对应的函数入口)。

    .endm

在创建完任务,调用vTaskStartScheduler()之后就是通过这个宏进到第一个任务的。

2.2 保存现场

和恢复现场相比较,保存现场就是要把各种状态放到SP(pxTopOfStack)对应的地方上。代码如下:

.macro portSAVE_CONTEXT

    /* Save the LR and SPSR onto the system mode stack before switching to
    system mode to save the remaining system mode registers. */
    SRSDB   sp!, #SYS_MODE   // 先把lr和spsr入栈到system mode的栈中
    CPS     #SYS_MODE        // 切到system mode
    PUSH    {R0-R12, R14}    // 和恢复现场的顺序对应,寄存器入栈

    /* Push the critical nesting count. */
    LDR     R2, ulCriticalNestingConst
    LDR     R1, [R2]
    PUSH    {R1}

    /* Does the task have a floating point context that needs saving?  If
    ulPortTaskHasFPUContext is 0 then no. */
    LDR     R2, ulPortTaskHasFPUContextConst
    LDR     R3, [R2]
    CMP     R3, #0

    /* Save the floating point context, if any. */
    FMRXNE  R1,  FPSCR
    VPUSHNE {D0-D15}
    VPUSHNE {D16-D31}
    PUSHNE  {R1}

    /* Save ulPortTaskHasFPUContext itself. */
    PUSH    {R3}

    /* Save the stack pointer in the TCB. */
    LDR     R0, pxCurrentTCBConst
    LDR     R1, [R0]
    STR     SP, [R1]            // 这个时候的sp还是system mode下的sp,所以上下文内容都是在system mode下进行保存和切换的。

    .endm

将save context和restore context结合起来看,任务上下文的切换始终发生在system mode,即能改变user mode下的运行栈,又不会破坏其他特权级别(诸如svc,irq,fiq,hyp)下的运行栈。是一个实质上介于特权级别和用户级别的一个中间层级。

2.3 任务切换

任务切换方法由vTaskSwitchContext提供,负责从当前的ready list中选择一个优先级最高的任务。保存到pxCurrentTCB中。

至此,整个FreeRTOS的调度程序就工作起来了。

三、Tick Handler

前面讲到,触发FreeRTOS调度的有两种机制,一是swi handler,二是irq handler,准确的讲是tick handler。

一般针对每一个平台的port,都会实现一个对应的FreeRTOS_Tick_Handler,以Cortex A9为例,其位于Source/portable/GCC/ARM_CA9/port.c中。

void FreeRTOS_Tick_Handler( void )
{
    /* Set interrupt mask before altering scheduler structures.   The tick
    handler runs at the lowest priority, so interrupts cannot already be masked,
    so there is no need to save and restore the current mask value.  It is
    necessary to turn off interrupts in the CPU itself while the ICCPMR is being
    updated. */
    portCPU_IRQ_DISABLE();
    portICCPMR_PRIORITY_MASK_REGISTER = ( uint32_t ) ( configMAX_API_CALL_INTERRUPT_PRIORITY << portPRIORITY_SHIFT );
    __asm volatile (    "dsb        \n"
                        "isb        \n" );
    portCPU_IRQ_ENABLE();

    /* Increment the RTOS tick. */
    if( xTaskIncrementTick() != pdFALSE )
    {
        ulPortYieldRequired = pdTRUE;
    }

    /* Ensure all interrupt priorities are active again. */
    portCLEAR_INTERRUPT_MASK();
    configCLEAR_TICK_INTERRUPT();
}

里面主要就是一个函数:xTaskIncrementTick()。这个函数位于Source/tasks.c。

BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;

    /* Called by the portable layer each time a tick interrupt occurs.
    Increments the tick then checks to see if the new tick value will cause any
    tasks to be unblocked. */
    traceTASK_INCREMENT_TICK( xTickCount );
    if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
    {
        /* Minor optimisation.  The tick count cannot change in this
        block. */
        const TickType_t xConstTickCount = xTickCount + 1;

        /* Increment the RTOS tick, switching the delayed and overflowed
        delayed lists if it wraps to 0. */
        xTickCount = xConstTickCount;

        if( xConstTickCount == ( TickType_t ) 0U )
        {
            taskSWITCH_DELAYED_LISTS();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }

        /* See if this tick has made a timeout expire.  Tasks are stored in
        the queue in the order of their wake time - meaning once one task
        has been found whose block time has not expired there is no need to
        look any further down the list. */
        if( xConstTickCount >= xNextTaskUnblockTime )
        {
            for( ;; )
            {
                if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
                {
                    /* The delayed list is empty.  Set xNextTaskUnblockTime
                    to the maximum possible value so it is extremely
                    unlikely that the
                    if( xTickCount >= xNextTaskUnblockTime ) test will pass
                    next time through. */
                    xNextTaskUnblockTime = portMAX_DELAY; /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
                    break;
                }
                else
                {
                    /* The delayed list is not empty, get the value of the
                    item at the head of the delayed list.  This is the time
                    at which the task at the head of the delayed list must
                    be removed from the Blocked state. */
                    pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
                    xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );

                    if( xConstTickCount < xItemValue )
                    {
                        /* It is not time to unblock this item yet, but the
                        item value is the time at which the task at the head
                        of the blocked list must be removed from the Blocked
                        state - so record the item value in
                        xNextTaskUnblockTime. */
                        xNextTaskUnblockTime = xItemValue;
                        break;
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }

                    /* It is time to remove the item from the Blocked state. */
                    ( void ) uxListRemove( &( pxTCB->xStateListItem ) );

                    /* Is the task waiting on an event also?  If so remove
                    it from the event list. */
                    if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
                    {
                        ( void ) uxListRemove( &( pxTCB->xEventListItem ) );
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }

                    /* Place the unblocked task into the appropriate ready
                    list. */
                    prvAddTaskToReadyList( pxTCB );

                    /* A task being unblocked cannot cause an immediate
                    context switch if preemption is turned off. */
                    #if (  configUSE_PREEMPTION == 1 )
                    {
                        /* Preemption is on, but a context switch should
                        only be performed if the unblocked task has a
                        priority that is equal to or higher than the
                        currently executing task. */
                        if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
                        {
                            xSwitchRequired = pdTRUE;
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                    #endif /* configUSE_PREEMPTION */
                }
            }
        }

        /* Tasks of equal priority to the currently running task will share
        processing time (time slice) if preemption is on, and the application
        writer has not explicitly turned time slicing off. */
        #if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
        {
            if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
            {
                xSwitchRequired = pdTRUE;
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        #endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */

        #if ( configUSE_TICK_HOOK == 1 )
        {
            /* Guard against the tick hook being called when the pended tick
            count is being unwound (when the scheduler is being unlocked). */
            if( uxPendedTicks == ( UBaseType_t ) 0U )
            {
                vApplicationTickHook();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        #endif /* configUSE_TICK_HOOK */
    }
    else
    {
        ++uxPendedTicks;

        /* The tick hook gets called at regular intervals, even if the
        scheduler is locked. */
        #if ( configUSE_TICK_HOOK == 1 )
        {
            vApplicationTickHook();
        }
        #endif
    }

    #if ( configUSE_PREEMPTION == 1 )
    {
        if( xYieldPending != pdFALSE )
        {
            xSwitchRequired = pdTRUE;
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    #endif /* configUSE_PREEMPTION */

    return xSwitchRequired;
}

总的来讲,xTaskIncrementTick负责更新block list和ready list。然后设置标志位xSwitchRequired,告诉调度器是否需要进行任务切换。

整个函数分为两个分支: uxSchedulerSuspended == ( UBaseType_t ) pdFALSE 和 uxSchedulerSuspended != ( UBaseType_t ) pdFALSE 。

如果此时scheduler已经挂起的话,什么都不做。否则,做如下动作:

  1. 判断当前tick是否大于xNextTaskUnblockTime(初始值为0),如果大于,则需要进一步判断pxDelayedTaskList中的哪些task需要进入到readyList。
  2. 取出pxDelayedTaskList中的最小unblock time(xItemValue)的任务,如果当前tick小于该最小值(表示没有任务满足unblock需求),那么设置xNextTaskUnblockTime为xItemValue,然后退出。
  3. 否则的话,通过( void ) uxListRemove( &( pxTCB->xStateListItem ) );将该task从block状态移除。紧接着判断是不是在等待event,如果是的话,也从event list移除。
  4. 把该任务加到readyList中去。等待调度。判断是否支持preemption,如果支持的话,置xSwitchRequired。
### FreeRTOS任务调度的工作原理 FreeRTOS任务调度主要通过任务管理器来完成,该模块负责任务的创建、执行、切换以及销毁等操作。对于初学者来说,可以通过理解裸机实现中的软件定时器逻辑来间接掌握其工作流程[^1]。 #### 时间切片的概念 在 Cortex-M 内核上运行的 FreeRTOS 使用了“时间切片”的概念。“时间切片”指的是调度程序会在每次系统中断触发时,在具有相同优先级的任务间进行轮转。这种机制使得多个同优先级的任务能够共享 CPU 资源[^2]。 #### 可抢占式调度机制 FreeRTOS 支持可抢占式的调度机制,这意味着高优先级的任务可以打断低优先级任务的执行并立即获得控制权。当一个更高优先级的任务变为就绪态时,当前正在运行的任务会被挂起,而新任务则被激活并投入运行。此过程通常由硬件中断驱动,例如 RISC-V 架构下的时间中断或 ecall 调用[^3]。 #### 任务状态切换 任务的状态可以在以下几个阶段之间转换:休眠(Suspended)、就绪(Ready)、运行(Running) 阻塞(Blocked) 或延迟(Delayed)。这些状态的变化依赖于特定事件的发生,比如计时器超时或者信号量释放等条件满足后引起的状态迁移。 #### 底层实现细节 (以RISC-V为例) 为了更好地理解分析 FreeRTOS任务调度机制,建议深入研究它的链表结构及其相关算法,因为这直接影响到如何高效地管理遍历各个任务节点。另外需要注意的是不同处理器架构下虽然具体指令集有所差异但总体设计思路保持一致。 以下是简化版伪代码展示了一个典型场景——当发生一次周期性滴答中断(Tick Interrupt),如果存在更高级别的待处理请求,则会发生上下文切换: ```c void vPortYieldFromTick(void){ uint32_t ulTopOfStack; /* 获取最高优先级任务堆栈指针 */ portGET_HIGHEST_PRIORITY_TASK_STACK_POINTER(&ulTopOfStack); /* 判断是否有其他需要被执行的新任务 */ if(pxCurrentTCB != pxHighestPriorityTaskToRun ){ /* 执行实际的情境保存与恢复动作 */ portSWITCH_CONTEXT(); /* 更新全局变量指向新的当前任务控制块*/ pxCurrentTCB = pxHighestPriorityTaskToRun ; } } ``` 上述函数展示了在一个典型的滴答中断服务例程内部可能发生的部分逻辑片段之一即判断是否应该发起一次完整的环境交换从而让另一个更重要的进程得以继续推进下去^. ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叶玄青

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值