8、线程切换适配

上一章我们完成了 bsp 下项目工程中相关适配工作。

本章主要介绍 RT-Thread 线程切换的实现,相关适配工作为 RISC-V 对应架构通用,位于 libcpu/risc-v 目录下。

RT-Thread 线程切换可以使用中断中实现上下文切换或者在线程上下文中切换。

  1. 中断中实现上下文中切换是通过触发中断,在中断处理函数中保护现场、恢复现场切换至新线程等工作。ARM Core-M 系列、FreeRTOS 适配的 RISC-V 通常都采用这种方式,Cortex-M 架构触发 PendSV 中断,RISC-V 架构触发 software interrupt 中断,在中断处理函数中实现上下文切换。

  2. 线程中实现上下文切换是在线程中直接实现保护线程、恢复线程切换至新线程等工作。目前 RT-Thread 适配的 RISC-V 架构通常采用这种方式。

RT-Thread 为什么在适配 RISC-V 架构在线程中实现上下文切换,而在 ARM Cortex-M 架构上使用在中断中实现上下文切换呢?

ARM Cortex-M 实现了自动硬件自动压栈和出栈一些寄存器,而 RISC-V 标准中并没有要求,RISC-V 架构在需要通过软件方式保存、恢复寄存器,通过中断实现上下文切换并没有带来性能提升。很多 IP 设计厂家会通过扩展方式实现了这部分功能,在保证通用性的前提下,RT-Thread 在 RISC-V 架构上通常采用在线程中实现上下文切换。

线程调度主要涉及到线程切出和切入,相对应的是保护现场和恢复现场。

  1. 保护现场: 保护现场即保存源线程的当前寄存器状态至线程栈 sp 变量中,用于后续再次需要运行该线程后恢复现场。

  2. 恢复现场: 将目标线程栈中的 sp 变量中保存的状态,恢复到当前寄存器中,并切换至目标线程。

8.1、相关寄存器

  1. RA

  2. MEPC

  3. MSTATUS

  4. MSCRATCH

8.2、相关数据结构与宏

硬件栈数据结构

RT-Thread 线程切换涉及一个很重要的的结构体 struct rt_hw_stack_frame,指向线程栈中的硬件栈。

typedef struct rt_hw_stack_frame
{
    rt_ubase_t epc;        /* epc - epc    - program counter                     */
    rt_ubase_t ra;         /* x1  - ra     - return address for jumps            */
    rt_ubase_t mstatus;    /*              - machine status register             */
    rt_ubase_t gp;         /* x3  - gp     - global pointer                      */
    rt_ubase_t tp;         /* x4  - tp     - thread pointer                      */
    rt_ubase_t t0;         /* x5  - t0     - temporary register 0                */
    rt_ubase_t t1;         /* x6  - t1     - temporary register 1                */
    rt_ubase_t t2;         /* x7  - t2     - temporary register 2                */
    rt_ubase_t s0_fp;      /* x8  - s0/fp  - saved register 0 or frame pointer   */
    rt_ubase_t s1;         /* x9  - s1     - saved register 1                    */
    rt_ubase_t a0;         /* x10 - a0     - return value or function argument 0 */
    rt_ubase_t a1;         /* x11 - a1     - return value or function argument 1 */
    rt_ubase_t a2;         /* x12 - a2     - function argument 2                 */
    rt_ubase_t a3;         /* x13 - a3     - function argument 3                 */
    rt_ubase_t a4;         /* x14 - a4     - function argument 4                 */
    rt_ubase_t a5;         /* x15 - a5     - function argument 5                 */
#ifndef __riscv_32e
    rt_ubase_t a6;         /* x16 - a6     - function argument 6                 */
    rt_ubase_t a7;         /* x17 - a7     - function argument 7                 */
    rt_ubase_t s2;         /* x18 - s2     - saved register 2                    */
    rt_ubase_t s3;         /* x19 - s3     - saved register 3                    */
    rt_ubase_t s4;         /* x20 - s4     - saved register 4                    */
    rt_ubase_t s5;         /* x21 - s5     - saved register 5                    */
    rt_ubase_t s6;         /* x22 - s6     - saved register 6                    */
    rt_ubase_t s7;         /* x23 - s7     - saved register 7                    */
    rt_ubase_t s8;         /* x24 - s8     - saved register 8                    */
    rt_ubase_t s9;         /* x25 - s9     - saved register 9                    */
    rt_ubase_t s10;        /* x26 - s10    - saved register 10                   */
    rt_ubase_t s11;        /* x27 - s11    - saved register 11                   */
    rt_ubase_t t3;         /* x28 - t3     - temporary register 3                */
    rt_ubase_t t4;         /* x29 - t4     - temporary register 4                */
    rt_ubase_t t5;         /* x30 - t5     - temporary register 5                */
    rt_ubase_t t6;         /* x31 - t6     - temporary register 6                */
#endif
#ifdef ARCH_RISCV_FPU
    rv_floatreg_t f0;      /* f0  */
    rv_floatreg_t f1;      /* f1  */
    rv_floatreg_t f2;      /* f2  */
    rv_floatreg_t f3;      /* f3  */
    rv_floatreg_t f4;      /* f4  */
    rv_floatreg_t f5;      /* f5  */
    rv_floatreg_t f6;      /* f6  */
    rv_floatreg_t f7;      /* f7  */
    rv_floatreg_t f8;      /* f8  */
    rv_floatreg_t f9;      /* f9  */
    rv_floatreg_t f10;     /* f10 */
    rv_floatreg_t f11;     /* f11 */
    rv_floatreg_t f12;     /* f12 */
    rv_floatreg_t f13;     /* f13 */
    rv_floatreg_t f14;     /* f14 */
    rv_floatreg_t f15;     /* f15 */
    rv_floatreg_t f16;     /* f16 */
    rv_floatreg_t f17;     /* f17 */
    rv_floatreg_t f18;     /* f18 */
    rv_floatreg_t f19;     /* f19 */
    rv_floatreg_t f20;     /* f20 */
    rv_floatreg_t f21;     /* f21 */
    rv_floatreg_t f22;     /* f22 */
    rv_floatreg_t f23;     /* f23 */
    rv_floatreg_t f24;     /* f24 */
    rv_floatreg_t f25;     /* f25 */
    rv_floatreg_t f26;     /* f26 */
    rv_floatreg_t f27;     /* f27 */
    rv_floatreg_t f28;     /* f28 */
    rv_floatreg_t f29;     /* f29 */
    rv_floatreg_t f30;     /* f30 */
    rv_floatreg_t f31;     /* f31 */
#endif
}rt_hw_stack_frame_t;

