17、实时系统配置:PREEMPT_RT内核编译、实时线程优先级设置、延迟测试与抖动分析

做Jetson实时系统配置,说白了就是让Linux从“尽力而为”变成“说到做到”。

我最早接触这个需求,是在一个无人机飞控项目上。当时用的普通内核,电机控制线程偶尔被调度走,结果就是——飞机在空中抖了一下。嗯,就那么一下,差点炸机。从那以后,我对实时性就特别敏感。

17.1 为什么需要PREEMPT_RT?

标准Linux内核是个“老好人”。它允许一个低优先级的任务长时间占用CPU,高优先级的实时任务只能等着。这在桌面系统上没问题,但在工业控制、机器人、自动驾驶上,这就是灾难。

PREEMPT_RT补丁做了什么?它把内核的大部分自旋锁换成了可抢占的互斥锁。中断处理也变成了线程。说白了,就是让高优先级任务能“插队”。

核心变化:

  • 中断线程化:中断处理程序变成内核线程,可以被调度
  • 自旋锁替换:大部分spin_lock变成rt_mutex
  • 优先级继承:防止优先级反转
  • 高精度定时器:HRTIMER默认开启

17.2 编译PREEMPT_RT内核(Jetson平台)

Jetson平台编译内核,和普通x86不太一样。你需要用NVIDIA提供的交叉编译工具链。我习惯在Ubuntu主机上交叉编译,然后刷到Jetson上。

17.2.1 获取源码

# 下载L4T内核源码(以Jetson Orin为例)
export JETSON_MODEL=orin
export L4T_VERSION=35.3.1

# 从NVIDIA官网下载内核源码包
wget https://developer.nvidia.com/embedded/l4t/r35_release_v3.1/sources/public_sources.tbz2

# 解压
tar -xjf public_sources.tbz2
cd Linux_for_Tegra/source
tar -xjf kernel_src.tbz2

# 进入内核目录
cd kernel/kernel-5.10

17.2.2 打PREEMPT_RT补丁

# 下载对应内核版本的PREEMPT_RT补丁
# 内核版本5.10.104-rt60
wget https://cdn.kernel.org/pub/linux/kernel/projects/rt/5.10/patch-5.10.104-rt60.patch.xz

# 解压并打补丁
xzcat patch-5.10.104-rt60.patch.xz | patch -p1

# 检查是否有reject文件
find . -name "*.rej"

注意:我曾经遇到过补丁打不上,原因是NVIDIA对内核做了修改。你需要手动解决冲突。建议先备份原始源码。

17.2.3 配置内核

# 使用NVIDIA默认配置
make ARCH=arm64 tegra_defconfig

# 开启PREEMPT_RT
make ARCH=arm64 menuconfig

# 进入以下路径:
# General setup --->
#   Preemption Model (Fully Preemptible Kernel (Real-Time)) --->
#     (X) Fully Preemptible Kernel (Real-Time)

# 确认以下选项已开启:
# Kernel Features --->
#   [*] High Resolution Timer Support
#   [*] Tickless System (Dynamic Ticks)
#   Timer frequency ---> 1000 Hz

17.2.4 编译与安装

# 设置交叉编译环境
export CROSS_COMPILE=aarch64-linux-gnu-
export ARCH=arm64

# 编译内核和模块
make -j$(nproc) Image
make -j$(nproc) modules

# 编译设备树
make -j$(nproc) dtbs

# 安装模块到临时目录
make modules_install INSTALL_MOD_PATH=./modules_install

# 打包内核
cd arch/arm64/boot
# 生成Image文件,用于刷机或替换

我个人习惯把编译好的内核放到SD卡上,通过修改extlinux.conf来启动新内核。这样万一有问题,还能切回原内核。

17.3 实时线程优先级设置

内核准备好了,接下来就是应用层的事。实时线程的优先级设置,我用的是POSIX的sched_setscheduler接口。

17.3.1 设置SCHED_FIFO优先级

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

void set_realtime_priority(pthread_t thread, int priority) {
    struct sched_param param;
    param.sched_priority = priority;

    // 使用SCHED_FIFO实时调度策略
    int ret = pthread_setschedparam(thread, SCHED_FIFO, &param);
    if (ret != 0) {
        // 处理错误,通常是没有CAP_SYS_NICE权限
        perror("pthread_setschedparam");
    }
}

// 使用示例
int main() {
    pthread_t control_thread;
    // 创建线程...
    set_realtime_priority(control_thread, 80);  // 优先级范围1-99
    // ...
}

经验之谈:优先级不是越高越好。我曾经把控制线程设成99,结果它把网卡中断都抢占了,导致网络丢包。建议控制类线程设80-90,数据采集设70-80,普通任务设50以下。

17.3.2 内存锁定

