上篇聊了FreeRTOS等RTOS在MCU上的应用。但很多机器人场景中,计算密集的任务(感知、规划)需要跑在Linux上。普通Linux的实时性不够好——最大延迟可能达到毫秒级别。PREEMPT_RT补丁把Linux变成了一个接近RTOS的实时操作系统。

为什么Linux不够实时

普通Linux内核中有很多不可抢占的区域。最大的问题是中断处理——中断处理期间内核是关抢占的,如果一个中断处理程序执行时间长(比如网络驱动处理大量数据包),其他高优先级的实时线程就被阻塞了。

另一个问题是spin_lock。内核中大量使用spin_lock来保护共享数据结构。持有spin_lock期间,内核禁止抢占。如果锁的持有时间长,实时线程的调度延迟就大。

还有内核线程的问题。一些内核操作(比如内存回收、文件系统同步)在内核线程中执行,这些线程的优先级和调度策略不受用户控制。

PREEMPT_RT做了什么

PREEMPT_RT补丁对Linux内核做了几个关键修改,大幅提高了实时性能。

中断线程化。把几乎所有的中断处理程序都转换成了内核线程。中断触发时只做一个最小化的应答(ack硬件),实际的处理工作在线程上下文中执行。这样中断处理就变成了一个普通的内核线程,可以被高优先级的实时线程抢占。每个中断线程可以独立设置优先级和调度策略。比如网络中断线程可以设低优先级(不影响控制任务),而定时器中断线程设高优先级。

Spin_lock变成可抢占的Mutex。原来持有spin_lock时不能抢占,现在变成了可以睡眠的锁(rt_mutex)。高优先级的实时线程不会因为低优先级线程持有锁而被长时间阻塞。当实时线程等待rt_mutex时,持锁线程的优先级会被临时提升(优先级继承),确保它尽快释放锁。实测表明,PREEMPT_RT内核的锁操作比普通内核慢2到5倍,但在大多数机器人应用中这个开销可以接受。

高精度定时器。PREEMPT_RT使用高精度定时器(hrtimer)替代了原来的低精度定时器,定时分辨率从毫秒级提升到了纳秒级。这对需要精确定时的控制任务非常重要——比如1kHz的控制循环,如果用低精度定时器,周期可能在0.9ms到1.1ms之间波动;用hrtimer可以控制在1ms正负几微秒。

线程化的softirq。softirq(比如网络收包的NAPI、块设备IO完成)也被线程化了,可以在实时线程的调度框架下管理。这意味着你可以给网络收包线程设一个较低的优先级,让它不会干扰控制任务。

内核中还有一些区域保持不可抢占。比如某些体系结构相关的代码(上下文切换的核心部分)、一些底层的内存管理操作。这些区域通常非常短(几百纳秒),对实时性能的影响有限。

实时性能的量化

PREEMPT_RT的效果可以用cyclictest来量化。在一台普通的x86机器上:

普通Linux:最大延迟通常在几百微秒到几毫秒之间,取决于系统负载。高负载时可能超过10ms。

PREEMPT_RT Linux:最大延迟通常在几十微秒以内。即使在较高负载下,也能保持在100微秒以下。

在ARM嵌入式平台上(比如i.MX6或Jetson),PREEMPT_RT的表现类似。最大延迟从普通Linux的几毫秒降低到几十微秒。

# cyclictest的使用示例
# 创建3个实时线程,优先级80,周期1ms,运行60秒
sudo cyclictest -l100000000 -m -S -p80 -i1000 -t1 -a0 -D60
# 输出: T:0 (12345) P:80 I:1000 C:60000 Min:1 Act:2 Avg:2 Max:8
# Max:8 表示最大延迟是8微秒

怎么编译和使用PREEMPT_RT

编译PREEMPT_RT内核的步骤。下载Linux内核源码和对应版本的PREEMPT_RT补丁。给内核打补丁:patch -p1 < patch-xxx-rt.patch。配置内核:make menuconfig,在"Fully Preemptible Kernel"选项中选择"Fully Preemptible"。编译安装内核。重启后用uname -r确认内核版本带有"rt"标记。