该结构体和芯片架构的寄存器密切相关,RISC-V 架构包括 mepc、mstatus、x1、x3~x31、f0~f31。

疑问:

  1. 这里发现这个结构体中没有 x0 和 x2,为什么?

因为 x0 为 0,不需要保存;x2 为 sp 指向当前程序运行栈顶地址。

线程栈数据结构

线程栈结构体部分变量:

struct rt_thread
{
    struct rt_object            parent;
    rt_list_t                   tlist;                  /**< the thread list */

    /* stack point and entry */
    void                        *sp;                    /**< stack point */
    void                        *entry;                 /**< entry */
    void                        *parameter;             /**< parameter */
    void                        *stack_addr;            /**< stack address */
    rt_uint32_t                 stack_size;             /**< stack size */

    ...
}

为了让线程切换相关函数可以兼容 RV32、RV64 多种 RISC-V 指令体系,线程切换相关函数中使用了较多宏,这些宏定义在 cpuport.h 文件中

/* Preprocessor Definition */
#if __riscv_flen == 32
#define ARCH_RISCV_FPU
#define ARCH_RISCV_FPU_S
#endif

#if __riscv_flen == 64
#define ARCH_RISCV_FPU
#define ARCH_RISCV_FPU_D
#endif

/* bytes of register width  */
#ifdef ARCH_CPU_64BIT
#define STORE                   sd
#define LOAD                    ld
#define REGBYTES                8
#else
#define STORE                   sw
#define LOAD                    lw
#define REGBYTES                4
#endif

/* Preprocessor Definition */
#ifdef ARCH_RISCV_FPU
#ifdef ARCH_RISCV_FPU_D
#define FSTORE                  fsd
#define FLOAD                   fld
#define FREGBYTES               8
#define rv_floatreg_t           rt_int64_t
#endif
#ifdef ARCH_RISCV_FPU_S
#define FSTORE                  fsw
#define FLOAD                   flw
#define FREGBYTES               4
#define rv_floatreg_t           rt_int32_t
#endif
#endif

在后续介绍的相关任务切换函数中,都使用到了这些宏。

8.3、相关指令

