一、前言

在嵌入式实时操作系统(RTOS)中,线程(Thread) 是最基本的调度单位。理解线程的定义和切换机制,是掌握 RTOS 内核原理的关键。本文将通过分析 RT-Thread 的源码,详细讲解:

  1. 线程控制块的定义
  2. 线程栈的初始化
  3. 调度器的实现
  4. 上下文切换的汇编实现

二、核心数据结构

2.1 线程控制块(Thread Control Block)

线程控制块是操作系统管理线程的核心数据结构,在 rtdef.h 中定义:

struct rt_thread
{
    void        *sp;              /* 线程栈指针 */
    void        *entry;           /* 线程入口地址 */
    void        *parameter;       /* 线程形参 */    
    void        *stack_addr;      /* 线程栈起始地址 */
    rt_uint32_t stack_size;       /* 线程栈大小,单位为字节 */

    rt_list_t   tlist;            /* 线程链表节点 */
};
typedef struct rt_thread *rt_thread_t;

成员说明:

成员 说明
sp 栈指针(Stack Pointer),指向线程栈的当前位置
entry 线程入口函数地址
parameter 传递给线程入口函数的参数
stack_addr 线程栈的起始地址(低地址)
stack_size 线程栈的总大小
tlist 双向链表节点,用于将线程挂载到各种队列(就绪列表、阻塞列表等)

2.2 双向链表

RT-Thread 使用双向链表管理线程队列,定义在 rtdef.h

struct rt_list_node
{
    struct rt_list_node *next;    /* 指向后一个节点 */
    struct rt_list_node *prev;    /* 指向前一个节点 */
};
typedef struct rt_list_node rt_list_t;

链表操作函数定义在 rtservice.h 中,包括初始化、插入、删除等操作。

关键宏 rt_list_entry

/* 已知一个结构体变量中某个成员的地址,求出该结构变量的首地址 */
#define rt_container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member)))

#define rt_list_entry(node, type, member) \
    rt_container_of(node, type, member)

参数含义

  • ptr:已知的结构体成员(链表节点)的地址
  • type:外层结构体的类型
  • member:该成员在结构体中的名字

计算逻辑

  1. (type *)0:把地址 0 强制转换成 type*,相当于 “虚拟” 一个 type 类型的结构体在地址 0 处。
  2. &((type *)0)->member:取这个虚拟结构体中 member 成员的地址,结果就是该成员在结构体中的偏移量(单位:字节)。
  3. (char *)(ptr):把 ptr 转成 char*,保证指针减法以字节为单位。
  4. ptr 地址减去偏移量,得到外层结构体的首地址,再强转为 type*

这个宏的作用是通过链表节点指针获取包含该节点的结构体指针,是内核中非常常用的技巧。


三、线程栈初始化

3.1 栈帧结构

在 Cortex-M3 架构中,当发生异常(如 PendSV)时,硬件会自动保存一部分寄存器到栈中,软件需要手动保存剩余的寄存器。

1. 手动保存的寄存器(r4~r11)

struct stack_frame
{
    /* r4 ~ r11 register,异常发生时需要手动保存的寄存器 */
    rt_uint32_t r4;
    rt_uint32_t r5;
    rt_uint32_t r6;
    rt_uint32_t r7;
    rt_uint32_t r8;
    rt_uint32_t r9;
    rt_uint32_t r10;
    rt_uint32_t r11;

    struct exception_stack_frame exception_stack_frame;
};

2. 自动保存的寄存器(异常栈帧)

struct exception_stack_frame
{
    /* 异常发生时硬件自动保存的寄存器 */
    rt_uint32_t r0;
    rt_uint32_t r1;
    rt_uint32_t r2;
    rt_uint32_t r3;
    rt_uint32_t r12;
    rt_uint32_t lr;   /* 链接寄存器 */
    rt_uint32_t pc;   /* 程序计数器 */
    rt_uint32_t psr;  /* 程序状态寄存器 */
};

栈帧布局(从高地址到低地址):

高地址
+------------------+
|       PSR        |  <- 栈顶(高地址)
+------------------+
|       PC         |  <- 线程入口地址
+------------------+
|       LR         |  <- 链接寄存器
+------------------+
|       R12        |
+------------------+
|       R3         |
+------------------+
|       R2         |
+------------------+
|       R1         |
+------------------+
|       R0         |  <- 线程参数(通过R0传递)
+------------------+
|       R11        |
+------------------+
|       R10        |
+------------------+
|       R9         |
+------------------+
|       R8         |
+------------------+
|       R7         |
+------------------+
|       R6         |
+------------------+
|       R5         |
+------------------+
|       R4         |  <- 初始化后的栈指针SP指向这里(低地址)
+------------------+
低地址

