4、SMP调度器:RT-Thread SMP调度器架构、负载均衡策略、线程迁移、优先级继承
好,咱们进入SMP调度器这个核心话题。说实话,这部分内容我当年啃了很久才真正吃透。单核调度你只要管好一个CPU就行,但多核环境下,调度器要同时管好几个CPU,还要保证它们别闲着、别打架。嗯,这里面的门道不少。
4.1 RT-Thread SMP调度器整体架构
RT-Thread的SMP调度器,说白了就是给每个CPU核心都配了一个独立的就绪队列。每个核心都有自己的优先级位图,自己管自己的线程。但调度器本身是全局统一的——所有核心共享同一套调度算法逻辑。
我画个简化的架构图给你看:
+------------------+ +------------------+ +------------------+
| CPU0 | | CPU1 | | CPU2 |
| +------------+ | | +------------+ | | +------------+ |
| | 就绪队列0 | | | | 就绪队列1 | | | | 就绪队列2 | |
| | 优先级位图0 | | | | 优先级位图1 | | | | 优先级位图2 | |
| +------------+ | | +------------+ | | +------------+ |
| | 当前线程0 | | | | 当前线程1 | | | | 当前线程2 | |
| +------------+ | | +------------+ | | +------------+ |
+------------------+ +------------------+ +------------------+
| | |
+-----------------------+-----------------------+
|
+-----------------+
| 全局调度锁 |
| 线程迁移决策 |
| 负载均衡触发 |
+-----------------+
每个核心独立运行调度器,但通过全局调度锁来同步关键操作。我在项目中遇到过一个问题:两个核心同时触发线程迁移,结果把同一个线程抢来抢去。后来加了迁移状态标志才解决。
4.2 负载均衡策略
负载均衡,说白了就是让每个核心干的活差不多。别一个核心忙死,另一个核心闲死。RT-Thread的负载均衡策略主要有两种:
4.2.1 周期性负载均衡
系统每隔一段时间(比如每个系统Tick)检查一次各核心的负载情况。怎么算负载?看就绪队列里线程的数量和优先级权重。
我习惯用一个简单的公式:
负载值 = 就绪线程数 × 平均优先级权重
当某个核心的负载值超过平均值一定阈值(比如20%),就触发迁移。
4.2.2 事件触发负载均衡
有些场景下周期性检查不够及时。比如一个高优先级线程突然就绪,而某个核心正闲着。这时候应该立即触发迁移。
RT-Thread在以下事件中会检查负载均衡:
- 线程创建时
- 线程唤醒时
- 线程优先级改变时
- 线程退出时
4.3 线程迁移
线程迁移就是把一个线程从一个核心的就绪队列挪到另一个核心。听起来简单,但实现起来坑不少。
4.3.1 迁移条件
不是所有线程都能随便迁移。RT-Thread里线程有个cpu_affinity属性,表示它允许在哪些核心上运行。如果线程绑定了CPU0,那就不能把它迁到CPU1。
迁移的基本条件:
- 目标核心的负载低于源核心
- 线程允许在目标核心上运行(亲和性检查)
- 线程当前不在临界区或中断上下文
- 线程不是正在运行的(不能把正在执行的线程抢走)
4.3.2 迁移过程
迁移过程我拆成三步:
- 摘除:从源核心的就绪队列中移除线程
- 插入:将线程插入目标核心的就绪队列
- 触发:如果目标核心空闲,触发调度
代码层面大概是这样:
static void rt_scheduler_migrate_thread(struct rt_thread *thread,
rt_uint8_t target_cpu)
{
rt_base_t level;
// 关中断,防止竞争
level = rt_hw_interrupt_disable();
// 1. 从源核心队列移除
rt_scheduler_remove_thread(thread);
// 2. 更新线程的绑核信息
thread->bind_cpu = target_cpu;
// 3. 插入目标核心队列
rt_scheduler_insert_thread(thread);
// 4. 如果目标核心空闲,触发调度
if (rt_cpu_self() != target_cpu) {
rt_hw_ipi_send(RT_SCHEDULE_IPI, target_cpu);
}
rt_hw_interrupt_enable(level);
}
4.4 优先级继承
优先级继承是解决优先级反转问题的经典方案。在SMP环境下,这个问题变得更复杂了。
4.4.1 单核优先级继承
先回顾下单核的情况。假设有三个线程:T1(高优先级)、T2(中优先级)、T3(低优先级)。T3持有锁,T1等待锁,T2抢占T3。这时候T1被T2间接阻塞了——这就是优先级反转。
解决方案:T1等待锁时,把T3的优先级临时提升到T1的水平。这样T3就能尽快执行完释放锁,T1就能继续跑了。
4.4.2 SMP下的优先级继承
多核环境下,情况更复杂。比如T3在CPU0上持有锁,T1在CPU1上等待这个锁。这时候要把T3的优先级提升,但T3可能正在CPU0上跑,而T1在CPU1上等着。
RT-Thread的处理方式:
- 每个锁维护一个等待队列
- 当高优先级线程等待锁时,检查持有锁的线程优先级
- 如果持有者优先级更低,就提升它
- 释放锁时恢复原始优先级
我遇到过的一个坑:在SMP环境下,两个核心上的线程同时等待同一个锁,结果优先级继承链变得很长。后来我限制了继承深度,最多继承3层。
4.5 调度器性能优化建议
最后分享几个我在实际项目中用到的优化技巧:
| 优化点 | 方法 | 效果 |
|---|---|---|
| 调度锁粒度 | 使用每个核心独立的调度锁,减少竞争 | 调度延迟降低30% |
| 就绪队列查找 | 使用位图加速最高优先级查找 | O(1)时间复杂度 |
| 迁移阈值 | 根据核心数量动态调整 | 避免频繁迁移 |
| 亲和性缓存 | 缓存线程上次运行的核心 | 减少缓存缺失 |
嗯,调度器这部分内容确实比较烧脑。但只要你理解了「每个核心独立调度、全局协调迁移」这个核心思想,剩下的就是细节问题了。我当年做第一个SMP项目时,光调度器就调了两个月。但一旦跑通,那种成就感是单核系统给不了的。
下一节我们会讲中断负载均衡,那是另一个有意思的话题。