伪指令

  • csrw 用法:csrw csr, rs 基础指令:csrrw x0, csr, rs 含义:写控制寄存器中对应比特

  • csrr 用法:csrr rd,csr 基础指令:CSRRS rd,csr,x0 含义:读取控制寄存器 示例:

csrr t0, mstatus                # 读取 mstatus 到 t0
  • csrs (Control and Status Register Set) 用法:csrs csr, rs 基础指令:csrrs x0, csr, rs 含义:置位控制寄存器中对应比特 示例:

csrs mstatus, (1 << 2)                # 将 mstatus bit3 置1
  • beqz 用法:beqz rs, offset 基础指令:beq rs, x0, offset 含义:寄存器为零分支跳转 示例:

  • andi 立即数按位与指令 语法:andi rd, rs1, imm12 操作:rd rs1 & sign_extend(imm12)

  • sw 字存储指令 语法:sw rs2, imm12(rs1)

  • lw 有符号扩展字加载指令 语法:lw rd, imm12(rs1)

mret

当异常程序处理完成后,最终要从异常服务程序中退出,并返回主程序。

RISC-V 中定义了一组退出指令 mret,sret,和 uret,对于机器模式,对应 mret。注意高等级的特权模式可以执行低等级的xret指令,即 M 模式可以执行 mret,sret 和 uret。在机器模式下退出异常时候,软件必须使用 mret。

RISC-V 架构规定,处理器执行完 mret 指令后,硬件行为如下:

  1. 停止执行当前程序流,转而从 CSR 寄存器 MEPC 定义的 PC 地址开始执行。

  2. 硬件更新 CSR 寄存器机器模式状态寄存器 MSTATUS。MSTATUS 寄存器 MIE 域被更新为当前 MPIE 的值。MPIE 域的值则更新为1。

8.4、线程栈初始化

内核在线程创建和线程初始化中调用 rt_hw_stack_init 函数实现线程栈初始化。

/**
 * This function will initialize thread stack
 *
 * @param tentry the entry of thread
 * @param parameter the parameter of entry
 * @param stack_addr the beginning stack address
 * @param texit the function will be called when thread exit
 *
 * @return stack address
 */
rt_uint8_t *rt_hw_stack_init(void       *tentry,
                             void       *parameter,
                             rt_uint8_t *stack_addr,
                             void       *texit)
{
    struct rt_hw_stack_frame *frame;
    rt_uint8_t         *stk;
    int                i;

    stk  = stack_addr + sizeof(rt_ubase_t);
    stk  = (rt_uint8_t *)RT_ALIGN_DOWN((rt_ubase_t)stk, REGBYTES);
    stk -= sizeof(struct rt_hw_stack_frame);

    frame = (struct rt_hw_stack_frame *)stk;

    for (i = 0; i < sizeof(struct rt_hw_stack_frame) / sizeof(rt_ubase_t); i++)
    {
        ((rt_ubase_t *)frame)[i] = 0xdeadbeef;
    }

    frame->ra      = (rt_ubase_t)texit;
    frame->a0      = (rt_ubase_t)parameter;
    frame->epc     = (rt_ubase_t)tentry;

    /* force to machine mode(MPP=11) and set MPIE to 1 */
#ifdef ARCH_RISCV_FPU
    frame->mstatus = 0x7880;
#else
    frame->mstatus = 0x1880;
#endif

    return stk;
}

rt_hw_stack_init 函数主要完成了以下工作:

  1. 对齐线程栈,对齐长度与当前指令集长度有关,RV32 为 4 字节,RV64 为 8 字节。

  2. 将所有栈数据设置为 0xdeadbeef。

  3. 将传入的 texit、parameter、tentry 分别赋值至线程栈中的 ra、a0、epc。

  • texit:RT-Thread 内部函数 _thread_exit。

  • tentry:用户创建线程的线程函数。

  • parameter:传递给线程函数参数。

  1. 设置 mstatus,包括 MIE、MPIE、MPP、FS,实现使能 FPU、开启总中断等功能。RT-Thread 一开始就关闭了全局中断,在开始调度器时调用 rt_hw_context_switch_to 函数,会将当前线程栈的 mstatus 的值赋值给芯片的 mstatus 寄存器,完成全局中断开启。

rt_hw_stack_init 函数在 _thread_init 函数中被调用,调用形式为:

thread->sp = (void *)rt_hw_stack_init(thread->entry, thread->parameter,
                                        (rt_uint8_t *)((char *)thread->stack_addr + thread->stack_size - sizeof(rt_ubase_t)),
                                        (void *)_thread_exit);