注意:ARM Cortex-M 的栈是从高地址向低地址增长的(向下增长)。

3.2 栈初始化函数

rt_hw_stack_init() 函数在 cpuport.c 中实现:

rt_uint8_t *rt_hw_stack_init(void       *tentry,
                             void       *parameter,
                             rt_uint8_t *stack_addr)
{
    struct stack_frame *stack_frame;
    rt_uint8_t         *stk;
    unsigned long       i;

    /* 获取栈顶指针(传入的stack_addr是栈的最高地址) */
    stk  = stack_addr + sizeof(rt_uint32_t);

    /* 将stk指针对齐8字节(AAPCS标准要求) */
    stk  = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);

    /* stk指针向下移动sizeof(struct stack_frame)个偏移,为栈帧分配空间 */
    stk -= sizeof(struct stack_frame);

    /* 将stk指针强制转换为stack_frame类型 */
    stack_frame = (struct stack_frame *)stk;

    /* 初始化栈空间为0xdeadbeef(便于调试时识别未使用的栈空间) */
    for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
    {
        ((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;
    }

    /* 初始化异常发生时自动保存的寄存器 */
    stack_frame->exception_stack_frame.r0  = (unsigned long)parameter; /* r0: 参数 */
    stack_frame->exception_stack_frame.r1  = 0;                        /* r1 */
    stack_frame->exception_stack_frame.r2  = 0;                        /* r2 */
    stack_frame->exception_stack_frame.r3  = 0;                        /* r3 */
    stack_frame->exception_stack_frame.r12 = 0;                        /* r12 */
    stack_frame->exception_stack_frame.lr  = 0;                        /* lr */
    stack_frame->exception_stack_frame.pc  = (unsigned long)tentry;    /* 入口地址 */
    stack_frame->exception_stack_frame.psr = 0x01000000L;              /* PSR */

    /* 返回线程栈指针(指向stack_frame的起始地址,即r4的位置) */
    return stk;
}

关键点解析:

  1. 8字节对齐:Cortex-M3 的 AAPCS(ARM Architecture Procedure Call Standard)标准要求栈必须8字节对齐,这是为了保证某些指令(如 LDRD/STRD)能正确执行。

  2. 0xdeadbeef:这是一个特殊的魔数(Magic Number),用于初始化栈空间。在调试时,如果看到栈中出现这个值,说明该位置尚未被使用过。

  3. PSR = 0x01000000:设置 Thumb 模式位(bit24)。Cortex-M3 只支持 Thumb-2 指令集,必须确保在 Thumb 状态下执行。

  4. R0 = parameter:根据 AAPCS 标准,函数的第一个参数通过 R0 寄存器传递。


四、线程初始化

4.1 线程初始化函数

rt_thread_init() 函数在 thread.c 中实现:

rt_err_t rt_thread_init(struct rt_thread *thread,
                        void (*entry)(void *parameter),
                        void             *parameter,
                        void             *stack_start,
                        rt_uint32_t       stack_size)
{
    /* 初始化线程链表节点 */
    rt_list_init(&(thread->tlist));

    /* 保存线程入口函数和参数 */
    thread->entry = (void *)entry;
    thread->parameter = parameter;

    /* 保存栈信息 */
    thread->stack_addr = stack_start;
    thread->stack_size = stack_size;

    /* 初始化线程栈,并返回线程栈指针 */
    /* 注意:传入的是栈顶地址(高地址) */
    thread->sp = (void *)rt_hw_stack_init( thread->entry, 
                                           thread->parameter,
                                           (void *)((char *)thread->stack_addr + thread->stack_size - 4) );

    return RT_EOK;
}

流程说明:

  1. 初始化链表节点:将线程的 tlist 初始化为空链表(next 和 prev 都指向自己)
  2. 保存线程函数信息:记录线程入口地址和参数
  3. 保存栈信息:记录栈的起始地址和大小
  4. 初始化栈:调用 rt_hw_stack_init() 初始化栈帧,并将返回的栈指针保存到 sp

重要说明

  • stack_start 是栈的起始地址(低地址)
  • stack_start + stack_size - 4 计算出栈顶地址(高地址)
  • 栈向下增长,所以初始化时从高地址开始布局

五、调度器实现

5.1 调度器初始化

rt_system_scheduler_init() 函数在 scheduler.c 中实现:

/* 线程就绪列表(按优先级组织,每个优先级一个链表) */
rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];

