一、 调度器概述与设计哲学

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对同优先级线程采用时间片轮询策略。其基本流程为:

    1. 调度器选择当前最高优先级就绪队列头部的线程运行。
    2. 为该线程启动时间片计时(remaining_tick)。
    3. 当时间片耗尽,该线程不会被挂起,而是被移动到同优先级就绪队列的尾部
    4. 调度器再从队列头部取出下一个线程运行。 这种“移至尾部”的操作保证了同优先级线程的调度是公平且可预测的,形成了A->B->C->A->B->C…的循环序列,这对于嵌入式系统的确定性至关重要。
  • 3. 调度闭环:系统节拍(SysTick)如何驱动调度器

  • 调度器并非“凭空”触发时间片轮转或延时唤醒,其底层动力来源于硬件定时器产生的周期性节拍中断。

    • 节拍中断入口:硬件定时器(如 Cortex-M 的 SysTick)每次溢出触发中断,执行 SysTick_Handlerrt_tick_increase()
    • 核心流程
      1. 全局节拍计数器 rt_tick +1
      2. 时间片递减:遍历当前运行线程的 remaining_tick,若 >0-1;若减至 0,则置位 RT_THREAD_STAT_YIELD_MASK 标志,不立即切换,而是等待当前线程主动调用 rt_schedule() 或中断返回时再决策。
      3. 延时线程唤醒:扫描延时链表(rt_thread_timer_list),将 timeout 到期的线程从延时态移至就绪态,更新 rt_thread_ready_priority_group 位图。
      4. 触发调度:若上述操作改变了最高就绪优先级,或唤醒了更高优先级线程,则调用 rt_schedule()。中断退出时通过 PendSV 完成实际切换。
  • 时间片耗尽不会立即抢占当前线程,而是打上 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. 调度发生的时机

  • 调度行为可分为主动调度和被动调度。

    1. 线程主动阻塞:当运行中的线程调用如rt_thread_delay()rt_sem_take()等函数而进入挂起或阻塞状态时,会主动放弃CPU,触发调度。
    2. 被更高优先级线程抢占:当一个比当前运行线程优先级更高的线程进入就绪态(例如,由中断唤醒或创建启动),系统会立即进行调度,让高优先级线程抢占CPU。
    3. 同优先级时间片耗尽或让出:同优先级线程的时间片用完,或被调用rt_thread_yield()主动让出CPU时,会在同优先级队列内进行轮转调度。
    4. 中断退出时:中断服务例程处理完毕退出时,是一个重要的调度点。如果中断唤醒了更高优先级的线程,可能会发生抢占。
  • 四、调度器初始化

  • 调度器初始化在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 个时钟周期。
  • 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
    
Logo

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

更多推荐