通过该函数的传入的参数可以看到,传入的线程栈起始地址 (stack_addr)为 thread->stack_addr + thread->stack_size,该地址为栈顶地址。rt_hw_stack_init 函数将对齐后的线程栈顶地址并减去结构体 struct rt_hw_stack_frame 数据长度作为返回值存储至线程的栈 sp 变量,这个地址也就是线程栈空余空间栈顶地址,后续所有的线程切换都会用到这个 sp 变量。

stack_addr

8.5、无来源线程的上下文切换函数

该函数在调度器启动第一个线程的时候调用,以及在 signal 中被调用,在 SMP 和非 SMP 的芯片上,该函数对应的 API 不同。

  • 非 SMP:void rt_hw_context_switch_to(rt_ubase_t to)

  • SMP:void rt_hw_context_switch_to(rt_ubase_t to, stuct rt_thread *to_thread)

不同 API 传入的参数不同,在同一汇编函数中实现,非 SMP 的函数只有1个参数传入给了 a0 寄存器,SMP 的函数同时会将第 2 个参数传给了 a1 寄存器。

rt_hw_context_switch_to 函数实现如下:

/*
 * #ifdef RT_USING_SMP
 * void rt_hw_context_switch_to(rt_ubase_t to, stuct rt_thread *to_thread);
 * #else
 * void rt_hw_context_switch_to(rt_ubase_t to);
 * #endif
 * a0 --> to
 * a1 --> to_thread
 */
    .globl rt_hw_context_switch_to
rt_hw_context_switch_to:
    la t0, __rt_rvstack
    csrw mscratch,t0

    LOAD sp, (a0)

#ifdef RT_USING_SMP
    mv   a0,   a1
    call  rt_cpus_lock_status_restore
#endif
    LOAD a0,   2 * REGBYTES(sp)
    csrw mstatus, a0
    j    rt_hw_context_switch_exit

rt_hw_context_switch_to 在调度器启动第一个线程的时候调用,传入的 to 变量即为上文讲到的 rt_hw_stack_init 函数返回保存在线程栈中的 struct rt_hw_stack_frame 指向的地址,这个地址指向的是结构体的第 0 个成员。

在非 SMP 环境下 rt_hw_context_switch_to 函数中完成了 4 个工作:

  1. 将启动文件中设置的栈顶地址 __rt_rvstack 赋值给 mscratch。

    la t0, __rt_rvstack
    csrw mscratch,t0
  1. 将目标线程栈 to 指向地址的值赋值给 sp。

LOAD sp, (a0)
  1. struct rt_hw_stack_frame 结构体中的第 2 个成员内容赋值给了 mstatus。

    LOAD a0,   2 * REGBYTES(sp)
    csrw mstatus, a0
  1. 跳转至 rt_hw_context_switch_exit 函数。

    j    rt_hw_context_switch_exit

8.6、恢复现场

指令集架构对应的线程切出与切入是对应的,这里要先重点介绍一下 rt_hw_context_switch_exit 这个函数,函数完成主要工作是目标线程现场恢复并切换到该线程。该函数在后续还会被调用。

.global rt_hw_context_switch_exit
rt_hw_context_switch_exit:
#ifdef RT_USING_SMP
#ifdef RT_USING_SIGNALS
    mv a0, sp

    csrr  t0, mhartid
    /* switch interrupt stack of current cpu */
    la    sp, __stack_start__
    addi  t1, t0, 1
    li    t2, __STACKSIZE__
    mul   t1, t1, t2
    add   sp, sp, t1 /* sp = (cpuid + 1) * __STACKSIZE__ + __stack_start__ */

    call rt_signal_check
    mv sp, a0
#endif
#endif
    /* resw ra to mepc */
    LOAD a0,   0 * REGBYTES(sp)
    csrw mepc, a0

    LOAD x1,   1 * REGBYTES(sp)
#ifdef ARCH_RISCV_FPU
    li    t0, 0x7800
#else
    li    t0, 0x1800
#endif
    csrw  mstatus, t0
    LOAD a0,   2 * REGBYTES(sp)
    csrs mstatus, a0

    LOAD x4,   4 * REGBYTES(sp)
    LOAD x5,   5 * REGBYTES(sp)
    LOAD x6,   6 * REGBYTES(sp)
    LOAD x7,   7 * REGBYTES(sp)
    LOAD x8,   8 * REGBYTES(sp)
    LOAD x9,   9 * REGBYTES(sp)
    LOAD x10, 10 * REGBYTES(sp)
    LOAD x11, 11 * REGBYTES(sp)
    LOAD x12, 12 * REGBYTES(sp)
    LOAD x13, 13 * REGBYTES(sp)
    LOAD x14, 14 * REGBYTES(sp)
    LOAD x15, 15 * REGBYTES(sp)
