RT-Thread开发:线程定义与线程切换实现详解
一、前言
在嵌入式实时操作系统(RTOS)中,线程(Thread) 是最基本的调度单位。理解线程的定义和切换机制,是掌握 RTOS 内核原理的关键。本文将通过分析 RT-Thread 的源码,详细讲解:
- 线程控制块的定义
- 线程栈的初始化
- 调度器的实现
- 上下文切换的汇编实现
二、核心数据结构
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:该成员在结构体中的名字
计算逻辑:
(type *)0:把地址0强制转换成type*,相当于 “虚拟” 一个type类型的结构体在地址0处。&((type *)0)->member:取这个虚拟结构体中member成员的地址,结果就是该成员在结构体中的偏移量(单位:字节)。(char *)(ptr):把ptr转成char*,保证指针减法以字节为单位。- 用
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;
}
关键点解析:
-
8字节对齐:Cortex-M3 的 AAPCS(ARM Architecture Procedure Call Standard)标准要求栈必须8字节对齐,这是为了保证某些指令(如 LDRD/STRD)能正确执行。
-
0xdeadbeef:这是一个特殊的魔数(Magic Number),用于初始化栈空间。在调试时,如果看到栈中出现这个值,说明该位置尚未被使用过。
-
PSR = 0x01000000:设置 Thumb 模式位(bit24)。Cortex-M3 只支持 Thumb-2 指令集,必须确保在 Thumb 状态下执行。
-
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;
}
流程说明:
- 初始化链表节点:将线程的
tlist初始化为空链表(next 和 prev 都指向自己) - 保存线程函数信息:记录线程入口地址和参数
- 保存栈信息:记录栈的起始地址和大小
- 初始化栈:调用
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 关键技术点
- PendSV 异常机制
- 优先级设为最低(0xFF),确保在其他中断处理完成后才执行
- 通过向
NVIC_INT_CTRL写入0x10000000触发
- 双栈机制(MSP/PSP)
- MSP:主栈指针,用于中断处理
- PSP:进程栈指针,用于线程运行
- 通过设置 LR 的 bit2 切换
- 寄存器保存策略
- 硬件自动保存:xPSR、PC、LR、R12、R3~R0(进入异常时)
- 软件手动保存:R4~R11(在 PendSV_Handler 中)
- 栈的增长方向
- ARM Cortex-M 栈向下增长(从高地址到低地址)
STMFD(先减后存)用于保存,LDMFD(先读后增)用于恢复
- AAPCS 调用约定
- 函数参数通过 R0~R3 传递
- 栈必须8字节对齐
- 使用 Thumb-2 指令集(PSR bit24 = 1)
九、参考
- 《RT-Thread内核实现与应用开发实战指南》- 野火
- 《Cortex-M3权威指南》- Joseph Yiu
- RT-Thread官方文档
- ARM Architecture Procedure Call Standard (AAPCS)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)