第二章:Linux内核配置——PREEMPT_RT补丁安装、内核参数调优、CPU隔离与亲和性设置

各位同学,欢迎来到第二章。

上一章我们聊了实时系统的概念,说白了就是「在规定时间内必须完成指定任务」。这一章,我们直接动手,把一台普通的Linux机器,改造成一个能跑实时任务的系统。

我个人习惯把这一章叫做「硬核改造三部曲」:打补丁、调参数、绑CPU。这三步走完,你的ROS2节点才能谈得上「实时」二字。

2.1 PREEMPT_RT补丁:给Linux内核装上「实时引擎」

先问大家一个问题:为什么普通Linux内核不适合做实时控制?

原因很简单——内核里到处都是不可抢占的临界区。比如一个高优先级的实时任务想运行,但内核正在处理一个低优先级的系统调用,它只能干等着。这在机器人控制里是致命的,你想想看,电机控制周期是1ms,结果被一个文件读写操作卡了5ms,机器人不摔才怪。

PREEMPT_RT补丁就是来解决这个问题的。它把内核里那些「长锁」替换成了「短锁」,把中断处理线程化,让高优先级任务几乎可以随时抢占CPU。

核心变化:

  • 自旋锁变成可睡眠的互斥锁
  • 中断处理程序变成内核线程
  • 高精度定时器(HRT)默认开启
  • 优先级继承机制防止优先级反转

2.1.1 安装步骤(以Ubuntu 22.04 + Kernel 5.15为例)

嗯,这里要注意,不同内核版本的操作略有差异,但思路是一样的。

# 1. 查看当前内核版本
uname -r
# 输出:5.15.0-86-generic

# 2. 下载对应内核源码和PREEMPT_RT补丁
# 去 kernel.org 下载 5.15.y 版本,以及 rt.patch
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.137.tar.xz
wget https://cdn.kernel.org/pub/linux/kernel/projects/rt/5.15/patch-5.15.137-rt70.patch.xz

# 3. 解压并打补丁
tar -xf linux-5.15.137.tar.xz
cd linux-5.15.137
xzcat ../patch-5.15.137-rt70.patch.xz | patch -p1

# 4. 配置内核(关键步骤)
make menuconfig
# 进入 General setup —> Preemption Model
# 选择:Fully Preemptible Kernel (Real-Time)
# 保存退出

我的经验:我第一次打补丁时,忘了选Preemption Model,编译出来的内核跟普通内核没区别。折腾了两天才发现是这里没改。所以,打完补丁后第一件事就是检查这个选项。

# 5. 编译并安装
make -j$(nproc)
sudo make modules_install
sudo make install

# 6. 重启并选择RT内核
sudo reboot
# 在GRUB菜单中选择新内核启动

# 7. 验证
uname -a
# 输出应包含:PREEMPT RT
cat /sys/kernel/realtime
# 返回 1 表示实时内核已启用

2.2 内核参数调优:把「实时潜力」榨出来

装好PREEMPT_RT只是第一步。就像你买了一辆赛车,但没调ECU参数,它还是跑不快。内核参数调优就是做这件事。

2.2.1 关键参数一览

参数 默认值 推荐值 说明
kernel.sched_rt_runtime_us 950000 -1 实时任务可占用的CPU时间(微秒),-1表示不限制
kernel.sched_rt_period_us 1000000 1000000 实时任务调度周期,一般保持默认
kernel.hung_task_timeout_secs 120 0 关闭hung task检测,避免误杀实时任务
vm.swappiness 60 10 降低交换分区使用倾向,减少延迟抖动
kernel.nmi_watchdog 1 0 关闭NMI看门狗,减少中断干扰

警告:设置 kernel.sched_rt_runtime_us = -1 意味着实时任务可以独占CPU。如果实时任务出现死循环,系统会完全卡死。我建议在调试阶段先用 -1,生产环境一定要加看门狗机制。

2.2.2 配置方法

# 临时生效(重启后失效)
sudo sysctl -w kernel.sched_rt_runtime_us=-1
sudo sysctl -w vm.swappiness=10