#ifndef __riscv_32e
    LOAD x16, 16 * REGBYTES(sp)
    LOAD x17, 17 * REGBYTES(sp)
    LOAD x18, 18 * REGBYTES(sp)
    LOAD x19, 19 * REGBYTES(sp)
    LOAD x20, 20 * REGBYTES(sp)
    LOAD x21, 21 * REGBYTES(sp)
    LOAD x22, 22 * REGBYTES(sp)
    LOAD x23, 23 * REGBYTES(sp)
    LOAD x24, 24 * REGBYTES(sp)
    LOAD x25, 25 * REGBYTES(sp)
    LOAD x26, 26 * REGBYTES(sp)
    LOAD x27, 27 * REGBYTES(sp)
    LOAD x28, 28 * REGBYTES(sp)
    LOAD x29, 29 * REGBYTES(sp)
    LOAD x30, 30 * REGBYTES(sp)
    LOAD x31, 31 * REGBYTES(sp)

    addi sp,  sp, 32 * REGBYTES
#else
    addi sp,  sp, 16 * REGBYTES
#endif

#ifdef ARCH_RISCV_FPU
    FLOAD   f0, 0 * FREGBYTES(sp)
    FLOAD   f1, 1 * FREGBYTES(sp)
    FLOAD   f2, 2 * FREGBYTES(sp)
    FLOAD   f3, 3 * FREGBYTES(sp)
    FLOAD   f4, 4 * FREGBYTES(sp)
    FLOAD   f5, 5 * FREGBYTES(sp)
    FLOAD   f6, 6 * FREGBYTES(sp)
    FLOAD   f7, 7 * FREGBYTES(sp)
    FLOAD   f8, 8 * FREGBYTES(sp)
    FLOAD   f9, 9 * FREGBYTES(sp)
    FLOAD   f10, 10 * FREGBYTES(sp)
    FLOAD   f11, 11 * FREGBYTES(sp)
    FLOAD   f12, 12 * FREGBYTES(sp)
    FLOAD   f13, 13 * FREGBYTES(sp)
    FLOAD   f14, 14 * FREGBYTES(sp)
    FLOAD   f15, 15 * FREGBYTES(sp)
    FLOAD   f16, 16 * FREGBYTES(sp)
    FLOAD   f17, 17 * FREGBYTES(sp)
    FLOAD   f18, 18 * FREGBYTES(sp)
    FLOAD   f19, 19 * FREGBYTES(sp)
    FLOAD   f20, 20 * FREGBYTES(sp)
    FLOAD   f21, 21 * FREGBYTES(sp)
    FLOAD   f22, 22 * FREGBYTES(sp)
    FLOAD   f23, 23 * FREGBYTES(sp)
    FLOAD   f24, 24 * FREGBYTES(sp)
    FLOAD   f25, 25 * FREGBYTES(sp)
    FLOAD   f26, 26 * FREGBYTES(sp)
    FLOAD   f27, 27 * FREGBYTES(sp)
    FLOAD   f28, 28 * FREGBYTES(sp)
    FLOAD   f29, 29 * FREGBYTES(sp)
    FLOAD   f30, 30 * FREGBYTES(sp)
    FLOAD   f31, 31 * FREGBYTES(sp)

    addi    sp, sp, 32 * FREGBYTES
#endif

    mret

这里的 sp 变量依然指向的是参数 to 指向的地址,也就是 struct rt_hw_stack_frame 指向的首地址。

在非 SMP 环境下 rt_hw_context_switch_exit 函数依次完成以下工作

  1. 将目标线程栈结构体 struct rt_hw_stack_frame 中的第 0 个成员 rt_ubase_t epc 值赋值给 mepc。

  2. 恢复 mstatus 寄存器的值

#ifdef ARCH_RISCV_FPU
    li    t0, 0x7800
#else
    li    t0, 0x1800
#endif
    csrw  mstatus, t0
    LOAD a0,   2 * REGBYTES(sp)
    csrs mstatus, a0