/* 当前运行线程的控制块指针 */
struct rt_thread *rt_current_thread;

/* 线程休眠列表(用于存放被删除的线程) */
rt_list_t rt_thread_defunct;

void rt_system_scheduler_init(void)
{    
    register rt_base_t offset;    

    /* 初始化就绪列表:每个优先级的链表都初始化为空 */
    for (offset = 0; offset < RT_THREAD_PRIORITY_MAX; offset ++)
    {
        rt_list_init(&rt_thread_priority_table[offset]);
    }

    /* 初始化当前线程控制块指针为空 */
    rt_current_thread = RT_NULL;

    /* 初始化线程休眠列表 */
    rt_list_init(&rt_thread_defunct);
}

配置参数(在 rtconfig.h 中定义):

#define RT_THREAD_PRIORITY_MAX  32     /* 最大优先级数 */
#define RT_ALIGN_SIZE           4      /* 最小对齐字节数 */

5.2 启动调度器

rt_system_scheduler_start() 函数:

void rt_system_scheduler_start(void)
{
    register struct rt_thread *to_thread;

    /* 手动指定第一个运行的线程(从优先级0的链表中取出第一个线程) */
    to_thread = rt_list_entry(rt_thread_priority_table[0].next,
                              struct rt_thread,
                              tlist);
    rt_current_thread = to_thread;

    /* 切换到第一个线程,该函数是汇编实现,不会返回 */
    rt_hw_context_switch_to((rt_uint32_t)&to_thread->sp);
}

5.3 线程调度

rt_schedule() 函数实现简单的线程切换:

void rt_schedule(void)
{
    struct rt_thread *to_thread;
    struct rt_thread *from_thread;

    /* 两个线程轮流切换:
     * 如果当前是优先级0的线程,则切换到优先级1的线程
     * 如果当前是优先级1的线程,则切换到优先级0的线程
     */
    if( rt_current_thread == rt_list_entry( rt_thread_priority_table[0].next,
                                            struct rt_thread,
                                            tlist) )
    {
        from_thread = rt_current_thread;
        to_thread = rt_list_entry( rt_thread_priority_table[1].next,
                                   struct rt_thread,
                                   tlist);
        rt_current_thread = to_thread;
    }
    else
    {
        from_thread = rt_current_thread;
        to_thread = rt_list_entry( rt_thread_priority_table[0].next,
                                   struct rt_thread,
                                   tlist);
        rt_current_thread = to_thread;
    }

    /* 触发上下文切换(汇编实现) */
    rt_hw_context_switch((rt_uint32_t)&from_thread->sp,(rt_uint32_t)&to_thread->sp);
}

注意:这是一个简化版的调度器,仅用于演示两个线程的轮流切换。实际 RT-Thread 的调度器会遍历所有优先级找到最高优先级的就绪线程。


六、上下文切换(汇编实现)

上下文切换是 RTOS 最核心的功能,使用汇编语言在 context_rvds.s 中实现。

6.1 相关寄存器定义

; 系统控制块(SCB)寄存器地址
SCB_VTOR        EQU     0xE000ED08     ; 向量表偏移寄存器
NVIC_INT_CTRL   EQU     0xE000ED04     ; 中断控制状态寄存器
NVIC_SYSPRI2    EQU     0xE000ED20     ; 系统优先级寄存器(2)
NVIC_PENDSV_PRI EQU     0x00FF0000     ; PendSV 优先级值(设置为最低优先级)
NVIC_PENDSVSET  EQU     0x10000000     ; 触发PendSV异常的位

为什么使用 PendSV 进行上下文切换?

PendSV(Pending Supervisor Call)是 Cortex-M 专门设计用于上下文切换的异常:

  • 优先级最低:可以确保在其他所有中断处理完成后才执行上下文切换
  • 可挂起:可以在任何时候触发,但会等到当前中断处理完成后再执行

6.2 切换到第一个线程

rt_hw_context_switch_to() 函数用于启动第一个线程:

