任务调度
任务调度实现在多个任务之间轮流使用CPU,他的主要工作分为三个阶段:
- 保存当前任务上下文到任务栈
- 选择新任务
- 恢复新任务的上下文
这三个步骤需要在中断服务函数中执行,所以要求执行的速度要快,所以任务选择策略需要满足快的要求,同时使用汇编代码编写。 任务调度器的具体实现与硬件架构相关,所以需要具备一定的CPU知识。本文以Cortex-M3架构为例。
一、任务运行环境
任务运行环境、也就是任务上下文,当我们说任务获得CPU时,究竟发生了什么?
CPU运行时,是通过不断的读取指令、数据到CPU的寄存器、然后执行指令处理数据。而任务与任务之间的区别就是代码和数据不同,所以任务获得CPU,本质上就是将任务的代码、数据复制到CPU的寄存器。而任务的代码、数据就是任务的上下文。
以Cortex-M3为例,CPU的寄存器如图所示:
堆栈寄存器
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中断中进行上下文切换。其工作如图:
中断过程
当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提供通用的任务选择方法、也提供了与具体硬件相关的任务选择方法,可以通过宏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 ) ) )