RT-Thread线程调度器内核
一、 调度器概述与设计哲学
RT-Thread的线程调度器是其实时操作系统的核心,它负责在多个就绪线程中做出仲裁,决定哪个线程获得CPU的执行权。其根本设计目标是确保高优先级任务能够获得及时响应,同时兼顾系统的公平性与确定性。
RT-Thread采用 全抢占式优先级 调度模型。这意味着,除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统中的其他部分(包括调度器自身)都是可以抢占的。当有比当前线程优先级更高的线程进入就绪状态时,当前线程会立即被换出,CPU资源将被高优先级线程抢占。
这种调度机制的核心支撑点是两个不可分割的线程属性:优先级和时间片。它们共同构成了调度器的决策依据,决定了线程何时能运行以及能运行多久。
二、 核心调度机制详解
1. 优先级(Priority):资源抢占的“法律条文”
RT-Thread的优先级体系是一个静态优先级抢占式模型,其规则非常明确:数值越小,优先级越高(0为最高优先级)。这与硬件中断优先级的设计逻辑保持一致。
- 优先级范围:系统默认支持256个优先级(0~255)。其中,优先级0通常保留给最关键的实时任务,而优先级255则固定分配给空闲线程,该线程在所有其他线程都挂起或阻塞时运行以维持系统最低功耗。
- 工程实践:在实际项目中,为了平衡功能与资源开销,常通过宏定义(如
RT_THREAD_PRIORITY_MAX)将可用优先级限制在更小的范围(例如8个或32个)。线程总数与优先级数量是两个独立的概念,多个线程可以共享同一个优先级。 - 作用:优先级划定了任务的“阶层”。它确保了像PWM波形生成这样的硬实时任务(高优先级)总能抢占像日志记录这样的后台任务(低优先级)。
2. 时间片(Tick):同级竞争
时间片机制的作用域被严格限定在相同优先级的线程集合内。当所有就绪线程优先级各不相同时,时间片参数失效,调度退化为纯抢占调度。
-
单位与含义:时间片的单位是系统节拍。其长度由
RT_TICK_PER_SECOND宏定义决定。例如,若该值为1000(即1ms一个tick),则线程的时间片参数tick=10表示其单次最大连续运行时长为10毫秒。 -
核心价值:防止同优先级线程独占CPU,确保同级任务的响应公平性。例如,防止GUI渲染线程长时间阻塞同级的音频解码线程。
-
调度策略
RT-Thread对同优先级线程采用时间片轮询策略。其基本流程为:
- 调度器选择当前最高优先级就绪队列头部的线程运行。
- 为该线程启动时间片计时(
remaining_tick)。 - 当时间片耗尽,该线程不会被挂起,而是被移动到同优先级就绪队列的尾部。
- 调度器再从队列头部取出下一个线程运行。 这种“移至尾部”的操作保证了同优先级线程的调度是公平且可预测的,形成了A->B->C->A->B->C…的循环序列,这对于嵌入式系统的确定性至关重要。
-
3. 调度闭环:系统节拍(SysTick)如何驱动调度器
-
调度器并非“凭空”触发时间片轮转或延时唤醒,其底层动力来源于硬件定时器产生的周期性节拍中断。
-
- 节拍中断入口:硬件定时器(如 Cortex-M 的 SysTick)每次溢出触发中断,执行
SysTick_Handler→rt_tick_increase()。 - 核心流程:
- 全局节拍计数器
rt_tick+1 - 时间片递减:遍历当前运行线程的
remaining_tick,若>0则-1;若减至0,则置位RT_THREAD_STAT_YIELD_MASK标志,不立即切换,而是等待当前线程主动调用rt_schedule()或中断返回时再决策。 - 延时线程唤醒:扫描延时链表(
rt_thread_timer_list),将timeout到期的线程从延时态移至就绪态,更新rt_thread_ready_priority_group位图。 - 触发调度:若上述操作改变了最高就绪优先级,或唤醒了更高优先级线程,则调用
rt_schedule()。中断退出时通过 PendSV 完成实际切换。
- 全局节拍计数器
- 节拍中断入口:硬件定时器(如 Cortex-M 的 SysTick)每次溢出触发中断,执行
-
时间片耗尽不会立即抢占当前线程,而是打上
YIELD标志。真正的抢占发生在该线程主动调用阻塞API、或中断唤醒更高优先级线程时。这保证了线程内部关键代码段的执行完整性。 -
三、 调度器的实现机理
-
1. 关键数据结构
-
- 线程控制块:每个线程都由一个
struct rt_thread控制块管理,其中包含了线程的所有信息,如栈指针、入口函数、状态、当前优先级、初始优先级以及剩余时间片等。 - 就绪队列数组:调度器内部维护了一个关键的数组
rt_thread_priority_table[RT_THREAD_PRIORITY_MAX]。数组的每个元素都是一个链表头,用于链接所有处于就绪状态的、具有相同优先级的线程,形成一个双向环形链表。
- 线程控制块:每个线程都由一个
-
2. 优先级查找算法:位图法
-
为了快速、恒定地找到最高优先级的就绪线程,RT-Thread采用了基于位图的优先级算法,其时间复杂度为O(1),与系统中有多少就绪线程无关。
-
- 位图变量:系统使用一个位图变量(如
rt_thread_ready_priority_group,32位)来标识哪些优先级上有就绪的线程。每一位代表一个优先级,置1表示该优先级有线程就绪。 - 查找过程:调度器通过查找位图变量中最低有效位为1的位置来确定最高优先级。这个过程通过一个预先生成的查找表
__lowest_bit_bitmap[]来实现,实现了极快的查表操作。但在 Cortex-M3/M4/M7 等带 DSP 扩展的核上,RT-Thread 默认会使用硬件CLZ(Count Leading Zeros)指令替代查表,实现真正的 O(1) 且无需占用 Flash 空间。
- 位图变量:系统使用一个位图变量(如
-
3. 调度发生的时机
-
调度行为可分为主动调度和被动调度。
-
- 线程主动阻塞:当运行中的线程调用如
rt_thread_delay()、rt_sem_take()等函数而进入挂起或阻塞状态时,会主动放弃CPU,触发调度。 - 被更高优先级线程抢占:当一个比当前运行线程优先级更高的线程进入就绪态(例如,由中断唤醒或创建启动),系统会立即进行调度,让高优先级线程抢占CPU。
- 同优先级时间片耗尽或让出:同优先级线程的时间片用完,或被调用
rt_thread_yield()主动让出CPU时,会在同优先级队列内进行轮转调度。 - 中断退出时:中断服务例程处理完毕退出时,是一个重要的调度点。如果中断唤醒了更高优先级的线程,可能会发生抢占。
- 线程主动阻塞:当运行中的线程调用如
-
四、调度器初始化
-
调度器初始化在int rtthread_startup()中调用,在上电初期运行。
-
void rt_system_scheduler_init(void) { rt_base_t offset; #ifndef RT_USING_SMP rt_scheduler_lock_nest = 0; //调度器锁,为0时允许调度,非0不允许调度 #endif /* RT_USING_SMP */ RT_DEBUG_LOG(RT_DEBUG_SCHEDULER, ("start scheduler: max priority 0x%02x\n", RT_THREAD_PRIORITY_MAX)); //将全局优先级链表初始化,比如说有32个优先级,0~31,每一个优先级都有一个自己的链表,而rt_thread_priority_table[0]就表示优先级0链表的头结点,这里将头结点指针指向自己。 for (offset = 0; offset < RT_THREAD_PRIORITY_MAX; offset ++) { rt_list_init(&rt_thread_priority_table[offset]); } //高优先级掩码 rt_thread_ready_priority_group = 0; } -
五、启动系统调度器
-
在调度器或其他IPC组件初始化完成后,则调用系统调度器启动函数(不考虑多核场景)。
-
void rt_system_scheduler_start(void) { struct rt_thread *to_thread; //要切换的线程控制块 rt_ubase_t highest_ready_priority;//最高优先级线程 //在这个地方是初始化,所以运行最高优先级线程为0,通过_scheduler_get_highest_priority_thread函数返回最高优先级线程的控制块指针 to_thread = _scheduler_get_highest_priority_thread(&highest_ready_priority); //记录,后续使用rt_current_thread标记当前运行线程 rt_current_thread = to_thread; //从就绪链表中移除 rt_schedule_remove_thread(to_thread); //标记为运行状态 to_thread->stat = RT_THREAD_RUNNING; //切换函数栈 rt_hw_context_switch_to((rt_ubase_t)&to_thread->sp); } -
可以看到是按照几个步骤来实现的,获取高优先级线程控制块(_scheduler_get_highest_priority_thread),记录并修改部分状态(rt_current_thread)(rt_schedule_remove_thread),切换函数栈运行(rt_hw_context_switch_to)。将一步一步开始解析,首先是如何获取高优先级现成控制块(不考虑多核场景)
-
static struct rt_thread* _scheduler_get_highest_priority_thread(rt_ubase_t *highest_prio) { struct rt_thread *highest_priority_thread;//临时线程控制块指针 rt_ubase_t highest_ready_priority; //__rt_ffs查表法,通过rt_thread_ready_priority_group在创建线程时设定的优先级组,获取最高优先级,也就是最低非0位。 highest_ready_priority = __rt_ffs(rt_thread_ready_priority_group) - 1; //根据最高的优先级组,从优先级链表中获取第一个线程控制块指针 highest_priority_thread = rt_list_entry(rt_thread_priority_table[highest_ready_priority].next, struct rt_thread, tlist); //记录 *highest_prio = highest_ready_priority; //返回 return highest_priority_thread; } //在kservice.c内核服务函数中,用于返回当前最高的优先级 /* __lowest_bit_bitmap 表将任何可能的8位数值的最低位索引计算 如0x00 二进制为00000000 最低非0的位置为0 0x01 二进制为00000001 最低非0的位置为0 0x02 二进制为00000010 最低非0的位置为1 等 因此只需要通过传入优先级,即可找到当前优先级的最低非0位是多少 比如说传入优先级为5,即00000101 最低非0位为0,__lowest_bit_bitmap[5]=0 通过这个返回的值就可以知道优先级 const rt_uint8_t __lowest_bit_bitmap[] = { /* 00 */ 0, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 10 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 20 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 30 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 40 */ 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 50 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 60 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 70 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 80 */ 7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 90 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* A0 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* B0 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* C0 */ 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* D0 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* E0 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* F0 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0 }; */ int __rt_ffs(int value) { if (value == 0) return 0; if (value & 0xff) return __lowest_bit_bitmap[value & 0xff] + 1; if (value & 0xff00) return __lowest_bit_bitmap[(value & 0xff00) >> 8] + 9; if (value & 0xff0000) return __lowest_bit_bitmap[(value & 0xff0000) >> 16] + 17; return __lowest_bit_bitmap[(value & 0xff000000) >> 24] + 25; } -
六、任务切换
-
我们已经通过__rt_ffs查表算法,得知了优先级状态表最低有效非0位的位置,得知该位置就是表示该优先级为当前调度的最高优先级,然后通过优先级链表(每个优先级都有一个自己的双向链表)反推出结构体首地址,得出该线程的控制块指针。一切准备就绪,接下来就是要修改cpu的切换方式,让cpu的pc指向该线程。切换函数为rt_hw_context_switch_to,这个属于汇编函数,定义在rthw.h中。需要注意的是
-
- 调度器只做逻辑决策(选线程、更新状态),不直接操作寄存器。
- 真正的切换由汇编函数完成:保存当前线程的 CPU 现场,加载新线程的现场,最后跳转到新线程执行。
-
/* * void rt_hw_context_switch_to(rt_uint32 to, struct rt_thread *to_thread); * r0 --> to (thread stack) * r1 --> to_thread */ .globl rt_hw_context_switch_to //全局伪指令,支持c文件调用 rt_hw_context_switch_to: //标号,声明函数 ldr sp, [r0] //加载r0所指向的栈地址的值加载到sp中 b rt_hw_context_switch_exit//跳转至rt_hw_context_switch_exit该标号 -
.global rt_hw_context_switch_exit //声明全局标号 rt_hw_context_switch_exit: //标号 ldmfd sp!, {r1} //从sp取一个字给r1,然后sp=sp+4 msr spsr_cxsf, r1 /* 更新SPSR状态寄存器 */ ldmfd sp!, {r0-r12,lr,pc}^ /* 将sp指向的栈帧出栈,直至CPU指向PC完成切换 */ -
步骤
-
1、将r0(传递进来的线程sp)复制给系统sp
-
2、将系统sp指向的值(CPSR)复制给R1,SP+4,此时SP指向R0
-
3、R1写入SPSR
-
4、从sp出弹出15个字,也就是弹出CPU的所有寄存器,触发弹出PC时跳转,此时PC流水线更新,从线程入口开始运行。
-
即 先从传递进来的线程sp,cpsr拷贝到spsr,然后栈帧递减直至pc弹出截止,pc更新执行线程入口。
-
思考一下几个问题: 1、CPSR、SPSR有什么作用,为什么必须要CPSR->SPSR 答:CPSR 程序状态寄存器(current program status register) 用户级编程时用于存储的条件码 SPSR 程序状态保存寄存器(saved program status register)用于在各种异常返回的时候保存CPSR的内容,方便返回 因为在ARM指令集中,状态寄存器是不允许被直接读取的,只能用专用指令访问。 2、在rt_hw_context_switch_exit中弹出了寄存器PC,为什么弹出了传入线程栈的PC就可以运行,弹出的数据去哪了,本质应该只是指针操作。 答:弹出的PC在弹出指令周期内,由硬件完成两个操作 2.1 更新CPU的pc指向弹出的PC地址 2.2 CPSR从SPSR取值更新 出栈时将数据路由到CPU的指定寄存器,直到更新到PC触发流水线。 -
以上是完成的初始化流程,不涉及调度器的实现,主要是完成以下几个步骤
-
1、在components.c中完成系统调度器的初始化,
-
调度器初始化主要是初始化优先级表以及空闲链表。
-
2、首次启动系统调度器
-
2.1获取高优先级线程的线程控制块
-
2.2完成任务切换,在汇编函数中完成CPSR的更新,并且CPU的栈帧切换至在初始化时的模拟栈帧。
-
七、 调度器实现
-
我们在使用RTOS时,通常是会在创建线程后启动系统运行,启动函数如下,在线程内核对象中已解析过。
-
rt_err_t rt_thread_startup(rt_thread_t thread) -
在该函数中会调用 rt_schedule();来启动调度器,从而实现优先级的快速查找的线程的切换。
-
RT-Thread很细心的区分了多核场景下和单核场景下的rt_schedule的实现,在进行调度器函数解析之前,先了解一下几个函数的实现。
-
进入临界区
-
void rt_enter_critical(void) { register rt_base_t level; level = rt_hw_interrupt_disable(); rt_scheduler_lock_nest ++; //用一个变量表示,当rt_scheduler_lock_nest非零表示锁住了 rt_hw_interrupt_enable(level); } #endif RTM_EXPORT(rt_enter_critical); -
退出临界区
-
void rt_exit_critical(void) { register rt_base_t level; level = rt_hw_interrupt_disable(); rt_scheduler_lock_nest --; if (rt_scheduler_lock_nest <= 0) { rt_scheduler_lock_nest = 0; rt_hw_interrupt_enable(level); if (rt_current_thread) { rt_schedule();/* 从临界区出来触发一次调度 */ } } else { rt_hw_interrupt_enable(level); } } #endif RTM_EXPORT(rt_exit_critical); -
对于调度器关键就是rt_scheduler_lock_nest是否为0,若为0则未上锁,非0则上锁。临界区只是关闭任务调度,并不会影响中断。内部开关中断是防止值被篡改,保证原子性。
-
调度器:以下为单核场景下的实现
-
void rt_schedule(void) { rt_base_t level; struct rt_thread *to_thread; struct rt_thread *from_thread;//指针,表示需要从from_thread切换至to_thread level = rt_hw_interrupt_disable();//防止中断服务程序(ISR)或高优先级任务在修改中途抢占,导致就绪队列链表断裂或状态不一致(竞态条件)。 /* 使用嵌套计数器而非布尔值,允许多层临界区嵌套。只有最外层临界区退出(计数器归0)时,才允许真正执行切换,避免“提前切换”破坏原子操作。*/ if (rt_scheduler_lock_nest == 0)//每一位代表一个优先级。若为 0,说明无用户线程就绪(仅空闲线程可能运行)。这是 O(1) 快速拦截,避免无谓的队列遍历和切换开销。 { rt_ubase_t highest_ready_priority; if (rt_thread_ready_priority_group != 0) { int need_insert_from_thread = 0; //获取当前最高优先级 to_thread = _scheduler_get_highest_priority_thread(&highest_ready_priority); //调度决策:继续运行当前线程 vs 切换 if ((rt_current_thread->stat & RT_THREAD_STAT_MASK) == RT_THREAD_RUNNING) { //当前 vs 就绪链表优先级 if (rt_current_thread->current_priority < highest_ready_priority) { //优先级比较,数值越小,优先级越高,继续运行 to_thread = rt_current_thread; } else if (rt_current_thread->current_priority == highest_ready_priority && (rt_current_thread->stat & RT_THREAD_STAT_YIELD_MASK) == 0)//同优先级并且时间片还未结束,继续运行 { to_thread = rt_current_thread; } else//有更高优先级就绪,或同优先级时间片已耗尽 { need_insert_from_thread = 1; } //每次调度决策后重置标志,为下一轮时间片计数做准备。 rt_current_thread->stat &= ~RT_THREAD_STAT_YIELD_MASK; } //执行切换准备(仅当目标线程 ≠ 当前线程),如果确定更换,此步进去 if (to_thread != rt_current_thread) { /* 记录 线程TCB 即将从from -> to*/ rt_current_priority = (rt_uint8_t)highest_ready_priority; from_thread = rt_current_thread; rt_current_thread = to_thread; RT_OBJECT_HOOK_CALL(rt_scheduler_hook, (from_thread, to_thread)); //将旧线程插入到就绪队列结尾,也就是就绪队列头结点之前 if (need_insert_from_thread) { //旧线程TCB,更改状态位就绪状态 rt_schedule_insert_thread(from_thread); } //将新线程从就绪链表中移除,即将运行新线程 rt_schedule_remove_thread(to_thread); to_thread->stat = RT_THREAD_RUNNING | (to_thread->stat & ~RT_THREAD_STAT_MASK); RT_DEBUG_LOG(RT_DEBUG_SCHEDULER, ("[%d]switch to priority#%d " "thread:%.*s(sp:0x%08x), " "from thread:%.*s(sp: 0x%08x)\n", rt_interrupt_nest, highest_ready_priority, RT_NAME_MAX, to_thread->name, to_thread->sp, RT_NAME_MAX, from_thread->name, from_thread->sp)); #ifdef RT_USING_OVERFLOW_CHECK//栈溢出检查 _scheduler_stack_check(to_thread); #endif /* RT_USING_OVERFLOW_CHECK */ if (rt_interrupt_nest == 0)//判断当前是否在 ISR 中,任务级调度可直接切换;中断级调度需延迟到 ISR 返回前执行,避免中断嵌套导致的上下文混乱。 { extern void rt_thread_handle_sig(rt_bool_t clean_state); RT_OBJECT_HOOK_CALL(rt_scheduler_switch_hook, (from_thread)); //汇编切换线程栈 rt_hw_context_switch((rt_ubase_t)&from_thread->sp, (rt_ubase_t)&to_thread->sp); rt_hw_interrupt_enable(level); goto __exit; } else { RT_DEBUG_LOG(RT_DEBUG_SCHEDULER, ("switch in interrupt\n")); //仅将 from/to 的 sp 保存到全局变量,并设置 rt_thread_switch_interrupt_flag。实际切换在 ISR 出口汇编中执行。原因:ISR 本身已压栈部分寄存器,直接切换会破坏中断返回现场;延迟切换保证 ISR 完整执行后再跳至新线程。 rt_hw_context_switch_interrupt((rt_ubase_t)&from_thread->sp, (rt_ubase_t)&to_thread->sp); } } else// to_thread == rt_current_thread { //不切换,也可能因超时/状态变化触发调度。需重新标记运行状态并维护就绪队列一致性。 rt_schedule_remove_thread(rt_current_thread); rt_current_thread->stat = RT_THREAD_RUNNING | (rt_current_thread->stat & ~RT_THREAD_STAT_MASK); } } } /* enable interrupt */ rt_hw_interrupt_enable(level); __exit: return; } -
八、PendSV延迟切换
-
PendSV(Pendable Service Call)是 ARMv7-M/ARMv8-M 架构专为操作系统任务切换设计的异常。RT-Thread 在 Cortex-M 平台完全依赖 PendSV 实现中断上下文的延迟安全切换。
-
1. 为什么需要 PendSV?
-
- 硬件自动压栈/出栈:异常进入时,CPU 硬件自动将
R0~R3, R12, LR, PC, xPSR压入当前线程栈;异常返回时自动弹出。操作系统只需手动保存/恢复R4~R11。 - 最低优先级保证原子性:PendSV 优先级通常配置为最低(如
0xFF)。这确保它只在所有高优先级中断全部执行完毕后才触发,避免中断嵌套导致栈帧混乱。 - Tail-Chaining 优化:若 PendSV 挂起时刚好有其他中断结束,CPU 会跳过完整的异常返回流程,直接进入 PendSV,节省 12 个时钟周期。
- 硬件自动压栈/出栈:异常进入时,CPU 硬件自动将
-
2. RT-Thread 中的 PendSV 触发机制
-
在 ISR 中调用
rt_schedule()时,不会立即切换任务,而是执行: -
rt_hw_context_switch_interrupt -
此时中断服务函数继续执行剩余代码。直到 ISR 退出前,CPU 检查异常挂起状态,发现 PendSV 已就绪,自动转入
PendSV_Handler。 -
3.PendSV_Handler核心汇编流程 -
在目前主流的架构设计中,在rt_schedule会调用rt_hw_context_switch来实现任务切换,在中断会调用rt_hw_context_switch_interrupt进行任务切换。
-
void rt_hw_context_switch(rt_ubase_t from, rt_ubase_t to) { // 记录 from/to 线程 TCB rt_interrupt_from_thread = (struct rt_thread *)from; rt_interrupt_to_thread = (struct rt_thread *)to; rt_thread_switch_interrupt_flag = 1; // 触发 PendSV 异常 (置位 ICSR 的 PENDSVSET 位) SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk; } -
以下是 RT-Thread Cortex-M 端口标准实现(GCC/ARMCC 通用逻辑):
-
PendSV_Handler LDR R0, =rt_thread_switch_interrupt_flag // R0 = &rt_thread_switch_interrupt_flag LDR R1, [R0] // R1 = rt_thread_switch_interrupt_flag 的值 CBZ R1, PendSV_exit // if (R1 == 0) goto PendSV_exit (无切换请求,直接退出) /* 2. 保存当前线程上下文(仅 R4-R11,R0-R3等已由硬件压栈) */ MRS R0, PSP // R0 = 当前线程栈指针 PSP (Process Stack Pointer) STMDB R0!, {R4-R11} // 将 R4~R11 压入 R0 指向的栈,R0 自动递减 (满递减栈) LDR R1, =rt_interrupt_from_thread// R1 = &rt_interrupt_from_thread LDR R1, [R1] // R1 = rt_interrupt_from_thread 的值 (旧线程 TCB 地址) STR R0, [R1] // from_thread->sp = R0 (保存更新后的旧线程栈顶) /* 3. 切换至目标线程 */ LDR R1, =rt_interrupt_to_thread// R1 = &rt_interrupt_to_thread LDR R1, [R1] // R1 = rt_interrupt_to_thread 的值 (新线程 TCB 地址) LDR R0, [R1] // R0 = to_thread->sp (读取新线程的栈顶指针) LDMIA R0!, {R4-R11} // 从新线程栈中弹出 R4~R11 到 CPU 寄存器,R0 自动递增 MSR PSP, R0 // 更新 PSP 指向新线程的栈 /* 4. 清理标志,准备异常返回 */ PendSV_exit: LDR R0, =rt_thread_switch_interrupt_flag// R0 = &rt_thread_switch_interrupt_flag MOV R1, #0// rt_thread_switch_interrupt_flag = 0 (清除切换标志) STR R1, [R0] //清除切换标志 ORR LR, LR, #0x04 //确保EXC_RETURN返回使用PSP栈 BX LR //执行异常返回,CPU 自动完成剩余寄存器出栈并跳转至新线程的 PC。 -
以下是任务级切换的流程,目前在Cortex-m3架构 任务切换和中断内的切换都是一样的走PendSV流程
-
[线程] 调用 rt_thread_delay() ↓ [内核] rt_schedule() → rt_hw_context_switch(A, B) ↓ [内核] 设置全局标志 + SCB->ICSR |= PENDSVSET ↓ (PendSV 被挂起,但优先级最低,不会立即打断线程A) [线程A] 继续执行 rt_schedule() 末尾的 rt_hw_interrupt_enable() ↓ [线程A] 函数返回,准备执行下一条 C 指令 ↓ (CPU 检查到 PendSV 挂起标志) [异常] 硬件自动压栈 (R0~R3, R12, LR, PC, xPSR) → 进入 PendSV_Handler ↓ [PendSV] 执行上述汇编流程:保存A的R4~R11 → 切换PSP → 恢复B的R4~R11 ↓ [PendSV] BX LR 触发异常返回 ↓ (硬件自动从B的PSP弹出 R0~R3, PC, xPSR) [线程B] 从上次挂起处继续执行 (或首次执行入口函数) -
九、ARM架构汇编设计
-
以上是M3架构下的设计,CPU通过PendSV异常的形式自动弹出栈帧。以下是ARM经典的架构的汇编。原理大致是一样的,都是保存上下文,切换CPU的sp指针。
-
前面介绍了rt_hw_context_switch_to用于初始化时的任务切换,直接将cpu的栈帧指向构造好的栈帧中。因此只有一个参数,而以下的rt_hw_context_switch,和中断下的rt_hw_context_switch_interrupt都有两个参数,分别是from和to,用来记录和追溯返回。
-
;/* ; * void rt_hw_context_switch(rt_uint32 from, rt_uint32 to); ; * r0 --> from ; * r1 --> to ; */ .def rt_hw_context_switch //RT-Thread 调度器 C 代码需通过全局符号调用此汇编函数。 .asmfunc rt_hw_context_switch //标号 STMDB sp!, {lr} ; 将当前运行的任务的LR压入栈中,此时sp指向的是from的任务栈,这个LR用于恢复 STMDB sp!, {r0-r12, lr} ; 保存CPU的r0~R12和LR压入栈中 MRS r4, cpsr ;读取CPSR状态 TST lr, #0x01 ;测试当前 lr 的最低位 ORRNE r4, r4, #0x20 ; 根据LR的最低位,区分Thumb模式和ARM模式 STMDB sp!, {r4} ; 压入CPSR STR sp, [r0] ; 将CPU指针保存在旧任务的TCB中 LDR sp, [r1] ; 读取新任务的栈指针到CPU_SP LDMIA sp!, {r4} ; 弹出之前的CPSR MSR spsr_cxsf, r4 LDMIA sp!, {r0-r12, lr, pc}^ ; pop new task r0-r12, lr & pc, copy spsr to cpsr -
分步骤如下:(假设当前CPU运行A,要切换至B,此时CPU要么处于A栈,要不处于中断栈中,此处为任务调用,因此只会在A栈中。因此SP指向的本质就是A栈)
-
1、保存当前CPU的上下文,先保存LR,用于任务返回时的PC,压入A栈
-
2、压入当前CPU的R0~R12和LR压入A栈
-
3、保存当前的CPSR到R4中,根据最低位判断指令集,然后压入A栈中,至此A栈上下文已保存
-
4、CPUSP切换至B栈,即R1寄存器的值
-
5、从B栈去除保存的SPSR
-
6、弹出B栈的上下文,替换到CPU的对应寄存器组,检测到PC时,CPU切换运行B线程,^表示硬件会自动完成寄存器赋值,并且自动完成SPSR给CPSR的赋值。
-
中断的设计是不一样的,在M4架构中有MSP的专用栈指针
-
其中r0 = from = &A.TCB r1 = to = &B.TCB
-
;/* ; * void rt_hw_context_switch_interrupt(rt_uint32 from, rt_uint32 to); ; */ .def rt_hw_context_switch_interrupt .asmfunc rt_hw_context_switch_interrupt LDR r2, pintflag // r2 = &rt_thread_switch_interrupt_flag LDR r3, [r2] // r3 = rt_thread_switch_interrupt_flag CMP r3, #1 // 比较标志是否为1 BEQ _reswitch // 若为1,直接跳转到_reswitch(只更新to线程) MOV r3, #1 // r3 = 1 STR r3, [r2] // 设置标志为1:rt_thread_switch_interrupt_flag = 1 LDR r2, pfromthread // r2 = &rt_interrupt_from_thread STR r0, [r2] // 保存当前线程TCB指针(from)到全局变量 //到此处表示需要更新from 和 to _reswitch LDR r2, ptothread // r2 = &rt_interrupt_to_thread STR r1, [r2] // 保存目标线程TCB指针(to)到全局变量 BX lr // 返回(不进行实际切换) .endasmfunc .def IRQ_Handler //中断产生会进入该汇编 IRQ_Handler STMDB sp!, {r0-r12,lr} // 保存中断现场到当前任务栈 BL rt_interrupt_enter // 进入中断,增加中断嵌套计数 BL rt_hw_trap_irq // 调用C语言的中断服务函数(可能会触发调度) BL rt_interrupt_leave // 退出中断,减少嵌套计数 // 检查是否需要切换任务 LDR r0, pintflag // r0 = &rt_thread_switch_interrupt_flag LDR r1, [r0] // r1 = 标志值 CMP r1, #1 // 是否需要切换?为1则跳转到真正的切换函数 BEQ rt_hw_context_switch_interrupt_do // 若需要,跳转到真正的切换函数 // 不需要切换,直接恢复现场并返回 LDMIA sp!, {r0-r12,lr} // 恢复寄存器 SUBS pc, lr, #4 // 中断返回(lr-4回到被中断的指令) .def rt_hw_context_switch_interrupt_do rt_hw_context_switch_interrupt_do MOV r1, #0 // r1 = 0 STR r1, [r0] // 将标志变量清零(rt_thread_switch_interrupt_flag = 0) LDMIA sp!, {r0-r12,lr} // 弹出 IRQ_Handler 中保存的现场到 r0~r12, lr // 此时 sp 恢复为进入 IRQ_Handler 之前的值 // lr 得到中断返回地址(被中断线程的 PC+4) STMDB sp, {r0-r3} // 暂存 r0~r3(不移动 sp) SUB r1, sp, #16 // r1 = sp-16,指向暂存区的起始 SUB r2, lr, #4 // r2 = 被中断线程的 PC(即线程A的返回地址) MRS r3, spsr // r3 = 被中断线程的 CPSR ; switch to SVC mode and no interrupt CPSID IF, #0x13 // 切换到 SVC 模式,关中断 STMDB sp!, {r2} // 压入 PC STMDB sp!, {r4-r12,lr} // 压入 r4~r12 及 lr(占位) LDMIA r1!, {r4-r7} // 从暂存区恢复原 r0~r3 到 r4~r7 STMDB sp!, {r4-r7} // 压入 r0~r3 STMDB sp!, {r3} // 压入 CPSR LDR r4, pfromthread LDR r5, [r4] // r5 = 线程A的TCB地址 STR sp, [r5] // 保存 sp(线程A的上下文被冻结) LDR r6, ptothread LDR r6, [r6]// r6 = 线程B的TCB地址 LDR sp, [r6] // sp 指向线程B之前保存的栈顶(包含完整上下文) LDMIA sp!, {r4} // 弹出B的CPSR MSR spsr_cxsf, r4 // 写入 SPSR LDMIA sp!, {r0-r12,lr,pc}^ // 弹出B的寄存器,同时恢复CPSR,跳转到B上次被切换的位置 -
假如有一个ATask和一个BTask,那么这个控制流如下:
-
硬件中断 → IRQ_Handler ├─ 保存 r0~r12,lr ├─ rt_interrupt_enter() ├─ rt_hw_trap_irq() → 可能调用 rt_schedule() │ └─ rt_hw_context_switch_interrupt() │ └─ 设置标志、保存from/to ├─ rt_interrupt_leave() └─ 检查标志 == 1 → 跳转到 rt_hw_context_switch_interrupt_do ├─ 清除标志 ├─ 弹出中断现场 ├─ 构造标准上下文 ├─ 保存到 from TCB ├─ 加载 to TCB 的 sp └─ 恢复 to 上下文并跳转 → 执行线程B
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)