[018] [RT-Thread学习笔记] 上下文切换分析

RT-Thread
学习笔记
相关API
实现上下文切换
为什么使用`PendSV`
成上下文切换
Cortex-M 的
上下文切换

RT-Thread版本:4.0.5
MCU型号:STM32F103RCT6(ARM Cortex-M3 内核)

1 相关API

函数和变量描述
void rt_hw_context_switch_to(rt_uint32_t to);没有来源线程的上下文切换,在调度器启动第一个线程的时候调用,以及在 signal 里面会调用
void rt_hw_context_switch(rt_uint32_t from, rt_uint32_t to);从 from 线程切换到 to 线程,用于线程和线程之间的切换
void rt_hw_context_switch_interrupt(rt_uint32_t from, rt_uint32_t to);从 from 线程切换到 to 线程,用于中断里面进行切换的时候使用
rt_uint32_t rt_thread_switch_interrupt_flag;表示需要在中断里进行切换的标志
rt_uint32_t rt_interrupt_from_thread, rt_interrupt_to_thread;在线程进行上下文切换时候,用来保存 from 和 to 线程的栈顶指针的指针(即保存&thread->sp

2 实现上下文切换

2.1 为什么使用PendSV完成上下文切换

Cortex-M的上下文切换由PendSV异常完成,为什么不用systick或其他异常,解释如下:

image-20220325164002026

上图为两个任务轮转调度,但当产生Systick异常时正在响应一个中断,则Systick异常会抢占其中断服务例程IRQ,若在此时OS进行上下文切换,会导致中断请求被延时,这在实时系统是不允许出现的。对于M3/4,当存在活跃的异常时,设计默认不允许返回到线程模式非基级的线程模式例外),否则触发Usage fault。

PendSV异常会自动延迟上下文切换的请求,直到其它的ISR都完成了处理后才放行(编程为最低优先级的异常)。如果OS检测到某IRQ正在活动并且被SysTick抢占,它将悬起一个PendSV异常,以便缓期执行上下文切换:

image-20220325164645239

  1. 任务A呼叫SVC请求任务切换
  2. OS收到请求做好上下文切换准备,并悬起PendSV异常
  3. 当CPU退出SVC后,立即进入PendSV进行上下文切换
  4. 当PendSV执行完后,然后任务B,切换到线程模式
  5. 中断发生,ISR开始执行
  6. ISR执行过程中,发生systick异常,抢占了该ISR
  7. OS执行必要操作,然后悬起PendSV异常以做好上下文切换的准备
  8. 当systick退出后,被抢占的ISR继续执行
  9. ISR执行完毕,PendSV服务例程开始执行,并在其中进行上下文切换
  10. PendSV执行完毕,回到任务A,切换到线程模式

2.2 Cortex-M 的上下文切换

在 ARM9 等平台,rt_hw_context_switch() 和 rt_hw_context_switch_interrupt() 的实现并不一样,在IRQ里如果触发了线程的调度,调度函数里会调用 rt_hw_context_switch_interrupt() 触发上下文切换。IRQ里处理完中断事务之后,中断退出之前,检查 rt_thread_switch_interrupt_flag 变量,如果该变量的值为 1,就根据 rt_interrupt_from_thread 变量和 rt_interrupt_to_thread 变量,完成线程的上下文切换。

在 Cortex-M 处理器架构里,基于自动部分压栈和 PendSV 的特性,上下文切换可以实现地更加简洁,并且允许中断嵌套。

因此,在线程和中断里进行上下文切换没有区别, rt_hw_context_switch() 和 rt_hw_context_switch_interrupt() 实现一致。

  • 线程环境下的上下文切换

image-20220325173011380

  • 中断环境下的上下文切换
    image-20220325173449183

硬件在进入 PendSV 中断之前自动保存了 from 线程的 PSR、PC、LR、R12、R3-R0 寄存器,然后 PendSV 里保存 from 线程的 R11~R4 寄存器,以及恢复 to 线程的 R4~R11 寄存器,最后硬件在退出 PendSV 中断之后,自动恢复 to 线程的 R0~R3、R12、LR、PC、PSR 寄存器。

2.2.1 rt_hw_context_switch_to()

rt_hw_context_switch_to() 只有目标线程,没有来源线程。只在第一次启动时在rt_system_scheduler_start()中调用。

image-20220325174053008

