实时系统中的多线程:调度策略、优先级与反转防护
实时系统,说白了就是「必须在规定时间内给出结果」的系统。你想想看,如果飞机的飞控系统晚了几毫秒响应,或者自动驾驶的刹车指令被延迟了,后果是什么?嗯,这就是实时系统的核心——确定性。
我在做工业机器人控制器的时候,就深刻体会过这一点。普通Linux上跑得好好的程序,一上实时环境就各种「抽风」。后来才发现,问题出在线程调度上。今天我们就来聊聊实时系统中的多线程,重点讲调度策略、优先级设置,以及那个让人头疼的优先级反转问题。
一、实时线程的调度策略
实时调度策略,本质上就是决定「下一个该谁跑」。我习惯把调度策略分为两大类:硬实时和软实时。硬实时要求绝对满足截止时间,软实时允许偶尔超时。
在POSIX标准中,主要有两种实时调度策略:
| 调度策略 | 特点 | 适用场景 |
|---|---|---|
SCHED_FIFO |
先进先出,非抢占式(同优先级下) | 短任务、确定性要求极高 |
SCHED_RR |
时间片轮转,同优先级轮流执行 | 多个同优先级实时任务 |
SCHED_DEADLINE |
基于截止时间的调度(Linux 3.14+) | 有明确截止时间的周期性任务 |
SCHED_FIFO 是我用得最多的策略。它的规则很简单:高优先级线程先跑,直到它主动让出CPU或者被更高优先级打断。同优先级下,谁先来谁先跑。我曾经在一个数据采集项目中,用FIFO策略处理中断响应线程,延迟从原来的几十毫秒降到了微秒级。
SCHED_RR 则适合多个同优先级的实时任务。比如你有三个传感器数据采集线程,优先级相同,用RR让它们轮流跑,每个跑一个时间片。这样不会有一个线程饿死。
SCHED_DEADLINE 是后来加入的,我个人觉得它更「智能」。你告诉内核:这个任务每100ms需要执行一次,每次最多跑10ms。内核会帮你算好什么时候该调度。不过要注意,这个策略需要内核支持,不是所有实时系统都有。
核心要点:选择调度策略时,先问自己三个问题:任务周期是否固定?截止时间是否明确?同优先级任务有多少?答案决定了你用FIFO、RR还是DEADLINE。
二、优先级设置:不是越高越好
很多新手一上来就把所有实时线程的优先级设成最高。嗯,这其实是个坑。优先级设置需要遵循几个原则:
- 关键性决定优先级:越关键的任务,优先级越高。比如安全监控线程应该比日志记录线程优先级高。
- 执行时间短的优先:短任务优先跑,可以快速释放CPU。我习惯把中断处理、快速响应这类短任务设高优先级。
- 避免同优先级过多:同优先级线程太多,调度开销会增大。一般建议不超过3-5个。
在Linux中,实时优先级范围是1-99(数值越大优先级越高)。我一般这样分配:
// 优先级分配示例
#define PRIO_CRITICAL 90 // 安全监控、紧急停止
#define PRIO_HIGH 70 // 控制算法、数据采集
#define PRIO_MEDIUM 50 // 状态监测、日志记录
#define PRIO_LOW 30 // 非实时辅助任务
// 设置线程优先级
struct sched_param param;
param.sched_priority = PRIO_HIGH;
pthread_setschedparam(thread, SCHED_FIFO, ¶m);
我曾经在一个项目中,把控制算法线程设成了优先级99,结果它把系统关键服务都阻塞了。后来降到70,配合中断亲和性设置,反而整体性能更稳定。记住:优先级不是越高越好,够用就行。
三、优先级反转:实时系统的「隐形杀手」
优先级反转,说白了就是「低优先级线程把高优先级线程堵住了」。为什么会这样?看个经典场景:
- 低优先级线程L拿到了一个互斥锁。
- 高优先级线程H来了,想拿同一个锁,被阻塞。
- 中优先级线程M开始运行(它不需要那个锁),抢占了L的CPU。
- L被M抢占,没法释放锁。H只能干等。
结果就是:最高优先级的H,被两个低优先级的线程「联手」堵死了。我在做无人机飞控时遇到过这个问题,一个传感器数据线程被堵了200ms,导致姿态解算超时,无人机差点炸机。嗯,从那以后我再也不敢忽视优先级反转了。
四、避免优先级反转的三种协议
解决优先级反转,主要有三种协议。我按推荐程度排序:
1. 优先级继承协议
这是最常用的方案。当高优先级线程被低优先级线程持有的锁阻塞时,低优先级线程临时「继承」高优先级线程的优先级。这样它就能尽快跑完、释放锁。
// POSIX 中启用优先级继承
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);
我习惯在关键路径上的所有互斥锁都开启优先级继承。虽然有一点性能开销,但比起系统崩溃,这点开销完全可以接受。
2. 优先级天花板协议
这个协议更「暴力」:每个锁都有一个「天花板优先级」,任何线程拿到这个锁,优先级自动升到天花板。这样就不会出现低优先级线程被中优先级抢占的情况。
// 设置优先级天花板
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_PROTECT);
pthread_mutexattr_setprioceiling(&attr, 80); // 天花板优先级80
天花板协议的好处是简单,不需要动态调整优先级。但缺点也很明显:如果天花板设得太高,会过度提升低优先级线程的优先级,影响系统整体调度。我一般只在锁的持有时间很短时用这个协议。
3. 无锁编程
终极方案——不用锁。用原子操作、读写锁、或者无锁队列来避免锁竞争。但这需要非常小心,我建议只在性能瓶颈处使用。
// 使用原子操作替代锁
std::atomic<int> counter{0};
void producer() {
counter.store(42, std::memory_order_release);
}
void consumer() {
int val = counter.load(std::memory_order_acquire);
}
我的建议:新手先用优先级继承协议,它最稳妥。等你对系统调度行为足够了解后,再考虑天花板协议或无锁编程。记住:能跑对,再谈优化。
五、知识体系总览
下面这张图总结了实时多线程调度的核心知识。我把它画成了流程图,方便你对照理解:
重要警告:实时调度策略需要root权限才能设置。在普通用户下调用 pthread_setschedparam 会返回 EPERM。另外,设置实时优先级后,如果线程陷入死循环,整个系统可能卡死。我建议在开发阶段加一个看门狗线程,监控实时线程的运行状态。
好了,关于实时系统中的多线程调度,核心就是这些。调度策略选对、优先级设好、反转防住,你的实时系统就能稳定运行。记住我那句老话:实时系统不怕慢,就怕不确定。确定性,才是实时系统的灵魂。
公众号:蓝海资料掘金营,微信deep3321