1. 垫话
从本文开始,正式进入对调度实现细节及底层的探讨。本文讨论 ARM mbed OS(RTX) 的上下文切换。解构调度器,按说不应该从“上下文切换”如此 meta 的细节入手。但从我个人角度来说,本文是一个回顾和总结,如我在《浅谈调度相关的元问题》一文所述,mbed OS 是一个支持分态的内核,其上下文切换实现的套路非常神似 linux,故而对 mbed OS 上下文切换的探讨有一定的推广意义。而且因为 ARM 没有历史包袱,相较于 x86 架构,其设计非常的 make sense,更加符合现代操作系统实现的直觉,尤其是 v7a。所以如果上下文切换本身是跳不过去的环节的话,基于 ARM 架构的 RTOS 去讲会更容易理解。
另外,ARM 的 v7m 架构隔离框架 uVisor,其底层适配的就是自家的 mbed OS。我对 ARM 代码的好感始自 uVisor,这是我读过最优秀的代码之一,有时间的话写一个 uVisor 解构系列文章。
2. 前言
本文解剖 mbed OS(下文简称 mbed)上下文切换实现的细节。mbed 是一个分态设计的内核,本文主要聚焦 ARM v7m 架构上 mbed 相关实现细节。
本文会对原代码进行精简甚至魔改,本文代码皆选择 v7m gcc 编译器下的实现。
本文需要一些 ARM v7m 架构的基础背景知识,但本文不会展开讨论 v7m 架构细节,相关内容推荐《cortex M3 权威指南》(下文简称 《权威指南》)。
3. 系统调用
既然分态,系统调用是必要的。
3.1 用户侧接口定义
类似 linux,mbed 的系统调用也是通过一套模板宏来定义。我们看一个典型的实现:
SVC0_3 (ThreadNew, osThreadId_t, osThreadFunc_t, void *, const osThreadAttr_t *)
#define SVC0_3(f,t,t1,t2,t3) \
__attribute__((always_inline)) \
__STATIC_INLINE t __svc##f (t1 a1, t2 a2, t3 a3) { \
SVC_ArgR(0,a1); \
SVC_ArgR(1,a2); \
SVC_ArgR(2,a3); \
SVC_ArgF(svcRtx##f); \
SVC_Call0(SVC_In3, SVC_Out1, SVC_CL0); \
return (t) __r0; \
}
#define SVC_RegF "r12"
#define SVC_ArgN(n) \
register uint32_t __r##n __ASM("r"#n)
#define SVC_ArgR(n,a) \
register uint32_t __r##n __ASM("r"#n) = (uint32_t)a
#define SVC_ArgF(f) \
register uint32_t __rf __ASM(SVC_RegF) = (uint32_t)f
#define SVC_In0 "r"(__rf)
#define SVC_In1 "r"(__rf),"r"(__r0)
#define SVC_In2 "r"(__rf),"r"(__r0),"r"(__r1)
#define SVC_In3 "r"(__rf),"r"(__r0),"r"(__r1),"r"(__r2)
#define SVC_In4 "r"(__rf),"r"(__r0),"r"(__r1),"r"(__r2),"r"(__r3)
#define SVC_Out0
#define SVC_Out1 "=r"(__r0)
#define SVC_CL0
#define SVC_CL1 "r1"
#define SVC_CL2 "r0","r1"
#ifndef __STATIC_INLINE
#define __STATIC_INLINE static inline
#endif
#define SVC_Call0(in, out, cl) \
__ASM volatile ("svc 0" : out : in : cl)
#ifndef __ASM
#define __ASM __asm
#endif
没必要搞清楚这组宏具体是怎么工作的,我们直接 gcc -E:
__attribute__((always_inline))
static inline osThreadId_t
__svcThreadNew (osThreadFunc_t a1, void * a2, const osThreadAttr_t * a3) {
register uint32_t __r0 __asm("r0") = (uint32_t)a1;
register uint32_t __r1 __asm("r1") = (uint32_t)a2;
register uint32_t __r2 __asm("r2") = (uint32_t)a3;
register uint32_t __rf __asm("r12") = (uint32_t)svcRtxThreadNew;
__asm volatile ("svc 0" : "=r"(__r0) : "r"(__rf),"r"(__r0),"r"(__r1),"r"(__r2) : );
return (osThreadId_t) __r0;
}
3.2 系统调用处理函数
SVC_Handler:
TST LR,#0x04
ITE EQ
MRSEQ R0,MSP
MRSNE R0,PSP
LDR R1,[R0,#24]
LDRB R1,[R1,#-2]
CBNZ R1,SVC_User
PUSH {R0,LR}
LDM R0,{R0-R3,R12}
BLX R12
POP {R12,LR}
STM R12,{R0-R1}
BX LR
3.3 用户侧接口调用
osThreadId_t osThreadNew (osThreadFunc_t func, void *argument, const osThreadAttr_t *attr) {
osThreadId_t thread_id;
if (IsIrqMode() || IsIrqMasked()) {
EvrRtxThreadError(NULL, (int32_t)osErrorISR);
thread_id = NULL;
} else {
thread_id = __svcThreadNew(func, argument, attr);
}
return thread_id;
}
4. 上下文
4.1 上下文初始化
线程上下文的初始化,在线程创建的内核侧接口,也就是 svcRtxThreadNew 中:
static osThreadId_t svcRtxThreadNew (osThreadFunc_t func, void *argument, const osThreadAttr_t *attr) {
... ...
ptr = (uint32_t *)thread->sp;
for (n = 0U; n != 13U; n++) {
ptr[n] = 0U;
}
ptr[13] = (uint32_t)osThreadExit;
ptr[14] = (uint32_t)func;
ptr[15] = xPSR_InitVal(
(bool_t)((osRtxConfig.flags & osRtxConfigPrivilegedMode) != 0U))
);
ptr[8] = (uint32_t)argument;
... ...
}
__STATIC_INLINE uint32_t xPSR_InitVal (bool_t privileged) {
uint32_t psr;
if (privileged)
psr = CPSR_MODE_SYSTEM;
else
psr = CPSR_MODE_USER;
return psr;
}
4.2 第一次上下文切换
在本文 3.2 节对系统调用处理函数的解析中,我们有意忽略了一个极其重要的点。因为在 3.2 节中,我们聚焦的是系统调用的实现。本节中,我们补充被忽略的事实:mbed 会在系统调用处理函数,调用完对应目标内核侧接口后,在返回用户态之前,做一次上下文切换。
mbed 内核的第一次上下文切换,也即将代码从内核代码,切入到第一个用户态程序中,是通过触发一次系统调用来实现的。没想到吧,大多数单特权级 RTOS 下不是这么实现的。
触发第一次系统调用的代码是 osKernelStart:
osStatus_t osKernelStart (void) {
osStatus_t status;
if (IsIrqMode() || IsIrqMasked()) {
EvrRtxKernelError((int32_t)osErrorISR);
status = osErrorISR;
} else {
status = __svcKernelStart();
}
return status;
}
static osStatus_t svcRtxKernelStart (void) {
.. ...
OS_Tick_Enable();
thread = osRtxThreadListGet(&osRtxInfo.thread.ready);
osRtxThreadSwitch(thread);
if ((osRtxConfig.flags & osRtxConfigPrivilegedMode) != 0U)
__set_CONTROL(0x02U);
else
__set_CONTROL(0x03U);
return osOK;
}
void osRtxThreadSwitch (os_thread_t *thread) {
thread->state = osRtxThreadRunning;
osRtxInfo.thread.run.next = thread;
}
4.3 上下文切换
3.2 节中,SVC 处理函数在调用完内核侧接口后就通过 BX LR 返回了,这是我们为了突出系统调用相关流程而故意进行了简化。实际上完整的 SVC 处理函数如下:
SVC_Handler:
TST LR,
ITE EQ
MRSEQ R0,MSP // Get MSP if return stack is MSP
MRSNE R0,PSP // Get PSP if return stack is PSP
LDR R1,[R0,
LDRB R1,[R1,
CBNZ R1,SVC_User // Branch if not SVC 0
PUSH {R0,LR} // Save SP and EXC_RETURN
LDM R0,{R0-R3,R12} // Load function parameters and address from stack
BLX R12 // Call service function
/* 第 11 行 PUSH 了调用者的栈 R0(SP)
* 这一行将调用者的栈 R0 POP 到 R12(SP) 中
*/
POP {R12,LR} // Restore SP and EXC_RETURN
STM R12,{R0-R1} // Store function return values
/* 以上代码同本文 3.2 节
* 以下代码在系统调用的返回路径上,处理上下文切换
*/
SVC_Context:
/* R1 = curr, R2 = next
* 如果 next(下一个要调度的线程)等于 curr(当前运行的线程),则无需上下文切换
* if next == curr: return
*/
LDR R3,=osRtxInfo+I_T_RUN_OFS // Load address of osRtxInfo.run
LDM R3,{R1,R2} // Load osRtxInfo.thread.run: curr & next
CMP R1,R2 // Check if thread switch is required
IT EQ
BXEQ LR // Exit when threads are the same
/* 如果 curr != NULL,则需要保存 curr 的上下文
* curr 为 NULL 的场景,有两类情况:
* 1. curr(正在运行的)线程被删除
* 2. 第一次上下文切换时,也就是 svcRtxKernelStart 时
*
* if curr != NULL: goto SVC_ContextSave
*/
CBNZ R1,SVC_ContextSave // Branch if running thread is not deleted
TST LR,#0x10 // Check if extended stack frame
BNE SVC_ContextSwitch
/* 第 17 行获取调用者(也即当前正在运行的线程)的栈,保存在 R12 中
* SVC_ContextSave 将当前上下文保存到以 R12 为 SP 的栈上
*/
SVC_ContextSave:
STMDB R12!,{R4-R11} // Save R4..R11
STR R12,[R1,#TCB_SP_OFS] // Store SP
STRB LR, [R1,#TCB_SF_OFS] // Store stack frame information
/* curr = next
*/
SVC_ContextSwitch:
STR R2,[R3] // osRtxInfo.thread.run: curr = next
/* R2 为 next,next 指向一个 osRtxThread_t
* R1 = (osRtxThread_t *)next->stack_frame(EXC_RETURN[7..0],《权威指南》9.6)
* R0 = (osRtxThread_t *)next->sp
* 从 R0(SP) 中恢复上下文,并切换入 next 线程
*/
SVC_ContextRestore:
LDRB R1,[R2,#TCB_SF_OFS] // Load stack frame information
LDR R0,[R2,#TCB_SP_OFS] // Load SP
ORR LR,R1,#0xFFFFFF00 // Set EXC_RETURN
LDMIA R0!,{R4-R11} // Restore R4..R11
MSR PSP,R0 // Set PSP
/* 从 SVC 处理函数返回
*/
SVC_Exit:
BX LR // Exit from handler
4.4 其他上下文切换点位
不仅系统调用返回用户态的点位上会做上下文切换,其他上下文切换点位有:
PendSV_Handler:
PUSH {R0,LR}
BL osRtxPendSV_Handler
POP {R0,LR}
MRS R12,PSP
B SVC_Context
SysTick_Handler:
PUSH {R0,LR}
BL osRtxTick_Handler
POP {R0,LR}
MRS R12,PSP
B SVC_Context
4.5 where is schedule()?
mbed 代码通篇没有一个命名类似 schedule 的函数,那 schedule() 对等位的逻辑是啥?
通过上文的分析,osRtxThreadSwitch 就是指示内核调度到目标线程(osRtxThreadSwitch 函数的入参),实际上,mbed 相关代码在调用此函数之前都会先通过 osRtxThreadListGet 从就绪队列上获取下一个需要被调度的线程(或其他类似逻辑)。类似:
// Switch to Ready Thread with highest Priority
thread = osRtxThreadListGet(&osRtxInfo.thread.ready);
osRtxThreadSwitch(thread);
换句话说,mbed 并未实现一个明确的 schedule 函数,而是在各逻辑执行 osRtxThreadSwitch 之前,自行从就绪队列上选择下一个要投入运行的线程,并传给 osRtxThreadSwitch 函数。这一套组合拳,就相当于是 schedule 函数。
5. 总结
mbed 是支持分态的(应用程序运行在非特权级,内核运行在特权级),最典型的上下文切换点位是系统调用返回用户态时。另外 PendSV、tick 这两个中断的处理函数中也会做上下文切换。内核中的 osRtxThreadSwitch 只是给内核一个“需要进行上下文切换了”的 hint,真正的上下文切换,要留待系统调用或中断处理函数返回用户态之前的上下文切换点位。该设计与 linux 是神似的。
原作者: 窗有老梅 戴胜冬