rt_hw_context_switch_to    PROC
    EXPORT rt_hw_context_switch_to

    ; 保存目标线程栈指针的地址到 rt_interrupt_to_thread
    LDR     r1, =rt_interrupt_to_thread
    STR     r0, [r1]

    ; 设置源线程为0(表示这是第一次切换,不需要保存上下文)
    LDR     r1, =rt_interrupt_from_thread
    MOV     r0, #0x0
    STR     r0, [r1]

    ; 设置中断标志位为1,表示需要切换
    LDR     r1, =rt_thread_switch_interrupt_flag
    MOV     r0, #1
    STR     r0, [r1]

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

    ; 触发 PendSV 异常(启动线程切换)
    LDR     r0, =NVIC_INT_CTRL
    LDR     r1, =NVIC_PENDSVSET
    STR     r1, [r0]

    ; 开中断(允许 PendSV 执行)
    CPSIE   F
    CPSIE   I

    ; 永远不会返回到这里,因为已经切换到线程运行
    ENDP

6.3 上下文切换

rt_hw_context_switch() 函数用于两个线程之间的切换:

rt_hw_context_switch    PROC
    EXPORT rt_hw_context_switch

    ; 检查中断标志位是否已为1(防止重复设置)
    LDR     r2, =rt_thread_switch_interrupt_flag
    LDR     r3, [r2]
    CMP     r3, #1
    BEQ     _reswitch           ; 如果已经是1,直接跳转到_reswitch
    MOV     r3, #1
    STR     r3, [r2]            ; 设置标志位为1

    ; 保存源线程栈指针的地址
    LDR     r2, =rt_interrupt_from_thread
    STR     r0, [r2]

_reswitch
    ; 保存目标线程栈指针的地址
    LDR     r2, =rt_interrupt_to_thread
    STR     r1, [r2]

    ; 触发PendSV异常实现上下文切换
    LDR     r0, =NVIC_INT_CTRL
    LDR     r1, =NVIC_PENDSVSET
    STR     r1, [r0]

    ; 从子程序返回(异常发生后会切换到新线程)
    BX      LR
    ENDP

6.4 PendSV 异常处理

PendSV_Handler 是实现上下文切换的核心代码:

PendSV_Handler   PROC
    EXPORT PendSV_Handler

    ; 保存当前中断状态,并禁用中断(防止嵌套)
    MRS     r2, PRIMASK
    CPSID   I

    ; 获取中断标志位,检查是否需要切换
    LDR     r0, =rt_thread_switch_interrupt_flag
    LDR     r1, [r0]
    CBZ     r1, pendsv_exit     ; 如果为0,不需要切换,直接退出

    ; 清空中断标志位
    MOV     r1, #0x00
    STR     r1, [r0]

    ; 检查rt_interrupt_from_thread是否为0
    LDR     r0, =rt_interrupt_from_thread
    LDR     r1, [r0]
    CBZ     r1, switch_to_thread  ; 为0表示第一次切换,不需要保存上下文

; ========================== 保存当前线程上下文 ==============================
    ; 当进入 PendSV Handler 时,硬件已经自动保存了以下寄存器到当前线程栈:
    ; xPSR、PC、LR、R12、R3、R2、R1、R0
    ; 现在需要手动保存 R4~R11

    MRS     r1, psp             ; 获取当前线程的栈指针(PSP)到 r1
    STMFD   r1!, {r4 - r11}     ; 将 R4~R11 保存到线程栈(STMFD:先减后存)
    LDR     r0, [r0]            ; r0 = rt_interrupt_from_thread(栈指针的地址)
    STR     r1, [r0]            ; 将更新后的栈指针保存到线程控制块

; ========================== 恢复目标线程上下文 ==============================
switch_to_thread
    LDR     r1, =rt_interrupt_to_thread
    LDR     r1, [r1]            ; 获取目标线程栈指针的地址
    LDR     r1, [r1]            ; 获取目标线程的栈指针值

    LDMFD   r1!, {r4 - r11}     ; 从线程栈恢复 R4~R11(LDMFD:先读后增)
    MSR     psp, r1             ; 更新 PSP 为新的栈指针

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

    ORR     lr, lr, #0x04       ; 设置 LR 的 bit2,确保异常返回时使用 PSP
    BX      lr                  ; 异常返回,硬件会自动恢复 R0~R3、R12、LR、PC、xPSR
    ENDP

上下文切换的完整流程:

线程A运行中
    |
    v
调用 rt_schedule() 决定切换到线程B
    |
    v
调用 rt_hw_context_switch(&from_sp, &to_sp)
    |
    v
触发 PendSV 异常
    |
    v
进入 PendSV_Handler:
    1. 保存线程A的 R4~R11 到线程A的栈
    2. 将线程A的 SP 保存到线程A控制块
    3. 从线程B的栈恢复 R4~R11
    4. 将线程B的 SP 加载到 PSP
    |
    v
执行 BX LR 异常返回
    |
    v
