RT-Thread上下文切换(基于arm926)
什么是线程?
代码经过预处理、编译、汇编和链接,最终形成一个可执行的文件,该可执行文件包含了指令和数据。
一个程序可由多个线程(任务)组成,可执行文件中的指令和数据中的一部分属于线程A,一部分属于线程B,还有一些可能被线程A和B共享。
程序启动后,线程A获取CPU执行权(就是将寄存器PC设置为属于线程A部分的指令),操作属于线程A的数据,通过这一些列的指令逻辑片段的处理,完成相应的任务。在必要时,切换到线程B,执行线程B的任务。通过上下文切换,来回处理线程A、B,实现系统的多任务处理。
线程上下文?
上下文表示一个线程执行的环境,该环境保存了当前线程的所有执行状态。恢复该环境后,线程就像没有被中断一样。上下文环境包括:
- 寄存器R0~R12
- LR
- SP
- PC
- CPSR
- 栈内存
RT-Thread中,使用一个struct rt_thread结构体来表示一个线程,该结构使用以下成员保存与上下文相关的信息:
- void *stack_addr:线程栈内存地址
- rt_uint32_t stack_size:线程栈内存大小
- void *sp:线程当前栈地址指针
因此每个线程,在内存中都有属于自己的一段栈内存,当线程被切换前,会将以上环境相关的寄存器、栈指针信息保存在自己的栈内存中。当需要恢复时,就通过栈指针信息,从栈内存中将上下文环境中的值取出,分别赋值给相应的寄存器,以此实现不同线程之间的上下文切换。
arm926架构芯片在栈中保存的环境,可以参考线程栈初始化函数:
1 /** 2 * This function will initialize thread stack 3 * 4 * @param tentry the entry of thread 5 * @param parameter the parameter of entry 6 * @param stack_addr the beginning stack address 7 * @param texit the function will be called when thread exit 8 * 9 * @return stack address 10 */ 11 rt_uint8_t *rt_hw_stack_init(void *tentry, void *parameter, 12 rt_uint8_t *stack_addr, void *texit) 13 { 14 rt_uint32_t *stk; 15 16 stack_addr += sizeof(rt_uint32_t); 17 stack_addr = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stack_addr, 8); 18 stk = (rt_uint32_t *)stack_addr; 19 20 *(--stk) = (rt_uint32_t)tentry; /* entry point */ 21 *(--stk) = (rt_uint32_t)texit; /* lr */ 22 *(--stk) = 0xdeadbeef; /* r12 */ 23 *(--stk) = 0xdeadbeef; /* r11 */ 24 *(--stk) = 0xdeadbeef; /* r10 */ 25 *(--stk) = 0xdeadbeef; /* r9 */ 26 *(--stk) = 0xdeadbeef; /* r8 */ 27 *(--stk) = 0xdeadbeef; /* r7 */ 28 *(--stk) = 0xdeadbeef; /* r6 */ 29 *(--stk) = 0xdeadbeef; /* r5 */ 30 *(--stk) = 0xdeadbeef; /* r4 */ 31 *(--stk) = 0xdeadbeef; /* r3 */ 32 *(--stk) = 0xdeadbeef; /* r2 */ 33 *(--stk) = 0xdeadbeef; /* r1 */ 34 *(--stk) = (rt_uint32_t)parameter; /* r0 : argument */ 35 /* cpsr */ 36 if ((rt_uint32_t)tentry & 0x01) 37 *(--stk) = SVCMODE | 0x20; /* thumb mode */ 38 else 39 *(--stk) = SVCMODE; /* arm mode */ 40 41 /* return task's current stack address */ 42 return (rt_uint8_t *)stk; 43 }
NOTE:
RT-Thread启动后,在start_gcc.S中,将CPU设置为SVC模式,中断屏蔽。因此,调度器开始运行之前,CPU一直处于中断屏蔽状态,当第一次任务调度开始时,才将CPSR设置为任务栈中的默认值(svc mode),此时才使能了中断。
上下文切换时机
1. rt_hw_context_switch_to: 没有来源线程,系统第一次启动调度器时使用
1 /* 2 * void rt_hw_context_switch_to(rt_uint32 to); 3 * r0 --> to 4 */ 5 .globl rt_hw_context_switch_to 6 rt_hw_context_switch_to: 7 LDR SP, [R0] @; SP寄存器更新为目标线程rt_thread->sp
8 LDMFD SP!, {R4} @; 出栈--cpsr,更新到R4
9 MSR SPSR_cxsf, R4 @; cpsr保存到spsr
10 LDMFD SP!, {R0-R12, LR, PC}^ @; 出栈--r0~r12, lr, pc; 同时更新CPSR<-SPSR
2.rt_hw_context_switch: 线程上下文切换,从当前线程切换到目标线程
线程上下文切换,始终处于SVC模式
1 /* 2 * void rt_hw_context_switch(rt_uint32 from, rt_uint32 to); 3 * r0 --> from 4 * r1 --> to 5 */ 6 .globl rt_hw_context_switch 7 rt_hw_context_switch: 8 STMFD SP!, {LR} @; 入栈--源线程from中的下一条要执行的指令地址
9 STMFD SP!, {R0-R12, LR} @; 入栈--lr, r12~r0
10 MRS R4, CPSR 11 STMFD SP!, {R4} @; 入栈--cpsr 12 STR SP, [R0] @; 源线程rt_thread->sp更新(r0)
13 LDR SP, [R1] @; 目标线程rt_thread->sp更新到SP寄存器
14 LDMFD SP!, {R4} @; 出栈--cpsr保存到R4
15 MSR SPSR_cxsf, R4 @; 更新spsr为目标线程的cpsr
16 LDMFD SP!, {R0-R12, LR, PC}^ @; 出栈--r0~r12, lr, pc; 同时更新cpsr<-spsr
3.rt_hw_context_switch_interrupt: 中断上下文切换,从当前线程切换到目标线程
rt_hw_context_switch_interrupt不进行实际的上下文切换,它仅仅是设置相关变量,保存切换信息,在实际的中断中,根据这些信息进行上下文切换,相关代码如下:
/* * r0 --> rt_thread_switch_interrupt_flag * ARM当前处于IRQ工作模式,sp_irq指向的内存中保存了被中断线程的上下文r0-r12,lr */ rt_hw_context_switch_interrupt_do: mov r1, #0 str r1, [r0] @;清除标记rt_thread_switch_interrtup_flag = 0 mov r1, sp @; r1保存irq模式下,指向r0~r3的属于被中断线程的环境 add sp, sp, #4*4 @; sp指向保存被中断线程的环境中的r4地址 ldmfd sp!, {r4-r12,lr} @; 出栈--从sp_irq恢复被中断线程的环境r4-r12, lr mrs r0, spsr @;r0保存被中断线程的cpsr sub r2, lr, #4 @; r2保存被中断线程将要执行的下一条指令的地址 msr cpsr_c, #I_BIT|F_BIT|MODE_SVC @; 模式切换: IRQ模式 -> SVC模式,中断屏蔽 @;SVC模式,使用sp_svc,即被中断线程执行时的sp值,该值指向被中断线程指向的栈内存 stmfd sp!, {r2} @; 入栈--r2:被中断线程将要执行的下一条指令的地址 stmfd sp!, {r4-r12,lr} @; 入栈--lr, r12-r4 ldmfd r1, {r1-r4} @; 出栈--从r1地址恢复r0~r3 stmfd sp!, {r1-r4} @; 入栈--r0~r3 stmfd sp!, {r0} @; 入栈--cpsr @; 更新from线程rt_thread->sp地址为栈顶地址 ldr r4, =rt_interrupt_from_thread ldr r5, [r4] str sp, [r5] @; 加载to线程的rt_thread->sp地址到SP寄存器 ldr r6, =rt_interrupt_to_thread ldr r6, [r6] ldr sp, [r6] ldmfd sp!, {r4} @; 出栈--cpsr msr spsr_cxsf, r4 @; 更新spsr<-cpsr ldmfd sp!, {r0-r12,lr,pc}^ @; 出栈--恢复目标线程环境r0-r12, lr, pc; 同时更新cpsr<-spsr
总结:
RT-Thread中的线程A\B上下文切换,包括:
- 拷贝环境相关寄存器数据到线程A的栈内存
- 从线程B的栈内存拷贝数据到寄存器
- 返回PC计数器值,执行线程B
显然,上下文切换属于计算密集型,频繁地进行上下文切换,将导致系统性能的下降,因此需要合理地进行任务规划,避免将CPU时间浪费在频繁的上下文切换中。