使用PREEMPT_RT时,应用程序需要设置实时调度策略。用sched_setscheduler()系统调用或者chrt命令:

# 把进程设置为SCHED_FIFO策略,优先级50
sudo chrt -f 50 ./my_realtime_app
# 或者用SCHED_RR(时间片轮转)
sudo chrt -r 50 ./my_realtime_app

SCHED_FIFO和SCHED_RR是两种实时调度策略。SCHED_FIFO没有时间片——高优先级任务一直运行直到主动让出CPU或者被更高优先级任务抢占。SCHED_RR有时间片——同优先级的实时任务轮流执行。

面试要点

PREEMPT_RT的核心修改。面试官问PREEMPT_RT做了什么,你要能说出中断线程化和spin_lock转Mutex这两个最关键的修改。这两个修改解决了Linux中最大的两个不可抢占区域。

PREEMPT_RT的局限性。虽然PREEMPT_RT大幅改善了实时性能,但它仍然不是硬实时的。内核中还有一些小的不可抢占区域(比如某些架构相关的代码),最大延迟不能保证为零。如果需要真正的硬实时保证(微秒级的确定性),还是需要用RTOS。PREEMPT_RT适合软实时和准硬实时的场景。

CPU隔离。除了PREEMPT_RT,Linux还提供了CPU隔离功能(isolcpus或nohz_full启动参数),可以把某些CPU核心从通用调度中隔离出来,专门给实时任务使用。这进一步减少了实时任务被干扰的可能性。在机器人中,通常把一个核心专门分配给控制任务,其他核心跑感知和规划。配合irqbalance的排除列表,确保中断也不会分配到隔离的核心上。

内核配置的影响。PREEMPT_RT的效果还跟其他内核配置有关。CONFIG_NO_HZ(动态时钟滴答)可以减少不必要的定时器中断。CONFIG_HIGH_RES_TIMERS启用高精度定时器。CONFIG_CPU_FREQ设为performance模式避免频率切换带来的延迟。这些配置组合起来能让PREEMPT_RT的效果最大化。

内存锁定。实时程序的内存应该被锁定在物理内存中(mlockall),防止被交换到磁盘。页面错误(page fault)会导致不可预测的延迟——如果一个页面不在物理内存中,内核需要从磁盘读取,这个时间可能是几毫秒甚至更长。在启动时调用mlockall(MCL_CURRENT | MCL_FUTURE)可以避免这个问题。

PREEMPT_RT的版本兼容性。PREEMPT_RT补丁需要跟特定的Linux内核版本匹配。不是每个内核版本都有对应的RT补丁。选择内核版本时要确认有对应的RT补丁可用。目前主线Linux(5.15+)已经合并了部分PREEMPT_RT的代码,未来可能会完全集成到主线中。在那之前,还是需要单独打补丁。

给你的建议

如果你在做机器人软件开发,PREEMPT_RT是必须了解的技术。建议在一台x86电脑或者树莓派上编译一个PREEMPT_RT内核,跑cyclictest看看效果。然后写一个简单的实时程序,体验SCHED_FIFO和SCHED_RR的区别。

在项目中,如果你的控制循环跑在Linux上,一定要用PREEMPT_RT。普通Linux的延迟波动会严重影响控制质量。配合CPU隔离和实时调度策略,Linux上的控制循环可以达到几十微秒级的抖动。

部署PREEMPT_RT时容易忽略的几个点。BIOS中关闭C-states和P-states——CPU的深度睡眠状态和频率切换都会引入延迟。关闭Turbo Boost——频率的动态变化导致执行时间不确定。设置正确的GRUB启动参数:isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3(隔离2号和3号核心)。用systemd设置实时任务的CPU亲和性:CPUAffinity=2。这些配置组合起来,能让你的实时任务获得最佳的确定性。

还有一个建议:用trace-cmd和kernelshark来分析PREEMPT_RT内核的调度行为。这两个工具可以可视化内核的调度事件,帮你找到延迟的来源。如果发现某个内核函数频繁导致长延迟,可能需要打补丁或者换一种实现方式。


上一篇:第315篇 RTOS与FreeRTOS

下一篇预告:第317篇 实时调度策略详解

 

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