RT-Thread上下文切换(基于arm926)

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上下文切换,包括:

  1. 拷贝环境相关寄存器数据到线程A的栈内存
  2. 从线程B的栈内存拷贝数据到寄存器
  3. 返回PC计数器值,执行线程B

  显然,上下文切换属于计算密集型,频繁地进行上下文切换,将导致系统性能的下降,因此需要合理地进行任务规划,避免将CPU时间浪费在频繁的上下文切换中。

 

posted @ 2019-05-17 17:06  NealWP  阅读(1234)  评论(0编辑  收藏  举报