实时线程还有一个坑——缺页中断。如果线程访问的内存页不在物理内存中,会发生缺页,导致延迟飙升。解决办法是锁定内存。

#include <sys/mman.h>

void lock_memory() {
    // 锁定当前和未来的所有内存页
    int ret = mlockall(MCL_CURRENT | MCL_FUTURE);
    if (ret != 0) {
        perror("mlockall");
    }
}

// 在main函数开头调用
int main() {
    lock_memory();
    // 预分配并初始化所有实时线程需要的内存
    // ...
    set_realtime_priority(control_thread, 80);
    // 进入实时循环
}

17.4 延迟测试与抖动分析

配置完了,怎么知道效果好不好?测延迟。我常用的工具是cyclictest,它是rt-tests套件的一部分。

17.4.1 安装rt-tests

# 在Jetson上编译安装
git clone git://git.kernel.org/pub/scm/utils/rt-tests/rt-tests.git
cd rt-tests
make
sudo make install

# 或者直接apt安装(如果源里有)
sudo apt install rt-tests

17.4.2 运行cyclictest

# 基本测试:1个线程,间隔1000微秒,运行60秒
sudo cyclictest -t 1 -p 80 -i 1000 -l 60000 -m

# 参数说明:
# -t 1    : 1个测试线程
# -p 80   : 优先级80
# -i 1000 : 间隔1000微秒(1ms)
# -l 60000: 循环60000次(60秒)
# -m      : 锁定内存

# 多线程测试:4个线程,不同优先级
sudo cyclictest -t 4 -p 80 -i 1000 -l 60000 -m -d 0

17.4.3 结果解读

cyclictest会输出统计信息。我一般关注三个指标:最小延迟、平均延迟、最大延迟。

# 输出示例
T: 0 (12345) P: 80 I: 1000 C:  60000 Min:      2 Act:    3 Avg:    4 Max:     18

# 含义:
# T: 线程ID
# P: 优先级
# I: 间隔(微秒)
# C: 计数
# Min: 最小延迟(微秒)
# Act: 当前延迟
# Avg: 平均延迟
# Max: 最大延迟(关键指标)

抖动分析:最大延迟和平均延迟的差值,就是抖动。差值越小,系统越稳定。我见过最好的Jetson Orin配置,最大延迟能控制在30微秒以内。如果超过100微秒,就要检查配置了。

17.4.4 绘制延迟分布图

只看最大值不够,我习惯把延迟数据导出来,画个分布图。这样能看出延迟的“尾巴”有多长。

# 导出延迟数据到文件
sudo cyclictest -t 1 -p 80 -i 1000 -l 60000 -m -h 100 -v > latency_data.txt

# -h 100 : 直方图,100个桶
# -v     : 详细输出

# 然后用Python或Excel画图
# 我一般用Python的matplotlib,几行代码搞定

17.5 知识体系图

下面这张图,是我自己总结的实时系统配置流程。从内核到应用,每一步都影响最终延迟。

实时系统配置流程 内核层 PREEMPT_RT补丁 → 中断线程化 → 自旋锁替换 → 高精度定时器 配置:Fully Preemptible Kernel + 1000Hz + HRTIMER 系统层 CPU隔离(isolcpus) → 中断亲和性 → 内存锁定(mlockall) 配置:/etc/default/grub 添加isolcpus=2,3 应用层 SCHED_FIFO优先级 → 内存预分配 → 避免系统调用 → 锁优化 配置:优先级80-90,锁定内存,使用无锁数据结构 验证:cyclictest延迟测试 → 抖动分析 → 优化迭代

17.6 常见问题与避坑

做实时系统配置,坑不少。我踩过的,列几个出来:

  • 优先级反转:低优先级任务持有锁,高优先级任务等锁。解决方案是使用优先级继承协议(pthread_mutexattr_setprotocol)。
  • 中断风暴:某个设备产生大量中断,抢占CPU。我遇到过USB控制器的问题,后来把中断亲和性绑到非实时核上解决了。
  • 动态调频:CPU频率缩放会导致延迟不稳定。建议设置performance governor。
  • DMA缓冲区:DMA操作可能绕过CPU缓存,导致一致性开销。用dma_alloc_coherent分配一致性内存。

我的调试习惯:先用cyclictest跑个基准,然后逐步调整配置。每次只改一个参数,再跑测试。这样能清楚知道哪个改动有效果。

17.7 总结

实时系统配置,不是装个补丁就完事。内核、系统、应用三层都要调。PREEMPT_RT是基础,但真正的延迟控制,在于你怎么用。

我见过有人把优先级设到99,结果系统卡死。也见过有人用普通内核,靠精心设计的用户态调度,达到微秒级延迟。工具是死的,人是活的。

最后说一句:测试,测试,再测试。没有cyclictest数据,别说你的系统是实时的。


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