3. 进程调度与优先级管理:实时进程调度策略与优先级反转
各位做嵌入式Linux开发的同行,咱们今天聊点硬核的——实时调度。说实话,我见过太多项目在调度这块栽跟头。明明硬件性能够,代码逻辑也没问题,但系统就是不稳定,偶尔卡顿一下。最后查出来,十有八九是调度策略没选对,或者优先级管理出了纰漏。
我个人习惯把调度问题分成两类:一类是“选错了策略”,另一类是“优先级管乱了”。咱们今天就把这两件事彻底讲透。
3.1 实时进程调度策略:SCHED_FIFO 与 SCHED_RR
Linux内核里,普通进程用的是SCHED_OTHER(CFS调度器)。但做实时应用,你得切换到实时调度策略。内核提供了两种:SCHED_FIFO 和 SCHED_RR。
3.1.1 SCHED_FIFO:先入先出,不打断
SCHED_FIFO,说白了就是“一个跑完,下一个再上”。只要这个进程不主动让出CPU,或者没有更高优先级的实时进程来抢,它就会一直跑下去。
我在项目中遇到过这样一个场景:一个数据采集线程,需要每1ms读取一次传感器。我用SCHED_FIFO给它设了最高优先级。结果呢?它确实准时了,但把其他所有线程都堵死了——包括负责网络通信的线程。系统直接“假死”。
SCHED_FIFO的调度规则其实很简单:
- 高优先级先跑
- 同优先级下,谁先就绪谁先跑
- 进程主动让出CPU(比如调用sched_yield()、阻塞在I/O上)后,才会轮到同优先级的其他进程
- 被更高优先级抢占后,恢复时继续跑完剩余时间片
3.1.2 SCHED_RR:轮转,大家都有份
SCHED_RR和FIFO的区别就一点:时间片轮转。同优先级的实时进程,每个跑一个固定时间片,时间到了就换下一个。这样就不会出现一个进程霸占CPU的情况。
你想想看,什么时候用RR?我个人的经验是:当你有多个同等重要的实时任务,而且它们都需要周期性执行时,RR是更好的选择。比如一个系统里同时有音频处理和视频处理,两者优先级相同,用RR就能保证两者都能得到CPU时间。
| 特性 | SCHED_FIFO | SCHED_RR |
|---|---|---|
| 同优先级调度 | 一个跑完才换下一个 | 时间片轮转 |
| 适用场景 | 短任务、需要低延迟的任务 | 多个同等重要的周期性任务 |
| 风险 | 一个任务死循环会导致同优先级任务饿死 | 时间片切换有额外开销 |
| 时间片 | 无时间片概念 | 默认100ms(可配置) |
3.1.3 如何设置实时调度策略
代码实现其实不复杂。我一般用pthread_setschedparam()或者sched_setscheduler()。下面给个例子:
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
void *realtime_thread(void *arg) {
// 实时任务逻辑
while (1) {
// 处理关键数据
usleep(1000); // 1ms周期
}
return NULL;
}
int main() {
pthread_t tid;
pthread_attr_t attr;
struct sched_param param;
// 初始化线程属性
pthread_attr_init(&attr);
// 设置为SCHED_FIFO策略
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
// 设置优先级(1-99,数字越大优先级越高)
param.sched_priority = 80;
pthread_attr_setschedparam(&attr, ¶m);
// 启用实时调度(必须)
pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
// 创建线程
pthread_create(&tid, &attr, realtime_thread, NULL);
pthread_join(tid, NULL);
return 0;
}
3.2 优先级反转:一个经典的坑
优先级反转,这个词做嵌入式的应该都听过。但真正在项目里遇到过的人,才知道它有多坑。
什么叫优先级反转?简单说就是:低优先级任务占着资源不放,导致高优先级任务被阻塞。结果就是,高优先级任务反而被低优先级任务“牵着鼻子走”。
我举个例子你就明白了。假设有三个任务:
- Task A:高优先级(90),负责紧急报警
- Task B:中优先级(50),负责普通数据处理
- Task C:低优先级(30),负责日志写入
正常情况应该是:Task A来了,Task C和Task B都得让路。但如果有共享资源(比如一个全局缓冲区),情况就变了:
- Task C先拿到锁,开始写日志
- Task A来了,想拿锁,但锁被Task C占着,只能阻塞等待
- Task B来了,它不需要锁,直接抢占Task C开始运行
- Task C被Task B抢占,锁一直不释放
- Task A继续等……直到Task B跑完,Task C才能继续,释放锁
你看,最高优先级的Task A,反而被中优先级的Task B和低优先级的Task C“联手”堵死了。这就是优先级反转。
3.3 优先级继承协议:解决方案
怎么解决优先级反转?Linux内核提供了优先级继承协议(Priority Inheritance Protocol)。
它的核心思想很简单:当低优先级任务持有高优先级任务需要的锁时,临时把低优先级任务的优先级提升到高优先级任务的级别。这样,中优先级的任务就没法抢占它了,锁就能尽快释放。
还是刚才那个例子,用优先级继承后:
- Task C拿到锁
- Task A来了,想拿锁,阻塞
- 内核检测到Task C持有Task A需要的锁,把Task C的优先级临时提升到90
- Task B来了,发现Task C优先级比自己高,抢不过,继续等
- Task C顺利跑完,释放锁,优先级降回30
- Task A拿到锁,继续执行
嗯,这里要注意:优先级继承不是万能的。它只能解决“直接反转”,对于“链式反转”(多个锁嵌套的情况)效果有限。更复杂的场景需要用优先级天花板协议(Priority Ceiling Protocol)。
3.3.1 Linux中的优先级继承实现
Linux内核里,优先级继承是通过实时互斥锁(rt_mutex)实现的。POSIX线程库里的PTHREAD_PRIO_INHERIT选项就是干这个的。
#include <pthread.h>
pthread_mutex_t lock;
pthread_mutexattr_t attr;
// 初始化互斥锁属性
pthread_mutexattr_init(&attr);
// 开启优先级继承
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
// 初始化互斥锁
pthread_mutex_init(&lock, &attr);
3.4 知识体系总览
下面这张图,是我自己总结的实时调度与优先级管理知识体系。每次做新项目,我都会对照着检查一遍:
3.5 实战建议
最后,我根据多年经验,给几条实在的建议:
- 优先级分配要留有余地:别把99级全用满。我一般留出10-20的余量给紧急任务,比如看门狗、紧急停机。
- 实时线程越短越好:SCHED_FIFO的线程,每次执行时间不要超过1ms。超过这个量,系统的实时性就会大打折扣。
- 锁的粒度要细:能用读写锁就别用互斥锁,能用原子操作就别用锁。我见过一个项目,一个全局锁保护了所有数据,结果实时性直接崩了。
- 测试要覆盖优先级反转场景:我曾经在测试阶段没发现反转问题,结果现场部署后,系统在特定负载下频繁卡顿。后来加了个压力测试用例,专门制造优先级反转场景,才把问题抓出来。
chrt命令可以查看和修改进程的调度策略和优先级。我调试时经常用chrt -p $$看当前shell的调度参数。用trace-cmd可以追踪调度事件,分析优先级反转是否发生。