# 永久生效
sudo vim /etc/sysctl.d/99-rt.conf
# 写入以下内容:
kernel.sched_rt_runtime_us = -1
kernel.sched_rt_period_us = 1000000
kernel.hung_task_timeout_secs = 0
vm.swappiness = 10
kernel.nmi_watchdog = 0

# 应用配置
sudo sysctl --system

2.3 CPU隔离与亲和性设置:给实时任务「专属跑道」

为什么要做CPU隔离?

你想想看,如果所有任务都在所有CPU上乱跑,一个实时任务可能被一个非实时的后台服务打断。CPU隔离就是把某些核心「藏起来」,只让实时任务使用。

2.3.1 内核启动参数隔离CPU

在GRUB配置中修改:

# 编辑 /etc/default/grub
GRUB_CMDLINE_LINUX="isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3"

# 参数说明:
# isolcpus=2,3    :将CPU2和CPU3从通用调度器中隔离
# nohz_full=2,3   :在这些CPU上关闭周期性时钟中断
# rcu_nocbs=2,3   :将RCU回调从这些CPU上卸载

# 更新GRUB
sudo update-grub
sudo reboot

我的习惯:我一般会留CPU0给系统管理任务,CPU1给中断处理,CPU2和CPU3给实时任务。这样系统不会因为实时任务独占CPU而完全失去响应。

2.3.2 使用taskset设置亲和性

# 启动时绑定CPU
taskset -c 2,3 ros2 run my_package realtime_node

# 运行时修改
# 先找到进程PID
ps -aux | grep realtime_node
# 假设PID是12345
taskset -p -c 2,3 12345

# 查看当前亲和性
taskset -p 12345
# 输出:pid 12345's current affinity mask: c
# 0xc 表示二进制 1100,即CPU2和CPU3

2.3.3 使用cpuset做更精细的控制

taskset只能设置亲和性,但无法控制其他任务不进入这些CPU。cpuset可以做到双向隔离。

# 创建cpuset
sudo mkdir /sys/fs/cgroup/cpuset/rt_tasks
echo "2-3" > /sys/fs/cgroup/cpuset/rt_tasks/cpuset.cpus
echo "0" > /sys/fs/cgroup/cpuset/rt_tasks/cpuset.mems

# 将实时任务移入
echo 12345 > /sys/fs/cgroup/cpuset/rt_tasks/cgroup.procs

避坑指南:我曾经在一个项目里只用了taskset,没做CPU隔离。结果系统后台的kswapd进程还是跑到了实时CPU上,导致控制周期抖动从50us飙到了500us。后来加上isolcpus参数,问题才解决。记住:亲和性设置和CPU隔离要配合使用,缺一不可。

2.4 验证实时性能

配置完了,怎么知道效果好不好?我一般用cyclictest来测延迟。

# 安装rt-tests
sudo apt install rt-tests

# 在隔离的CPU上运行测试
sudo cyclictest -p 99 -t 1 -n -l 100000 -a 2 -i 1000

# 参数说明:
# -p 99    :实时优先级
# -t 1     :1个测试线程
# -n       :使用nanosleep
# -l 100000:测试10万次
# -a 2     :绑定到CPU2
# -i 1000  :间隔1000微秒(1ms)

# 重点关注输出中的 Max 值
# 普通内核:Max 通常在 100-500us
# PREEMPT_RT + 调优后:Max 应小于 50us

我记得第一次在调优后的系统上跑cyclictest,看到Max值只有15us时,心里那个爽啊。但别高兴太早,这只是裸系统的表现。加上ROS2的DDS通信后,延迟会有所增加,我们后面章节会讲怎么优化。

2.5 本章小结

这一章我们干了三件事:

  • 打了PREEMPT_RT补丁,让内核变得可抢占
  • 调了内核参数,减少不必要的延迟源
  • 做了CPU隔离和亲和性设置,给实时任务专属跑道

这三步做完,你的系统已经具备了运行实时任务的基础。下一章,我们会把ROS2的executor配置和这些底层优化结合起来,真正实现端到端的实时控制。

嗯,先消化一下今天的内容。有什么问题,我们课上交流。