任务上下文空间
在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
中定义了任务相关的控制信息,包括:
静态信息,包括
pcTaskName
和uxPriority
,在任务创建时赋值。-
运行态信息:
堆栈信息 ,包括
pxStack
和pxTopOfStack
,描述了当前代码堆栈的基址和栈顶地址。状态链表:
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系统中msp
和psp
的差别,可查询相关文档。FreeRTOS中,psp
用于用户程序,msp
用于系统中断。任务堆栈的切换,实际是psp
指针的切换。
如上一节所述,跳出中断后,系统会跳回psp
中保存的中断点。而psp
已经被更新成新任务的堆栈地址,因此会跳回当前任务的挂起点,而不是之前进入中断的点。