这里的代码看起来有些奇怪,在未开启 FPU 功能宏时,先将 mstatus 对应的 bit11、bit12 置 1,然后再读取目标线程栈结构体 struct rt_hw_stack_frame 中的第 2 个成员 rt_ubase_t mstatus,清除 mstatus 寄存器对应的 bit。

但是在线程初始化函数 rt_hw_stack_init 函数中我们也是将线程栈结构体 struct rt_hw_stack_frame 中的第 2 个成员 rt_ubase_t mstatus 设置为 0x1880,是不是这里 csrw 语句有点多余了?这个问题留在后续解答。

  1. 依次恢复 x4~x31,f0~f31 寄存器的值,并调整当前 sp 地址为栈顶地址。

  2. 调用 mret 语句返回,mret 从 MEPC 寄存器定义的 PC 地址开始执行。

8.7、中断中线程切换

从 from 线程切换到 to 线程 rt_hw_context_switch_interrupt(rt_uint32_t from, rt_uint32_t to) 该函数用于中断中完成任务切换,SMP 的芯片实现方式不同。

  • 无 SMP

/*
 * #ifdef RT_USING_SMP
 * void rt_hw_context_switch_interrupt(void *context, rt_ubase_t from, rt_ubase_t to, struct rt_thread *to_thread);
 * #else
 * void rt_hw_context_switch_interrupt(rt_ubase_t from, rt_ubase_t to, rt_thread_t from_thread, rt_thread_t to_thread);
 * #endif
 */
#ifndef RT_USING_SMP
rt_weak void rt_hw_context_switch_interrupt(rt_ubase_t from, rt_ubase_t to, rt_thread_t from_thread, rt_thread_t to_thread)
{
    if (rt_thread_switch_interrupt_flag == 0)
        rt_interrupt_from_thread = from;

    rt_interrupt_to_thread = to;
    rt_thread_switch_interrupt_flag = 1;

    rt_trigger_software_interrupt();

    return ;
}
#endif /* end of RT_USING_SMP */
  • SMP

#ifdef RT_USING_SMP
/*
 * void rt_hw_context_switch_interrupt(void *context, rt_ubase_t from, rt_ubase_t to, struct rt_thread *to_thread);
 *
 * a0 --> context
 * a1 --> from
 * a2 --> to
 * a3 --> to_thread
 */
    .globl rt_hw_context_switch_interrupt
rt_hw_context_switch_interrupt:

    STORE a0, 0(a1)

    LOAD  sp, 0(a2)
    move  a0, a3
    call rt_cpus_lock_status_restore

    j rt_hw_context_switch_exit

#endif

该函数通过 rt_weak 申请可在需要中断中完成上下文切换的芯片上实现该函数。该函数在内核调度函数 rt_schedule() 中被调用,在非 SMP 的芯片上,调用形式为 rt_hw_context_switch_interrupt((rt_ubase_t)&from_thread->sp, (rt_ubase_t)&to_thread->sp, from_thread, to_thread)

中断中线程切换涉及到 3 个变量:

  • rt_uint32_t rt_thread_switch_interrupt_flag: 表示需要在中断中进行切换的标志。

  • rt_uint32_t rt_interrupt_from_thread, rt_interrupt_to_thread: 在线程进行上下文切换时,用于保存 from 和 to 线程。

在 libcpu/risc-v/common/interrupt_gcc.S 中断处理函数 SW_handler 中,通过变量 rt_thread_switch_interrupt_flag 判断是否需要找中断中进行线程切换,如果需要就通过 rt_interrupt_from_threadrt_interrupt_to_thread 变量做上下文切换。相关代码如下:

    /* Determine whether to trigger scheduling at the interrupt function */
    la    t0, rt_thread_switch_interrupt_flag
    lw    t2, 0(t0)
    beqz  t2, 1f
    /* clear the flag of rt_thread_switch_interrupt_flag */
    sw    zero, 0(t0)

    csrr  a0, mepc
    STORE a0, 0 * REGBYTES(sp)

    la    t0, rt_interrupt_from_thread
    LOAD  t1, 0(t0)
    STORE sp, 0(t1)

    la    t0, rt_interrupt_to_thread
    LOAD  t1, 0(t0)
    LOAD  sp, 0(t1)

8.8、线程和线程间切换

从 from 线程切换到 to 线程 rt_hw_context_switch(rt_uint32_t from, rt_uint32_t to) 该函数用于线程和线程之间切换。

该函数在内核调度函数 rt_schedule() 中被调用,在非 SMP 的芯片上,调用形式为 rt_hw_context_switch((rt_ubase_t)&from_thread->sp, (rt_ubase_t)&to_thread->sp);