;/*
; * void rt_hw_context_switch_to(rt_uint32 to);
; * r0 --> to
; * 该函数用于开启第一次线程切换
; */
rt_hw_context_switch_to    PROC
    EXPORT rt_hw_context_switch_to
    ; set to thread
    LDR     r1, =rt_interrupt_to_thread     ; 设置目的线程栈顶地址
    STR     r0, [r1]                        ; rt_interrupt_to_thread = &to_thread->sp 即线程栈顶地址sp的指针

    ; 设置 from 线程为0,表示不需要从保存 from 的上下文
    LDR     r1, =rt_interrupt_from_thread   ; 第一次启动没有源线程,栈顶地址的指针初始化为0
    MOV     r0, #0x0
    STR     r0, [r1]                        ; rt_interrupt_from_thread = 0

    ; 设置标志为 1,表示需要切换,这个变量将在 PendSV 异常处理函数里切换的时被清零
    LDR     r1, =rt_thread_switch_interrupt_flag
    MOV     r0, #1
    STR     r0, [r1]                        ; rt_thread_switch_interrupt_flag = 1

    ; 设置 PendSV 异常优先级为最低优先级
    LDR     r0, =NVIC_SYSPRI2
    LDR     r1, =NVIC_PENDSV_PRI  
    LDR.W   r2, [r0,#0x00]       ; r2 = *NVIC_SYSPRI2
    ORR     r1,r1,r2             ; r1 |= r2
    STR     r1, [r0]             ; *r0 = *NVIC_SYSPRI2 = r1

    ; 触发 PendSV 异常 (将执行 PendSV 异常处理程序)
    LDR     r0, =NVIC_INT_CTRL
    LDR     r1, =NVIC_PENDSVSET     
    STR     r1, [r0]            ; SCB_ICSR寄存器写0无效, 因为可以直接赋值

    ; 放弃芯片启动到第一次上下文切换之前的栈内容,将 MSP 设置启动时的值
    ; LDR     r0, =SCB_VTOR     ; 读取向量表相对基址的偏移字节
    ; LDR     r0, [r0]
    ; LDR     r0, [r0]
    ; MSR     msp, r0

    ; 使能全局中断和全局异常,使能之后将进入 PendSV 异常处理函数
    CPSIE   F
    CPSIE   I

    ; 不会执行到这里
    ENDP

rt_hw_context_switch_to函数主要功能:

  • 获取to线程栈顶指针的指针,以便直接修改它们的值
  • 将rt_interrupt_from_thread 置 0,表示不需要从保存 from 线程的上下文
  • rt_thread_switch_interrupt_flag 置 1
  • 设置 PendSV 异常优先级为最低优先级
  • 触发 PendSV 异常

注意:rt_interrupt_to_thread/rt_interrupt_from_thread的的值是一个指针(地址),即&thread->sp

2.2.2 rt_hw_context_switch()/ rt_hw_context_switch_interrupt()

实现从 from 线程切换到 to 线程的功能。
image-20220325174325605

;/*
; * void rt_hw_context_switch(rt_uint32 from, rt_uint32 to);
; * r0 --> from
; * r1 --> to
; */
rt_hw_context_switch_interrupt
    EXPORT rt_hw_context_switch_interrupt
rt_hw_context_switch    PROC
    EXPORT rt_hw_context_switch

    ; 检查 rt_thread_switch_interrupt_flag 变量是否为 1
    LDR     r2, =rt_thread_switch_interrupt_flag
    LDR     r3, [r2]
    CMP     r3, #1      
    BEQ     _reswitch   ; rt_thread_switch_interrupt_flag为1 则跳转
    MOV     r3, #1      
    STR     r3, [r2]    ; rt_thread_switch_interrupt_flag = 1

    LDR     r2, =rt_interrupt_from_thread   ; set rt_interrupt_from_thread
    STR     r0, [r2]                        ; rt_interrupt_from_thread = &from_thread->sp

_reswitch
    LDR     r2, =rt_interrupt_to_thread     ; set rt_interrupt_to_thread
    STR     r1, [r2]                        ; rt_interrupt_to_thread = &to_thread->sp

    ; 触发 PendSV 异常,将进入 PendSV 异常处理函数里完成上下文切换
    LDR     r0, =NVIC_INT_CTRL              ; 触发PendSV 异常,实现上下文切换
    LDR     r1, =NVIC_PENDSVSET
    STR     r1, [r0]
    BX      LR
    ENDP

rt_hw_context_switch/rt_hw_context_switch_interrupt函数主要功能:

  • 更新from/to线程栈顶指针的指针,以便直接修改它们的值(rt_thread_switch_interrupt_flag 为0,则不会更新from线程)
  • rt_thread_switch_interrupt_flag = 1
  • 触发 PendSV 异常

注意:在调用rt_hw_context_switch_xx时要用中断锁保护,以防被打断。

2.2.3 PendSV服务例程中实现真正的上下文切换

image-20220325174851339

; r0 --> switch from thread stack
; r1 --> switch to thread stack
; psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack
PendSV_Handler   PROC
    EXPORT PendSV_Handler

    ; 关闭全局中断保护上下文切换
    MRS     r2, PRIMASK
    CPSID   I

    ; 判断r1是否为0,为0则跳转到pendsv_exit, 表示PendSV已处理
    LDR     r0, =rt_thread_switch_interrupt_flag
    LDR     r1, [r0]
    CBZ     r1, pendsv_exit

    ; 清零 rt_thread_switch_interrupt_flag 变量
    MOV     r1, #0x00
    STR     r1, [r0]
     ; 如果rt_interrupt_from_thread为 0,就不进行 from 线程的上下文保存
    LDR     r0, =rt_interrupt_from_thread
    LDR     r1, [r0]
    CBZ     r1, switch_to_thread    ; 源线程sp的指针若为0(第一次启动为0),则跳过寄存器第一次保存现场
    ; 保存from源线程的现场: r4 - r11
    MRS     r1, psp                 ; get from thread stack pointer
    STMFD   r1!, {r4 - r11}         ; push r4 - r11 register
    LDR     r0, [r0]                ; r0 = &rt_interrupt_from_thread
    STR     r1, [r0]                ; update from thread stack pointer
; 恢复to目标线程的现场,并更新当前psp为目标线程的栈指针
switch_to_thread    
    LDR     r1, =rt_interrupt_to_thread ; rt_interrupt_to_thread全局变量保存的是thread sp的地址
    LDR     r1, [r1]
    LDR     r1, [r1]                ; load thread stack pointer

    LDMFD   r1!, {r4 - r11}         ; pop r4 - r11 register
    MSR     psp, r1                 ; update stack pointer

pendsv_exit
    ; 恢复全局中断状态
    MSR     PRIMASK, r2

    ; 修改 lr 寄存器的 bit2,确保进程使用 PSP 堆栈指针
    ORR     lr, lr, #0x04   ; lr = EXC_RETURN, 第二位置1表示返回线程模式后使用PSP
    BX      lr              ; psr, pc, lr, r12, r3, r2, r1, r0 will pop from [to_thread_stack] 
    ENDP

PendSV_Handler主要功能:

  • 进入ESR前,硬件将psr, pc, lr, r12, r3, r2, r1, r0 压入 [from] 线程栈中
  • 进入ESR中,若rt_thread_switch_interrupt_flag为0则退出,反之将其清零
  • 进入ESR中,软件将r4 - r11 压入 [from] 线程栈中
  • 进入ESR中,更新当前psp为to线程的栈指针
  • 进入ESR中,软件将 [to] 线程栈中的保存的r4 - r11 弹出给寄存器
  • 进入ESR中,将lr = EXC_RETURN的 第二位置1表示返回线程模式后使用PSP
  • 退出ESR,硬件将[to] 线程栈中的保存的psr, pc, lr, r12, r3, r2, r1, r0弹出给寄存器,跳转到pc所在地址继续取指执行。

关于rt_thread_switch_interrupt_flag的作用:

  • 线程环境中,主动进行任务调度,在rt_schedule中调用rt_hw_context_switch()函数后,此时已经将from/to线程的栈顶指针地址保存到rt_interrupt_from_thread/rt_interrupt_to_thread变量中,并且将rt_thread_switch_interrupt_flag置1,然后触发PendSV异常。

    但在中断屏蔽期间,若又悬起了systick异常,中断使能后,会先跳转到systick的ESR中执行。在systick中会检查当前线程的时间片与系统定时器,若当前线程时间片已用完线程定时器超时,则会调用rt_schedule从优先级链表中获取当前最高优先级的线程,此时to_thread可能会改变,但是from_thread不会改变,最后调用rt_hw_context_switch_interrupt()函数(两者实现一样),此时rt_thread_switch_interrupt_flag已为1,故不会更新rt_interrupt_from_thread,同时也不会再次将rt_thread_switch_interrupt_flag置1。

  • PendSV_Handler中的rt_thread_switch_interrupt_flag作用个人猜测,可能是在没有调用rt_hw_context_switch/rt_hw_context_switch_interrupt时,但意外触发了PendSV,此时会退出,无需切换。

END

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柯西的彷徨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值