内核调度机制深度解析

RTOS(实时操作系统)是一个多任务系统,其设计哲学是将复杂的业务逻辑拆解为多个独立、解耦的任务模块。通过 RTOS 提供的同步与通信机制实现数据交换,不仅极大方便了后期维护,更核心的优势在于其实时性:当高优先级任务就绪时,它能立即打断低优先级任务夺取 CPU 执行权。

理解实时性: 在裸机环境中,main 函数中的代码必须顺序执行。即使发生了紧急事件,除非通过中断,否则程序必须按既定顺序跑完前面的流程。而 RTOS 允许系统在任务之间“横向跳转”,实现真正的按需分配。


1. 硬件基石:MSP 与 PSP 指针

在 ARM Cortex-M 架构中,存在两个独立的栈指针寄存器:MSP (Main Stack Pointer)PSP (Process Stack Pointer)

  • 设计初衷: 将“操作系统内核/中断服务”使用的栈与“普通任务”使用的栈进行物理隔离。

  • 分配规则: * MSP: 用于裸机环境、异常处理(ISRs)和内核逻辑。

  • PSP: 专门用于用户级任务线程。

  • 优势: 使用双栈指针可以防止某个任务的栈溢出直接破坏内核栈(MSP),提高了系统的健壮性和安全性。


2. 核心“三剑客”:SVC, PendSV 与 SysTick

RTOS 的运转依赖于三个关键的中断。FreeRTOS 为了跨平台兼容,在底层对其进行了重定义:

/* FreeRTOS 对 Cortex-M 中断处理函数的统一映射 */
#define vPortSVCHandler     SVC_Handler      // 系统调用中断:主要用于启动第一个任务
#define xPortPendSVHandler  PendSV_Handler   // 悬起系统调用中断:核心上下文切换地
#define xPortSysTickHandler SysTick_Handler  // 系统滴答中断:提供时间片动力


3. 启动调度器:从裸机到多任务

启动调度器是通过 vTaskStartScheduler() 完成的。它会创建空闲任务、初始化定时器任务。这里启动过程其实可以单独拿出来说的,不过和调度详细不大,暂时不管。

这里空闲任务和定时计数器任务是两个内核任务,我们知道FreeRTOS是可以根据每个任务的需求分配CPU时间的,如果当前所有的任务都在阻塞或者在delay的状态中,那么CPU就不需要工作,这个时候就会进入空闲任务中,但是空闲任务中不是什么都不做,也会做一些收尾工作,比如我们在上一章节提到的回收动态分配的任务内存,这部分就不多说,因为它和调度关系不是很大,留给读者自己深究一下它到底做了什么。

定时计数器任务,因为FreeRTOS其实会维护他自己的内核时间,所以我们可以用这个机制来实现一些周期定时的任务,和定时计数器一样,但是精度肯定是没有定时计数器高的,这个后面我们深入介绍一下,因为软件定时器机制就是通过这个实现的,基本原理就是,每个定时计数器函数都是一个回调函数,这个任务根据当前时间是否到了,执行这个回调函数,实现软件定时器的效果。

3.1 准备环境:prvStartFirstTask

在正式调度前,系统还是裸机状态。我们需要清空 MSP 并触发一次 SVC 中断。

__asm void prvStartFirstTask( void )
{
    PRESERVE8

    /* 1. 重置 MSP:将其指向向量表定义的初始位置 */
    ldr r0, =0xE000ED08 
    ldr r0, [ r0 ]
    ldr r0, [ r0 ]
    msr msp, r0

    /* 2. 环境清理:确保处于特权级并开启中断 */
    mov r0, #0
    msr control, r0 
    cpsie i 
    cpsie f
    dsb
    isb

    /* 3. 触发 SVC 中断:正式切入 RTOS 模式 */
    svc 0   
    nop
    nop
}

3.2 启动第一个任务:vPortSVCHandler