上面讲到 RT-Thread 适配 RISC-V 架构通用采用线程上下文中实现线程切换而没有使用中断中实现上下文切换,内核使用 rt_hw_context_switch 函数实现线程切换。

rt_hw_context_switch 函数实现了保护现场的代码,并调用 rt_hw_context_switch_exit 函数实现恢复现场。

rt_hw_context_switch 传入的参数 from 和 to 分别对应源线程和目标线程的 sp 变量,对应 a0,a1 寄存器。涉及的工作使用了上面提到过的 struct rt_hw_stack_frame 结构体指向的线程栈中的 sp 变量。

线程切换很重要的一点是搞清楚当前 sp 寄存器地址是处于什么位置。发生线程切换时,sp 寄存器地址在空余栈空间顶,切出线程直接将需要保存的数据写入栈空间实现现场保护,待切入线程 sp 可直接读取到 struct rt_hw_stack_frame 结构体指向的第 0 个成员。

/*
 * #ifdef RT_USING_SMP
 * void rt_hw_context_switch(rt_ubase_t from, rt_ubase_t to, struct rt_thread *to_thread);
 * #else
 * void rt_hw_context_switch(rt_ubase_t from, rt_ubase_t to);
 * #endif
 *
 * a0 --> from
 * a1 --> to
 * a2 --> to_thread
 */
    .globl rt_hw_context_switch
rt_hw_context_switch:
    /* saved from thread context
     *     x1/ra       -> sp(0)
     *     x1/ra       -> sp(1)
     *     mstatus.mie -> sp(2)
     *     x(i)        -> sp(i-4)
     */
#ifdef ARCH_RISCV_FPU
    addi    sp, sp, -32 * FREGBYTES

    FSTORE  f0, 0 * FREGBYTES(sp)
    FSTORE  f1, 1 * FREGBYTES(sp)
    FSTORE  f2, 2 * FREGBYTES(sp)
    FSTORE  f3, 3 * FREGBYTES(sp)
    FSTORE  f4, 4 * FREGBYTES(sp)
    FSTORE  f5, 5 * FREGBYTES(sp)
    FSTORE  f6, 6 * FREGBYTES(sp)
    FSTORE  f7, 7 * FREGBYTES(sp)
    FSTORE  f8, 8 * FREGBYTES(sp)
    FSTORE  f9, 9 * FREGBYTES(sp)
    FSTORE  f10, 10 * FREGBYTES(sp)
    FSTORE  f11, 11 * FREGBYTES(sp)
    FSTORE  f12, 12 * FREGBYTES(sp)
    FSTORE  f13, 13 * FREGBYTES(sp)
    FSTORE  f14, 14 * FREGBYTES(sp)
    FSTORE  f15, 15 * FREGBYTES(sp)
    FSTORE  f16, 16 * FREGBYTES(sp)
    FSTORE  f17, 17 * FREGBYTES(sp)
    FSTORE  f18, 18 * FREGBYTES(sp)
    FSTORE  f19, 19 * FREGBYTES(sp)
    FSTORE  f20, 20 * FREGBYTES(sp)
    FSTORE  f21, 21 * FREGBYTES(sp)
    FSTORE  f22, 22 * FREGBYTES(sp)
    FSTORE  f23, 23 * FREGBYTES(sp)
    FSTORE  f24, 24 * FREGBYTES(sp)
    FSTORE  f25, 25 * FREGBYTES(sp)
    FSTORE  f26, 26 * FREGBYTES(sp)
    FSTORE  f27, 27 * FREGBYTES(sp)
    FSTORE  f28, 28 * FREGBYTES(sp)
    FSTORE  f29, 29 * FREGBYTES(sp)
    FSTORE  f30, 30 * FREGBYTES(sp)
    FSTORE  f31, 31 * FREGBYTES(sp)

#endif
#ifndef __riscv_32e
    addi  sp,  sp, -32 * REGBYTES
#else
    addi  sp,  sp, -16 * REGBYTES
#endif

    STORE sp,  (a0)

    STORE x1,   0 * REGBYTES(sp)
    STORE x1,   1 * REGBYTES(sp)

    csrr a0, mstatus
    andi a0, a0, 8
    beqz a0, save_mpie
    li   a0, 0x80
