一、实时系统中的多线程:调度策略与锁优化

各位同学,今天我们来聊聊实时系统中的多线程编程。说实话,这块内容我当年刚接触时也踩过不少坑。实时系统和我们平时写的普通程序最大的区别在哪?说白了,就是「时间确定性」——你的线程必须在规定时间内完成工作,晚一毫秒可能就是事故。

我在做工业控制系统时,就遇到过因为线程调度不当导致机械臂动作延迟的问题。嗯,那次教训挺深刻的。所以今天我把这些经验整理出来,希望能帮你少走弯路。

1.1 实时线程的调度策略

Linux 内核提供了两种主要的实时调度策略:SCHED_FIFOSCHED_RR。它们和普通线程的 SCHED_OTHER 完全不同。

调度策略 特点 适用场景
SCHED_FIFO 先入先出,高优先级线程会抢占低优先级 短小精悍的实时任务
SCHED_RR 时间片轮转,同优先级线程轮流执行 多个同等重要的实时任务
SCHED_OTHER 普通分时调度,非实时 后台任务、非关键任务

SCHED_FIFO 的策略很简单:谁优先级高谁先跑,直到它主动让出 CPU 或者被更高优先级的线程打断。同优先级的线程按先来后到排队。我个人习惯在需要快速响应的场景用这个,比如中断处理线程。

SCHED_RR 则给每个同优先级线程分配一个时间片。时间片用完了,就轮到下一个。这有点像大家轮流吃饭,每人吃固定时间。我在项目中用它来处理多个传感器数据采集线程,因为它们重要性相同,谁也不该饿着。

核心要点:实时调度策略的核心是「优先级驱动」。高优先级线程必须能立即抢占低优先级线程,这是实时性的根本保证。

1.2 线程优先级设置

设置优先级可不是随便填个数字就行。Linux 中实时线程的优先级范围是 1-99,数字越大优先级越高。普通线程的优先级是 0。

我曾经犯过一个错误:把三个实时线程的优先级都设成了 99。结果呢?它们互相抢占,系统负载飙升,反而比普通线程还慢。你想想看,优先级不是越高越好,而是要合理分配。

#include <pthread.h>
#include <sched.h>

// 设置线程为 SCHED_FIFO 策略,优先级 80
struct sched_param param;
param.sched_priority = 80;

pthread_t thread;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
pthread_attr_setschedparam(&attr, &param);
pthread_create(&thread, &attr, thread_func, NULL);

注意:设置实时优先级需要 root 权限。否则调用会失败。我在嵌入式开发板上调试时,经常忘了先切到 root 用户,结果线程一直跑在普通策略下,排查了半天。

优先级设计有个基本原则:关键任务用高优先级,非关键任务用低优先级。比如电机控制线程优先级设 90,日志记录线程设 10。这样即使日志线程卡住了,电机控制也能正常响应。

1.3 实时系统中的锁优化

锁,是实时系统的头号敌人。为什么?因为锁会导致优先级反转——一个低优先级线程拿着锁,高优先级线程只能干等着。

我记得有个项目,电机控制线程(优先级 90)和数据显示线程(优先级 50)共享一个缓冲区。数据显示线程拿了锁后,被一个优先级 60 的中断线程抢占了。结果电机控制线程等了整整 5 毫秒才拿到锁——这在实时系统中是不可接受的。

解决方案有几种:

  • 优先级继承协议:低优先级线程持有锁时,临时提升到等待该锁的最高优先级。Linux 的 pthread_mutexattr_setprotocol 可以设置。
  • 优先级天花板协议:锁的优先级设为所有可能使用它的线程中的最高值。
  • 无锁编程:用原子操作替代锁。比如 __sync_fetch_and_add 系列函数。
// 使用优先级继承的互斥锁
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);

pthread_mutex_t lock;
pthread_mutex_init(&lock, &attr);

我的建议:能不用锁就不用锁。用消息队列、无锁环形缓冲区、或者每个线程独享数据。我在音频处理项目中,就用无锁环形缓冲区替代了锁,延迟从 200 微秒降到了 5 微秒。

1.4 知识体系总览

下面这张图总结了实时多线程的核心知识点。你可以把它当作一个检查清单,做项目时对照着看。

实时系统多线程 调度策略 SCHED_FIFO SCHED_RR 优先级设置 范围 1-99 root权限 锁优化 优先级继承 无锁编程 避坑指南 • 不要把所有实时线程设成最高优先级 • 锁的持有时间越短越好,最好不超过 100 微秒 • 实时线程里不要调用 malloc、printf 等可能阻塞的函数

1.5 实战经验总结

最后,我把自己这些年做实时系统的经验浓缩成几条:

  1. 优先级分配要「够用就好」。不是所有任务都需要实时,把实时线程数量控制在 3-5 个以内。
  2. 锁的粒度要细。一个大锁拆成多个小锁,或者用读写锁。我见过有人用一个全局锁保护整个数据结构,结果实时性一塌糊涂。
  3. 测试要加压力。实时系统的问题往往在高负载下才暴露。我曾经在实验室跑得好好的,一上产线就出问题——因为产线上的中断频率是实验室的 10 倍。
  4. 善用工具。用 perfftrace 分析调度延迟,用 cyclictest 测量实时性。别靠猜。

记住:实时系统编程,本质上是「确定性」的编程。每一个函数调用、每一次锁操作、每一个线程切换,都要能算出最坏情况下的时间。做不到这一点,就不叫实时系统。

好了,这一章的内容就到这里。调度策略和锁优化是实时系统的基石,理解透了后面学起来就轻松了。下一章我们会深入讲实时系统中的内存管理,那又是另一番天地。


公众号:蓝海资料掘金营,微信deep3321