SVC 处理函数只执行一次。它的任务是:“伪装”一次异常返回,从而把预先在栈里准备好的任务环境“弹”进 CPU 寄存器。上一章中不是说了每个任务创建的时候会伪造一个现场,其实这里就已经开始起作用了。

__asm void vPortSVCHandler( void )
{
    PRESERVE8

    /* 获取 pxCurrentTCB,即当前要运行的任务 */
    ldr r3, =pxCurrentTCB 
    ldr r1, [ r3 ]  
    ldr r0, [ r1 ]  /* r0 现在是任务的栈顶指针 (pxTopOfStack) */

    /* 1. 手动出栈 R4-R11 和 R14 (EXC_RETURN) */
    /* 这些是创建任务时手工压入的“假现场” */
    ldmia r0!, {r4-r11,r14} 

    /* 2. 更新 PSP:剩下的寄存器 (R0-R3, PC等) 将由硬件自动处理 */
    msr psp, r0 
    isb
    mov r0, #0  
    msr basepri, r0

    /* 3. 利用 bx r14 触发硬件的“异常返回机制” */
    /* 处理器会自动从 PSP 弹出剩余的 PC, R0 等,跳转到任务入口 */
    bx r14  
}


4. 上下文切换:PendSV 机制

xPortPendSVHandler 是 RTOS 中最繁忙的函数,它负责在两个任务之间进行“现场保存”和“现场恢复”。

4.1 任务切换汇编分析

__asm void xPortPendSVHandler( void )
{
    extern uxCriticalNesting;
    extern pxCurrentTCB;
    extern vTaskSwitchContext;
    PRESERVE8

    /* --- 第一步:保存当前旧任务的现场 --- */
    mrs r0, psp           /* 获取当前任务的 PSP */
    isb
    ldr r3, =pxCurrentTCB 
    ldr r2, [ r3 ]        /* r2 = 当前 TCB 指针 */

    /* 如果使用了浮点单元,保存 FPU 寄存器 */
    tst r14, #0x10
    it eq
    vstmdbeq r0!, {s16-s31}

    /* 手动将 R4-R11 和 R14 压入任务栈 */
    stmdb r0!, {r4-r11, r14} 
    /* 将最新的栈顶地址保存回 TCB,以便下次找回 */
    str r0, [ r2 ] 

    /* --- 第二步:寻找下一个就绪任务 --- */
    stmdb sp!, {r0, r3}   /* 保护寄存器 */
    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
    msr basepri, r0       /* 关中断,进入调度临界区 */
    dsb
    isb
    bl vTaskSwitchContext /* 【关键 C 函数】:更新 pxCurrentTCB 指向新任务 */
    mov r0, #0
    msr basepri, r0       /* 开中断 */
    ldmia sp!, {r0, r3}

    /* --- 第三步:恢复新任务的现场 --- */
    ldr r1, [ r3 ]        /* 获取新任务的 TCB */
    ldr r0, [ r1 ]        /* 获取新任务的栈顶指针 pxTopOfStack */

    /* 从栈中弹出 R4-R11 和 R14 */
    ldmia r0!, {r4-r11, r14}

    /* 恢复 FPU 寄存器 */
    tst r14, #0x10
    it eq
    vldmiaeq r0!, {s16-s31}

    /* 将新任务的栈地址写到 PSP,硬件将从这里自动弹栈恢复 R0-R3, PC 等 */
    msr psp, r0 
    isb
    bx r14  /* 返回新任务 */
}


5. 核心调度算法逻辑

vTaskSwitchContext 被调用时,内核需要决定“谁是下一个”。

void vTaskSwitchContext( void )
{
    if( uxSchedulerSuspended != ( UBaseType_t ) 0U )
    {
        // 调度器都还没有启动
        xYieldPendings[ 0 ] = pdTRUE;
    }
    else
    {
        xYieldPendings[ 0 ] = pdFALSE;
        #if ( configUSE_POSIX_ERRNO == 1 )
        {
            pxCurrentTCB->iTaskErrno = FreeRTOS_errno;
        }
        #endif
        // 找优先级最高的第一个就绪任务
        taskSELECT_HIGHEST_PRIORITY_TASK();
        traceTASK_SWITCHED_IN();
        // 钩子函数机制
        portTASK_SWITCH_HOOK( pxCurrentTCB );
        #if ( configUSE_POSIX_ERRNO == 1 )
        {
            FreeRTOS_errno = pxCurrentTCB->iTaskErrno;
        }
        #endif
    }
}