硬件自动从线程B的栈恢复 R0~R3、R12、LR、PC、xPSR
    |
    v
线程B开始运行(从 PC 指向的地址开始执行)

双栈机制说明:

Cortex-M3 支持双栈机制:

  • MSP(Main Stack Pointer):主栈指针,用于中断处理(Handler Mode)
  • PSP(Process Stack Pointer):进程栈指针,用于线程运行(Thread Mode)

在 RTOS 中:

  • 中断处理使用 MSP(在启动文件 startup_ARMCM3.s 中设置)
  • 线程运行使用 PSP(每个线程有自己的栈空间)
  • ORR lr, lr, #0x04 设置 LR 的 bit2,告诉 CPU 异常返回后使用 PSP

七、main函数流程

int main(void)
{    
    /* 调度器初始化:初始化就绪列表、当前线程指针等 */
    rt_system_scheduler_init();

    /* 初始化线程1 */
    rt_thread_init( &rt_flag1_thread,                 /* 线程控制块 */
                    flag1_thread_entry,               /* 线程入口函数 */
                    RT_NULL,                          /* 线程参数 */
                    &rt_flag1_thread_stack[0],        /* 线程栈起始地址 */
                    sizeof(rt_flag1_thread_stack) );  /* 线程栈大小 */
    /* 将线程1插入就绪列表(优先级0) */
    rt_list_insert_before( &(rt_thread_priority_table[0]), &(rt_flag1_thread.tlist) );

    /* 初始化线程2 */
    rt_thread_init( &rt_flag2_thread,                 /* 线程控制块 */
                    flag2_thread_entry,               /* 线程入口函数 */
                    RT_NULL,                          /* 线程参数 */
                    &rt_flag2_thread_stack[0],        /* 线程栈起始地址 */
                    sizeof(rt_flag2_thread_stack) );  /* 线程栈大小 */
    /* 将线程2插入就绪列表(优先级1) */
    rt_list_insert_before( &(rt_thread_priority_table[1]), &(rt_flag2_thread.tlist) );

    /* 启动系统调度器(不再返回) */
    rt_system_scheduler_start(); 
}

线程函数示例:

/* 线程1:控制 flag1 变量翻转 */
void flag1_thread_entry(void *p_arg)
{
    for(;;)
    {
        flag1 = 1;
        delay(100);        
        flag1 = 0;
        delay(100);

        /* 主动放弃CPU,切换到其他线程 */
        rt_schedule();
    }
}

/* 线程2:控制 flag2 变量翻转 */
void flag2_thread_entry(void *p_arg)
{
    for(;;)
    {
        flag2 = 1;
        delay(100);        
        flag2 = 0;
        delay(100);

        /* 主动放弃CPU,切换到其他线程 */
        rt_schedule();
    }
}

八、总结

8.1 核心概念

概念 说明
线程控制块(TCB) 线程的"身份证",包含 sp、entry、stack 等线程运行所需的所有信息
线程栈 每个线程私有的栈空间,用于保存局部变量和寄存器上下文
就绪列表 按优先级组织的双向链表数组,管理所有等待运行的线程
上下文切换 保存当前线程的寄存器状态,恢复下一个线程的寄存器状态
PendSV Cortex-M 专门用于上下文切换的异常,优先级最低

8.2 关键技术点

  1. PendSV 异常机制
  • 优先级设为最低(0xFF),确保在其他中断处理完成后才执行
  • 通过向 NVIC_INT_CTRL 写入 0x10000000 触发
  1. 双栈机制(MSP/PSP)
  • MSP:主栈指针,用于中断处理
  • PSP:进程栈指针,用于线程运行
  • 通过设置 LR 的 bit2 切换
  1. 寄存器保存策略
  • 硬件自动保存:xPSR、PC、LR、R12、R3~R0(进入异常时)
  • 软件手动保存:R4~R11(在 PendSV_Handler 中)
  1. 栈的增长方向
  • ARM Cortex-M 栈向下增长(从高地址到低地址)
  • STMFD(先减后存)用于保存,LDMFD(先读后增)用于恢复
  1. AAPCS 调用约定
  • 函数参数通过 R0~R3 传递
  • 栈必须8字节对齐
  • 使用 Thumb-2 指令集(PSR bit24 = 1)

九、参考

  • 《RT-Thread内核实现与应用开发实战指南》- 野火
  • 《Cortex-M3权威指南》- Joseph Yiu
  • RT-Thread官方文档
  • ARM Architecture Procedure Call Standard (AAPCS)
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