[逆解] FreeRTOS 3 - 任务上下文

任务上下文空间

xTaskCreate中,为每个任务分配了一个TCB_t结构和一个堆栈空间,这就是任务需要维护的主要上下文信息。

/* Allocate space for the TCB.  Where the memory comes from depends on
 * the implementation of the port malloc function and whether or not static
 * allocation is being used. */
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );

if( pxNewTCB != NULL )
{
    /* Allocate space for the stack used by the task being created.
                       * The base of the stack memory stored in the TCB so the task can
                       * be deleted later if required. */
    pxNewTCB->pxStack = ( StackType_t * ) pvPortMallocStack( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
    
  // ...
}

绝大部分嵌入式系统没有虚拟地址的概念,FreeRTOS里的Task没有自己的地址空间表。从操作系统的概念来说,FreeRTOS的Task更像是线程,而不是进程。但无论线程还是进程,都需要维护自身的代码堆栈,保证函数调用结构的完整。

任务TCB

TCB_t中定义了任务相关的控制信息,包括:

  1. 静态信息,包括pcTaskNameuxPriority,在任务创建时赋值。

  2. 运行态信息:

    • 堆栈信息 ,包括pxStackpxTopOfStack,描述了当前代码堆栈的基址和栈顶地址。

    • 状态链表:xStateListItem,用于将任务串进响应的状态链表中。

    • 事件信息:xEventListItem,当任务挂起时,将任务串进等待的事件链表中去。

定义如下 (略去部分次要代码):

/*
 * Task control block.  A task control block (TCB) is allocated for each task,
 * and stores task state information, including a pointer to the task's context
 * (the task's run time environment, including register values)
 */
typedef struct tskTaskControlBlock       /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
    volatile StackType_t * pxTopOfStack; /*< Points to the location of the last item placed on the tasks stack.  THIS MUST BE THE FIRST MEMBER OF THE TCB STRUCT. */

    // ...
  
    ListItem_t xStateListItem;                  /*< The list that the state list item of a task is reference from denotes the state of that task (Ready, Blocked, Suspended ). */
    ListItem_t xEventListItem;                  /*< Used to reference a task from an event list. */
    UBaseType_t uxPriority;                     /*< The priority of the task.  0 is the lowest priority. */
    StackType_t * pxStack;                      /*< Points to the start of the stack. */
    char pcTaskName[ configMAX_TASK_NAME_LEN ]; /*< Descriptive name given to the task when created.  Facilitates debugging only. */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */

    // ...
} tskTCB;

/* The old tskTCB name is maintained above then typedefed to the new TCB_t name
 * below to enable the use of older kernel aware debuggers. */
typedef tskTCB TCB_t;

所有和任务相关的操作都需要TCB_t的帮助。

任务堆栈

任务堆栈保存了当前任务执行过程中的函数返回地址和本地变量。当任务切换的时候,必须正确切换堆栈地址,以保证当前任务能恢复本地变量值,以及能正确找到上一次的中断点。

FreeRTOS中可根据编译器的设定,定义堆栈的生长方向portSTACK_GROWTH。在xTaskCreate中都有体现。

堆栈的内容由编译器确定,但基本包括函数内定义的本地变量,返回值及返回地址。任务被挂起的时候,由于是一个中断,在进中断前,会在任务堆栈中保存挂起点的地址。任务切换完成后,中断退出,系统会从新堆栈中恢复新任务的挂起点。也就是说,堆栈切换后,中断服务程序将返回到新任务的上一个挂起点,继续执行新任务代码。

上下文切换过程

任务上下文的切换在xPortPendSVHandler中完成。这是一个平台相关的中断响应函数,一般用汇编代码编写。下列是ARM CM3的汇编实现。

    .align 4
xPortPendSVHandler: .asmfunc
    ;
  ; Step 1: 将寄存器信息(r4-r11)保存在上一个任务的堆栈(psp)中,并更新TCB中的栈顶地址
  ;
  
    ; 读取旧堆栈地址到r0
    mrs r0, psp
    isb
    ;/* Get the location of the current TCB. */
    ; 读取pxCurrentTCB地址到r3,此时指向上旧任务的TCB
    ldr r3, pxCurrentTCBConst
    ; 读取旧TCB地址到r2
    ldr r2, [r3]
    ;/* Save the core registers. */
    ; 将r4-r11保存到旧堆栈中
    stmdb r0!, {r4-r11}
    ;/* Save the new top of stack into the first member of the TCB. */
    ; 将当前堆栈顶地址保存在当前TCB_t的pxTopOfStack属性中
    str r0, [r2]

  ;
  ; Step 2: 调用vTaskSwitchContext函数,选出新Task,更新pxCurrentTCB指针
  ;
  
    ; 将r3(pxCurrentTCB), r14(中断点)压栈,注意,这里的sp是是msp,和前面的psp区隔
    stmdb sp!, {r3, r14}
    ; {
      ; 屏蔽中断
      ldr r0, ulMaxSyscallInterruptPriorityConst
      ldr r1, [r0]
      msr basepri, r1
      ; 数据指令隔离
      dsb
      isb
      ; {
          ; 调用vTaskSwitchContext函数
          ; 调用之后,pxCurrentTCB指向新TCB
          bl vTaskSwitchContext
      ; }
      ; 恢复中断
      mov r0, #0
      msr basepri, r0
  ; }
    ; 将r3(pxCurrentTCB), r14(中断点)退栈
    ldmia sp!, {r3, r14}

  ;
  ; Step3: 将新堆栈地址从新任务的TCB中取出,恢复psp,并从新堆栈中恢复r4-r11
  ;
  
    ;/* The first item in pxCurrentTCB is the task top of stack. */
    ; 将新堆栈顶地址读取到r0中
    ; 注意,这里r3指向pxCurrentTCB,已经在vTaskSwitchContext中指向了新的TCB
    ldr r1, [r3]
    ldr r0, [r1]
    ;/* Pop the core registers. */
    ; 恢复r4-r11
    ldmia r0!, {r4-r11}
    ; 将新栈地址赋值给psp,正式完成堆栈切换
    msr psp, r0
    isb
    ; 跳出中断
    bx r14
    .endasmfunc

vTaskSwitchContext是任务调度的核心,在任务调度一章中详述。

注意ARM系统中msppsp的差别,可查询相关文档。FreeRTOS中,psp用于用户程序,msp用于系统中断。任务堆栈的切换,实际是psp指针的切换。

如上一节所述,跳出中断后,系统会跳回psp中保存的中断点。而psp已经被更新成新任务的堆栈地址,因此会跳回当前任务的挂起点,而不是之前进入中断的点。

你可能感兴趣的:([逆解] FreeRTOS 3 - 任务上下文)