5.1 优先级选择:taskSELECT_HIGHEST_PRIORITY_TASK

内核通过一个循环查找,找到当前优先级最高且非空的就绪任务链表。

#define taskSELECT_HIGHEST_PRIORITY_TASK()                                      \
{                                                                               \
    UBaseType_t uxTopPriority = uxTopReadyPriority;                             \
                                                                                \
    /* 从最高优先级向下查找,直到找到第一个有任务的就绪链表 */                        \
    while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )       \
    {                                                                           \
        --uxTopPriority;                                                        \
    }                                                                           \
                                                                                \
    /* 从该链表中获取一个 TCB (如果是多个同级任务,则通过时间片轮转选择) */            \
    listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
    uxTopReadyPriority = uxTopPriority;                                         \
}

uxTopReadyPriority这是一个全局变量,记录了当前最高优先级的就绪任务链表,他是一个会随着任务的恢复和创建变化的变量,所以这个宏定义中的查找其实是很快的。

5.2 时间片轮转:listGET_OWNER_OF_NEXT_ENTRY

对于同等优先级的任务,RTOS 通过移动链表的 pxIndex 指针,确保每个任务执行一个 Tick 时间,实现“雨露均沾”。

#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )                                \
{                                                                                   \
    List_t * const pxConstList = ( pxList );                                       \
    /* 移动链表索引到下一个节点 */                                                   \
    ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;1. 核心结论:位置是由“插入算法”决定的
    在 FreeRTOS 中,任务在就绪列表中并不是每次切换都要“出链表再入链表”。
    
    如果是高优先级抢占: 低优先级任务原地不动。它依然留在它那个优先级的链表里,位置没变。只是 pxCurrentTCB 指向了高优先级任务。
    
    如果是同优先级时间片轮转: 任务也没有重新插入,而是通过移动链表的 pxIndex 指针,指向当前链表中的下一个任务。
    
    真正的“加入到优先级链表”的函数是 prvAddTaskToReadyList()(这是一个宏,最终调用 vListInsertEnd())。
    
                        \
    /* 如果移到了末尾的尾节点,则跳过并指向头部的第一个任务 */                          \
    if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \
    {                                                                               \
        ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;                \
    }                                                                               \
    /* 获取该节点对应的任务控制块 (TCB) */                                            \
    ( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;                                  \
}

我们这里再做深入说明,就是如果一个任务是自己释放了CPU使用权或者因为什么原因阻塞,他会加入到延时/阻塞链表中,但是如果是高优先级任务打断,这个任务还在就绪链表中,不过因为( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;这句话,任务一旦被选中就会变成同等优先级下的最后一个调度的任务。如果是时间片也是一样的,如果一个任务执行完了,因为我的pxIndex向后移动了一个,我需要从后往前才可以找到它,也就是最后一个了。以上时间片调度和优先级调度的机制也都非常清晰了。


总结

FreeRTOS 的调度本质是“利用异常机制接管硬件”:

  1. SysTick 周期性地检查是否有更高优先级任务就绪或时间片是否耗尽。
  2. PendSV 负责苦力活,即保存旧现场、加载新现场。
  3. 优先级链表 则是调度器的“账本”,记录了所有任务的状态。

通过对这些汇编与底层 C 代码的分析,我们可以清晰地看到:RTOS 并不是什么魔法,而是通过精妙的硬件栈管理,实现了一场持续不断的“偷梁换柱”。

声明: 本篇文章内容由作者原创撰写,并使用了 AI 进行辅助润色与排版优化,文章核心技术观点及逻辑内容已由作者本人严格审核。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