FreeRTOS学习 任务调度

任务调度

任务调度实现在多个任务之间轮流使用CPU,他的主要工作分为三个阶段:

  • 保存当前任务上下文到任务栈
  • 选择新任务
  • 恢复新任务的上下文

这三个步骤需要在中断服务函数中执行,所以要求执行的速度要快,所以任务选择策略需要满足快的要求,同时使用汇编代码编写。 任务调度器的具体实现与硬件架构相关,所以需要具备一定的CPU知识。本文以Cortex-M3架构为例。

一、任务运行环境

任务运行环境、也就是任务上下文,当我们说任务获得CPU时,究竟发生了什么?

CPU运行时,是通过不断的读取指令、数据到CPU的寄存器、然后执行指令处理数据。而任务与任务之间的区别就是代码和数据不同,所以任务获得CPU,本质上就是将任务的代码、数据复制到CPU的寄存器。而任务的代码、数据就是任务的上下文。

以Cortex-M3为例,CPU的寄存器如图所示:
FreeRTOS学习 任务调度

堆栈寄存器

Cortex M3有两个堆栈指针,但是在同一时刻只能使用一个。分别是:

  • MSP:主堆栈指针(默认)用于操作系统内核、异常服务例程、需要特权级别访问的应用代码
  • PSP:用户进程使用的指针。

连接寄存器LR

存储了函数调用的返回地址。配合BL使用:

BL fuck

程序跳转到fuck函数执行,且LR寄存器自动存储了fuck函数返回后要执行的下一条指令。

程序计数器PC

PC寄存器的值是的下一条指令的地址。可用于改变程序执行顺序。

特殊功能寄存器

是CM3的特点,这些寄存器只能通过MSR/MRS指令访问。临界区进入or退出就是通过开关中断实现。

  • 程序状态字寄存器PSRs:记录运算模块ALU的标志(进位、借位、符号以及中断号)
  • 中断屏蔽寄存器PRIMASK:开关所有可屏蔽中断
  • 中断屏蔽寄存器FAULTMASK:开关所有中断(除了NMI)
  • 中断屏蔽寄存器BASEPRI:当被设置为x时,优先级大于x的中断被屏蔽
  • 控制寄存器:定义当前的特权级别、使用的堆栈指针。

二、触发任务调度

操作系统必须提供一个接口,来实现任务调度。通常会使用软件中断来实现任务调度,在Cortex-M3中,在PendSV中断服务函数中实现任务调度。

PendSV

在systick中断会执行上下文切换,这时就可以在systick中调用PendSV,由于PendSV是一个可挂起的软件中断,等到systick中断退出后,会进入PendSV中断,在PendSV中断中进行上下文切换。其工作如图:

FreeRTOS学习 任务调度

中断过程

当Cortex-M3开始响应一个中断时,首先会由硬件自动保存寄存器xPSR ,PC,LR,R12,R3,R2,R1,R0到当前使用的栈中(PSP or MSP)。随后进入从中断向量表找到中断服务程序入口执行(在中断中一直使用MSP)。

退出中断时,将从进入服务函数前的栈中,将寄存器xPSR ,PC,LR,R12,R3,R2,R1,R0依次恢复。

三、任务调度

  • 当触发任务调度时,CPU检测到PendSV中断产生,于是由硬件将xPSR ,PC,LR,R12,R3,R2,R1,R0寄存器的值保存到当前的任务栈中,然后进入xPortPendSVHandler函数,先获取任务栈顶指针,将R11,R10,R9,R8,R7,R6,R5,R4寄存器的值压入任务栈中。

  • 通过vTaskSwitchContext函数选择一个新的任务。

  • 获取新的任务栈顶指针,通过栈顶指针恢复R11,R10,R9,R8,R7,R6,R5,R4寄存器的值,重新设置PSP为新任务栈顶指针,退出中断服务函数;硬件将自动从新任务栈中恢复R11,R10,R9,R8,R7,R6,R5,R4寄存器的值,从而实现了上下文恢复。

/**
 * PendSV中断服务函数
 * 任务上下文切换 
 */
