第四讲:实时调度策略——RT调度类与优先级那些事
各位同学,今天我们来聊聊实时调度。说实话,这部分内容在车载Linux里特别重要。你想想看,一辆车在高速上跑着,刹车系统突然被一个后台任务抢了CPU——这可不是闹着玩的。
我刚开始做车载项目时,对调度策略的理解也停留在“优先级高的先跑”这个层面。直到有一次,一个ADAS系统的摄像头数据采集任务莫名其妙地丢帧,排查了整整三天...嗯,最后发现是优先级反转搞的鬼。从那以后,我对实时调度就再也不敢马虎了。
4.1 RT调度类:FIFO与RR
Linux的实时调度类主要有两种:SCHED_FIFO和SCHED_RR。它们都属于RT调度类,优先级范围是0-99,数值越大优先级越高。
核心区别一句话:FIFO是“不吃完不放手”,RR是“吃一口让一让”。
4.1.1 SCHED_FIFO(先进先出)
FIFO调度策略,说白了就是“霸占CPU直到主动让出”。一个FIFO任务一旦获得CPU,除非它自己调用sleep、等待锁、或者被更高优先级的任务抢占,否则它会一直运行下去。
我在项目中遇到过这样一个场景:一个FIFO优先级的CAN报文处理任务,因为代码里有个死循环,结果把整个系统的UI响应都卡死了。嗯,这就是FIFO的“霸道”之处。
// 设置FIFO调度策略的示例
struct sched_param param;
param.sched_priority = 80; // 优先级80
if (sched_setscheduler(0, SCHED_FIFO, ¶m) == -1) {
perror("sched_setscheduler failed");
}
避坑指南:我曾经见过一个团队,把所有实时任务都设成FIFO最高优先级99。结果呢?一个任务出bug,整个系统都跟着遭殃。记住:FIFO任务要设计成“短小精悍”,执行时间必须可控。
4.1.2 SCHED_RR(轮转调度)
RR和FIFO很像,但多了一个时间片的概念。每个RR任务可以运行一个时间片(默认100ms),时间到了就自动让给同优先级的其他任务。
我个人习惯把RR用在那些“需要公平分配CPU”的场景。比如多个传感器数据采集任务,优先级相同,用RR就能保证每个传感器都能定期被服务到。
| 特性 | SCHED_FIFO | SCHED_RR |
|---|---|---|
| 时间片 | 无限制 | 固定时间片 |
| 同优先级抢占 | 不会主动让出 | 时间片用完自动让出 |
| 适用场景 | 短执行、高实时性任务 | 多个同优先级实时任务 |
| 风险 | 容易饿死低优先级 | 时间片切换开销 |
4.2 优先级反转——实时系统的隐形杀手
优先级反转,这个词听起来挺学术,其实道理很简单:一个高优先级任务,因为等一个低优先级任务持有的资源,结果被中等优先级任务“插队”了。
为什么会这样?我给你画个场景:
- 任务L(低优先级)拿到了锁
- 任务H(高优先级)来了,想拿锁,但锁被L占着,只能等
- 任务M(中等优先级)来了,它不需要锁,直接抢占了L
- 结果:H在等L,L被M抢占了CPU,H反而被M“间接”阻塞了
我在一个车载信息娱乐系统项目中就踩过这个坑。一个高优先级的触摸响应任务,因为等一个低优先级的日志写入锁,结果被一个中优先级的音乐解码任务拖了整整200ms。用户点屏幕,半秒后才反应——这体验能好吗?
4.3 优先级继承——解决问题的钥匙
Linux内核提供了优先级继承协议(Priority Inheritance Protocol)来解决这个问题。说白了就是:当高优先级任务等锁时,持有锁的低优先级任务会“临时继承”高优先级,防止被中等优先级任务抢占。
// 使用优先级继承的互斥锁
pthread_mutexattr_t attr;
pthread_mutex_t mutex;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_init(&mutex, &attr);
我的经验:在车载项目中,我建议所有保护共享资源的锁都启用优先级继承。虽然会带来一点点性能开销(大约5-10%),但相比优先级反转导致的系统崩溃,这点代价完全值得。
4.4 实时任务设计原则——实战总结
做了这么多年车载系统,我总结了几条实时任务设计的原则,分享给大家:
4.4.1 任务要“短”
实时任务的执行时间要尽可能短。我一般控制在1ms以内。如果任务需要长时间处理,就拆分成多个小任务,或者用工作队列把非实时部分延后处理。
4.4.2 避免动态内存分配
malloc/free在实时任务里是大忌。为什么?因为内存分配的时间不确定,可能触发页面错误或内存回收。我习惯在初始化阶段就把所有内存都分配好。
4.4.3 锁的粒度要细
锁的范围越小,阻塞时间越短。我曾经把一个全局锁拆成三个细粒度锁,系统响应时间直接降了60%。
4.4.4 优先级要合理分配
不要把所有任务都设成99。我一般这样分配:
- 90-99:安全关键任务(刹车、转向控制)
- 70-89:实时数据采集(传感器、CAN)
- 50-69:控制逻辑处理
- 30-49:人机交互
- 0-29:后台服务、日志
我曾经犯过的错:把一个日志刷新任务设成了FIFO优先级80,结果它和CAN处理任务抢CPU。日志丢了可以重打,CAN报文丢了可是会出事故的。记住:非实时任务永远不要用RT调度类。
4.5 实战检查清单
每次做完实时任务设计,我都会对照这个清单检查一遍:
- 所有实时任务是否都用了SCHED_FIFO或SCHED_RR?
- 优先级是否合理?有没有“平级过多”的情况?
- 共享资源的锁是否启用了优先级继承?
- 实时任务里有没有动态内存分配?
- 任务执行时间是否可预测?有没有循环等待?
- 有没有“优先级反转”的潜在风险?
好了,这一讲的内容就到这里。实时调度说难不难,说简单也不简单。关键是要理解背后的原理,然后在实践中不断积累经验。下一讲我们会聊聊多核环境下的调度优化,到时候见。