save_mpie:
    STORE a0,   2 * REGBYTES(sp)

    STORE x4,   4 * REGBYTES(sp)
    STORE x5,   5 * REGBYTES(sp)
    STORE x6,   6 * REGBYTES(sp)
    STORE x7,   7 * REGBYTES(sp)
    STORE x8,   8 * REGBYTES(sp)
    STORE x9,   9 * REGBYTES(sp)
    STORE x10, 10 * REGBYTES(sp)
    STORE x11, 11 * REGBYTES(sp)
    STORE x12, 12 * REGBYTES(sp)
    STORE x13, 13 * REGBYTES(sp)
    STORE x14, 14 * REGBYTES(sp)
    STORE x15, 15 * REGBYTES(sp)
#ifndef __riscv_32e
    STORE x16, 16 * REGBYTES(sp)
    STORE x17, 17 * REGBYTES(sp)
    STORE x18, 18 * REGBYTES(sp)
    STORE x19, 19 * REGBYTES(sp)
    STORE x20, 20 * REGBYTES(sp)
    STORE x21, 21 * REGBYTES(sp)
    STORE x22, 22 * REGBYTES(sp)
    STORE x23, 23 * REGBYTES(sp)
    STORE x24, 24 * REGBYTES(sp)
    STORE x25, 25 * REGBYTES(sp)
    STORE x26, 26 * REGBYTES(sp)
    STORE x27, 27 * REGBYTES(sp)
    STORE x28, 28 * REGBYTES(sp)
    STORE x29, 29 * REGBYTES(sp)
    STORE x30, 30 * REGBYTES(sp)
    STORE x31, 31 * REGBYTES(sp)
#endif
    /* restore to thread context
     * sp(0) -> epc;
     * sp(1) -> ra;
     * sp(i) -> x(i+2)
     */
    LOAD sp,  (a1)

#ifdef RT_USING_SMP
    mv   a0,   a2
    call  rt_cpus_lock_status_restore
#endif /*RT_USING_SMP*/

    j rt_hw_context_switch_exit

栈是一种先入后出的数据结构,RISC-V 栈空间为向下生长。

当前 sp 寄存器指向的地址是带切出线程栈顶位置,所以需要先采用减法调整 sp 的位置指向 struct rt_hw_stack_frame 中的第 0 个成员。

rt_hw_context_switch() 函数主要完成了以下工作:

  1. 先判断 FPU 是否使能,在使能的情况下先通过 addi    sp, sp, -32 * FREGBYTES 调整 sp 的位置,并读取 f0~f31 寄存器保存至线程栈。

  2. 通过 __riscv_32e 宏判断接下来调整 sp 位置的长度。

    #ifndef __riscv_32e
        addi  sp,  sp, -32 * REGBYTES
    #else
        addi  sp,  sp, -16 * REGBYTES
    #endif
    
  3. 将 sp 寄存器的值调整为 a0 指向地址的值,即传入的待切出线程的空余栈顶地址。

        STORE sp,  (a0)
    
  4. 将 x1 寄存器的值分别保存至线程栈结构体 struct rt_hw_stack_frame 中的第 0 个成员 rt_ubase_t epc 和第 1 个成员 rt_ubase_t ra

        STORE x1,   0 * REGBYTES(sp)
        STORE x1,   1 * REGBYTES(sp)
    

    在线程初始化 rt_hw_stack_init 中 x1 被赋值为参数 texit,对应 RT-Thread 内部函数 _thread_exit。

  5. 将 mstatus 寄存器读取至临时变量 a0 后做一些指定的运算,并保存至 struct rt_hw_stack_frame 中的第 0 个成员 rt_ubase_t mstatus

        csrr a0, mstatus                /* 读取 mstatus 寄存器值至 a0 */
        andi a0, a0, 8                  /*  a0 的值与 0x8 做逻辑与运算,mstatus 寄存器的 bit3  MIE(机器模式全局中断使能位) */
        beqz a0, save_mpie              /* a0 寄存器为 0 则跳转至 save_mpie */
        li   a0, 0x80                   /*  */
    save_mpie:
        STORE a0,   2 * REGBYTES(sp)    /*  a0 寄存器的值保存至线程栈对应的值 */
    
  6. 依次保存 x4~x31,f0~f31 寄存器的值。

  7. 将 sp 寄存器的值调整为 a1 指向地址的值,即传入的待切入线程的栈顶地址。

        LOAD sp,  (a1)
    
  8. 跳转至 rt_hw_context_switch_exit 函数。

        j rt_hw_context_switch_exit
    

    该函数在上面做过详细分解介绍,实现现场恢复。