__asm void xPortPendSVHandler( void )
{
    extern uxCriticalNesting;
    extern pxCurrentTCB;    /* pxCurrentTCB是一个指向当前任务TCB的指针 */
    extern vTaskSwitchContext;

    PRESERVE8

    /* 保存上下文 */
    mrs r0, psp                 //将psp保存到r0(psp栈顶指针)
    isb

    ldr r3, =pxCurrentTCB       //将pxCurrentTCB的地址保存到r3
    ldr r2, [ r3 ]              //读取pxCurrentTCB的值到r2,即当前任务TCB的地址,也是任务栈顶指针的地址

    stmdb r0 !, { r4 - r11 }    //以栈顶地址(r0)为基地址,将r4-r11寄存器的数据压入任务栈中,r0为新的任务栈顶地址
    str r0, [ r2 ]              //将r0(新的任务栈顶地址)保存到 “以r2的值为地址” 的内存上。(r2的值就是任务栈顶指针的地址)。这个操作就是更新任务控制块的栈顶指针成员。

    /* 此时r3保存的是pxCurrentTCB的地址,r14保存返回地址 */
    stmdb sp !, { r3, r14 }     //将r3、r14的值压入栈保护,后续调用vTaskSwitchContext时、r3、r14会被修改

    /* 选择新任务 */
    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY   //通过设置basepri寄存器屏蔽中断
    msr basepri, r0
    dsb
    isb
    bl vTaskSwitchContext       //跳转到vTaskSwitchContext执行

    /* 恢复上下文 */
    /* 从vTaskSwitchContext返回后、pxCurrentTCB的值就修改成下一个任务的tcb */
    mov r0, #0                  //打开中断
    msr basepri, r0
    ldmia sp !, { r3, r14 }     //恢复r3 r14的寄存器

    ldr r1, [ r3 ]              //读取pxCurrentTCB的值到r1,即新任务TCB的地址,也是新任务栈顶指针的地址
    ldr r0, [ r1 ]              //读取新任务栈顶地址到r0
    ldmia r0 !, { r4 - r11 }    //以栈顶地址为基地址,将栈内向上增长的8个字数据压入r4-r11寄存器(恢复新任务的运行环境)
    msr psp, r0                 //将新的栈顶地址保存到psp寄存器
    isb
    bx r14                      //退出中断
    nop
/* *INDENT-ON* */
}

整个过程中、任务栈内的数据内容大致如图所示:

FreeRTOS学习 任务调度

四、选择新任务

freertos提供通用的任务选择方法、也提供了与具体硬件相关的任务选择方法,可以通过宏configUSE_PORT_OPTIMISED_TASK_SELECTION来开启or关闭该功能。通过一个全局变量pxCurrentTCB来指向新任务的TCB。

freertos的每一个优先级都有一个就绪链表、通过数组将这些链表组织起来。数组的下标正好对应优先级,如图:

1、通用方式

当configUSE_PORT_OPTIMISED_TASK_SELECTION为0时,使用通用的任务选择方式,即选择 就绪任务 中优先级最高的任务。

这样就保证优先级高的任务能一直得到执行,符合实时操作系统的要求。freertos通过宏taskSELECT_HIGHEST_PRIORITY_TASK()实现该功能。

//任务选择:选择就绪链表中,优先级最高的第一个任务
#define taskSELECT_HIGHEST_PRIORITY_TASK()                                              \
    {                                                                                   \
        UBaseType_t uxTopPriority = uxTopReadyPriority;                                 \
                                                                                        \
        /* 找出就绪链表数组中,优先级最高的链表 */                    					  \
        while (listLIST_IS_EMPTY(&(pxReadyTasksLists[uxTopPriority])))                  \
        {                                                                               \
            configASSERT(uxTopPriority);                                                \
            --uxTopPriority;                                                            \
        }                                                                               \
                                                                                        \
        /* 获取就绪链表第一个任务到pxCurrentTCB 记录uxTopReadyPriority */                 \
        listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &(pxReadyTasksLists[uxTopPriority])); \
        uxTopReadyPriority = uxTopPriority;                                             \
    }

2、硬件优化方式

部分硬件提供了一些特殊指令or寄存器来优化该过程,可通过portGET_HIGHEST_PRIORITY接口来实现。

#define taskSELECT_HIGHEST_PRIORITY_TASK()                                              \
    {                                                                                   \
        UBaseType_t uxTopPriority;                                                      \
                                                                                        \
        /* Find the highest priority list that contains ready tasks. */                 \
        portGET_HIGHEST_PRIORITY(uxTopPriority, uxTopReadyPriority);                    \
        configASSERT(listCURRENT_LIST_LENGTH(&(pxReadyTasksLists[uxTopPriority])) > 0); \
        listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &(pxReadyTasksLists[uxTopPriority])); \
    } /* taskSELECT_HIGHEST_PRIORITY_TASK() */

对于Cortex-M3内核,可使用前导零来加速计算:

//使用CM3的前导零指令(计算二进制数有多少个零在前面)、得到uxReadyPriorities第一个1出现的位置,就是最大优先级
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities )    uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
上一篇:第13部分- Linux ARM汇编 移位操作


下一篇:驱动原理和逻辑说明