跳转至

第 7 章 内核、启动与实时调优

第 4 章的 chrt 让进程进入 SCHED_FIFO 只是用户态的一半;另一半在内核:抢占模型决定内核临界区能不能被抢占、CPU 隔离决定中断和调度器 tick 会不会打扰实时核、频率调节与深度睡眠决定唤醒一核要多久。这一章把内核侧的地基立起来:内核变体与 GRUB 启动参数、PREEMPT_RT 的语义与合流历史、cyclictest 抖动测量的最小形式、CPU 隔离与中断线程化的命令集、内存侧 mlockall 与 HugeTLB、频率与 C 态调优。判据统一:抖动数字必须由测量工具给出,测量命令与硬件平台一起写进正文;换平台不引用他处数字。

1. 内核版本、变体与 GRUB 启动参数

1.1 内核变体包

Ubuntu 的内核包分四类,命名与用途对应:

包 抢占模型 场景
linux-image-generic 通用(PREEMPT_DYNAMIC 于 5.10 起、此前 PREEMPT_VOLUNTARY) 默认
linux-image-lowlatency PREEMPT 打开,非硬实时 音频工作站、轻量实时
linux-image-realtime PREEMPT_RT 补丁版 硬实时控制回路
HWE 版(后缀 -hwe-22.04) 与前同、只是内核主线更新 新硬件

uname -v 或 /proc/cmdline 里能看到实际跑的变体与启动参数。Ubuntu 22.04 的仓库直接提供 RT 变体:linux-image-realtime(候选 5.15.0-1032.31),与 linux-image-lowlatency(5.15.0-195.160)同属 5.15 系列;

HWE 线只跟到 linux-image-lowlatency-hwe-22.04(6.8.0-146),没有对应的 realtime HWE 包——硬实时内核在 22.04 上停在 5.15 线。变体是否落地以 uname -r 的后缀为判据(-generic / -lowlatency / -realtime),不靠 apt list --installed 的混装推断。

Canonical 的 Real-time Ubuntu 文档(取用 2026-11)给出了各版本的正式供给形态:22.04 的实时内核对应 5.15 线、变体为 generic 与 intel-iotg 两种,22.04 HWE 的实时内核只有 6.8 generic;24.04 的实时内核为 6.8,变体 generic 与 raspi(树莓派)——22.04 没有的"realtime HWE"在 24.04 侧同样没有,HWE 线只供 generic;

26.04 起升到 7.0。安装渠道也按版本分界:24.04 及更早需先经 Ubuntu Pro 启用 realtime-kernel 服务(sudo pro attach + sudo pro enable realtime-kernel,再装 linux-realtime),26.04 起可直接从主仓库 sudo apt install ubuntu-realtime,树莓派上装 linux-raspi-realtime。

1.2 内核模块三命令

lsmod                                    # 已加载模块与引用计数
modinfo nvidia | head                    # 模块参数、依赖、vermagic
sudo modprobe -v i2c-dev                 # 显式加载;-r 卸载
ls /sys/module/<name>/parameters/        # 运行时可查的参数(如 nvidia NVreg_*)
sudo dmesg -T | tail                     # 内核日志(模块加载、错误、Secure Boot 拒载)

模块黑名单在 /etc/modprobe.d/*.conf:blacklist nouveau + install nouveau /bin/false 两条组合才彻底禁用 nouveau。

1.3 GRUB 启动参数与内核升级

改启动参数走 /etc/default/grub 里的 GRUB_CMDLINE_LINUX_DEFAULT,改完 sudo update-grub。查生效值:

cat /proc/cmdline

一台机器人上位机的 /proc/cmdline 里常见这类组合(示例读自 6.8.0-138-generic / Ubuntu 22.04 HWE 内核的实时配置现场,仅节选关键项):

BOOT_IMAGE=/boot/vmlinuz-6.8.0-138-generic root=UUID=... ro quiet splash
  preempt=full
  isolcpus=2,3 rcu_nocbs=2,3 nohz_full=2,3 irqaffinity=0xfffffff3
  intel_idle.max_cstate=0 processor.max_cstate=0 i915.enable_rc6=0
  tsc=reliable clocksource=tsc hpet=disable nmi_watchdog=0 nosoftlockup
  hugepages=0 audit=0 rcupdate.rcu_cpu_stall_suppress=1 numa_balancing=disable

每一条都是"实时回路上的一层噪声源关掉";逐条含义在「CPU 隔离与中断线程化」「内存与频率调优」两节。

内核升级与回退:sudo apt install linux-image-<ver> 装新内核、sudo apt remove linux-image-<old> 卸旧、GRUB 高级选项里选具体内核启动。生产上位机常保留两个可引导内核(当前 + 上一个),新内核出问题 GRUB 里选回上一个即可;引导管理器写"下一次启动选哪个"用 grub-reboot,写"默认"用 grub-set-default;直接读写 grubenv 环境块用 grub-editenv /boot/grub/grubenv set next_entry=…(上面两条命令都是它的封装)。

启动耗时定位一行命令:systemd-analyze 给总时间、systemd-analyze blame 给每个 unit 耗时、systemd-analyze critical-chain <unit> 给关键链。需历史火焰图时可用 bootchart(sudo apt install systemd-bootchart,启动参数加 systemd.bootchart),但日常量级定位一个 systemd-analyze 已够。

引导栈本身有两条:BIOS/MBR 走 grub-pc、UEFI 走 grub-efi-amd64;某些设备(如部分板端)用 systemd-boot 或 u-boot,与本板块主线关系不大。

从加电到实时进程的启动链:

flowchart LR
    fw["UEFI / BIOS"] --> grub["GRUB<br/>挑内核变体、传启动参数"]
    grub --> kern["内核 + initrd<br/>preempt / isolcpus / nohz_full"]
    kern --> init["systemd(PID 1)"]
    init --> rt["服务与实时线程<br/>SCHED_FIFO + 绑核"]

2. PREEMPT_RT 与 cyclictest

2.1 三种抢占模型

Linux 的抢占模型决定"内核代码在多长路径上可被抢占":

  • PREEMPT_NONE:只在系统调用返回与显式 cond_resched() 处让路,吞吐优先。
  • PREEMPT_VOLUNTARY:在更多点插入显式让路调用,桌面友好。
  • PREEMPT:几乎所有自旋锁转成可睡眠锁,中断线程化,实时性优先。
  • PREEMPT_RT(RT 补丁):把最后一段硬自旋也转成本地锁 + 优先级继承,中断线程化、软中断可抢占。

5.14 起 PREEMPT_RT 的大部分基础设施已并入主线(PREEMPT_DYNAMIC 允许启动时选模型);启动参数 preempt=full 或 preempt=rt 切当前模型。查当前实际生效:

cat /sys/kernel/debug/sched/preempt 2>/dev/null || cat /sys/kernel/debug/sched/features
uname -v            # 输出里含 SMP PREEMPT / PREEMPT_RT / PREEMPT_DYNAMIC 字样
grep PREEMPT /boot/config-$(uname -r) | head

坑:装了 RT 内核不等于硬实时

抢占性只是地基;用户态还要 SCHED_FIFO、绑核、关频率调节与运行期禁分配,缺一项抖动就回到毫秒级。

2.2 PREEMPT_RT 判据边界

装 PREEMPT_RT 不自动等于硬实时。硬实时的完整前提是四件一起:

  1. 内核抢占模型切到 preempt=rt(或装 -rt 变体)。
  2. 实时进程用 SCHED_FIFO / SCHED_RR 起,RLIMIT_RTTIME 允许它长占 CPU。
  3. 关键 CPU 做隔离(isolcpus / nohz_full / rcu_nocbs),中断走别的核。
  4. 频率与 C 态锁定(performance governor、禁 C6 / 深睡),避免"唤醒即 100 μs 起步"。

单做 1,抖动通常改善有限;ROS2-note 第 6 章的实时性专题与本章配合才成立。

判据:抖动数字必须带平台与命令

报数时写全 cyclictest 参数、内核变体与 CPU 型号;换平台不引用他处数字——抖动是“这套硬件 + 这套配置”的属性。

2.3 cyclictest 的最小形式

cyclictest(rt-tests 包)起一个高精度周期线程(默认 1 ms),测"应醒的时间 vs 实醒的时间"差值分布。最小命令:

sudo apt install rt-tests
sudo cyclictest -t 1 -p 80 -a 2 -i 1000 -m -l 100000 -h 500
## -t 1        一个测量线程
## -p 80       SCHED_FIFO 优先级 80
## -a 2        绑到 CPU 2(假设 CPU 2 已 isolcpus)
## -i 1000     周期 1000 μs
## -m          mlockall,避免测试进程本身触发缺页
## -l 100000   跑 10 万周期(约 100 s)
## -h 500      跑完打印延迟直方图,跟踪上限 500 μs

输出六列:I(interval)、HIST、Min、Avg、Max、Act。看的是分布而非单点:Avg 反映常态抖动,Max 是最坏一次的尾峰,二者要一起读,样本量太小(几百次)抓不到长尾。典型量级:通用内核即使已经做了 CPU 隔离,Max 仍常见数百微秒级的尖峰;换上 PREEMPT_RT 内核并把频率锁定到 performance,Max 才能收敛到几十微秒以内。具体数值与硬件、内核版本、背景负载强相关,换平台需重新测。

各选项语义以随发行版的手册页为准(以下引 cyclictest(8) 手册页):-i 为线程基准周期(默认 1000 μs);-h US 运行结束后打印延迟直方图,参数值是跟踪的最大延迟(微秒),并让所有线程同优先级;-p 给首个线程设指定优先级、后续线程逐级递减;-m 即 mlockall;-N 以纳秒显示(默认微秒)。Canonical 的 Real-time Ubuntu 文档给出的基线形态与之一致:

sudo cyclictest --mlockall --smp --priority=80 --interval=200 --distance=0

文档同时要求以 root 或 realtime 组成员身份运行;限定样本量用 --loops=,定时长用 --duration=(后缀 m/h/d),微秒改纳秒用 --nsecs。

两个容易踩的点:

  • 启动参数到位≠硬实时。一条调优过的 cmdline 可能已经带齐 isolcpus / nohz_full / rcu_nocbs / irqaffinity / intel_idle.max_cstate=0 这类隔离与禁深度睡眠参数,但只要内核仍是 PREEMPT_DYNAMIC 的通用内核、scaling_governor 还是 schedutil,就还不构成硬实时——抢占模型与频率锁定这两步省不了(判据见「PREEMPT_RT 判据边界」)。
  • 隔离核绑不上。对已被 isolcpus=2,3 隔离的核跑 cyclictest -a 2,若当前 shell 所在 cgroup 的 cpuset 不含该核,会报 No allowable cpus to run on / Couldn't setaffinity … Invalid argument(可用 /proc/self/status 的 Cpus_allowed_list 确认本进程能用到哪些核)。要么不绑核跑基线,要么把测量进程放进允许该核的上下文(如 systemd 单元配 AllowedCPUs=2)再测。

通用内核与 RT 内核的对比,应同一台机、同一权限、各跑同样时长(--duration),比较两侧的 Max 与高分位——这组数字随内核切换而变,本文不固定给出,按上面的命令在目标机上自测。

3. CPU 隔离与中断线程化

3.1 隔离三件套

目标:让 CPU 2(例)成为"只跑你的实时线程"的核,其它一切都调度到别的核上。三件套含义:

  • isolcpus=2,3:调度器默认不把任何普通任务分派到 CPU 2、3;systemd 与 taskset 显式指定除外。
  • nohz_full=2,3:CPU 2、3 上只有一个 runnable 任务时停调度器 tick(1000 Hz 的周期性中断消失),进一步减少噪声。要求配 nohz=on 与 rcu_nocbs=。
  • rcu_nocbs=2,3:CPU 2、3 的 RCU 回调(一种内核内的读-写-同步机制)交给其它核跑。

/sys/devices/system/cpu/isolated 与 nohz_full 反映当前生效值。查 CPU 拓扑与 NUMA:

lscpu -e                   # 每核的 node / socket / core
cat /sys/devices/system/cpu/cpu2/topology/thread_siblings_list
numactl -H

3.2 中断亲和性

/proc/interrupts 每行是一个中断源;把中断从实时核挪走有两层做法:

## 全局:启动参数里 irqaffinity=0xfffffff3 意思是 IRQ 优先落到除 CPU 2 之外的所有核
## 单条 IRQ:运行时改
cat /proc/irq/44/smp_affinity      # 当前允许的 CPU 位图(十六进制掩码)
echo 1 | sudo tee /proc/irq/44/smp_affinity     # 只允许 CPU 0

内核线程 [irq/44-xxx] 与 [kworker/u:2] 会跟着 IRQ 亲和性走。注意:irqbalance 守护进程会周期性地重排 IRQ 亲和性——做 CPU 隔离时要 sudo systemctl mask irqbalance 关掉。

3.3 中断线程化

PREEMPT_RT 下,硬中断上半部把活交给一个高优先内核线程(/sys/kernel/debug/irq/threads/ 或 ps -eLo psr | grep irq/ 可见)。这让"中断处理"也能被调度、可被抢占——软实时语义由此得到。查具体中断是否线程化:

cat /proc/interrupts | head                # 名字带 :Threaded 的是已线程化
grep . /proc/irq/*/*_threaded 2>/dev/null  # 部分内核版本提供

threaded_irq 语义细节属内核侧编程,本板块止于运行时观察与配置。

4. 内存与频率调优

4.1 mlockall 与预分配

缺页(page fault)走内核态、可触发磁盘换页——实时回路里最贵的意外。用户态在启动时调 mlockall(MCL_CURRENT | MCL_FUTURE) 把当前与未来的所有页都锁在物理内存:

#include <sys/mman.h>
if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) { /* 处理 */ }

ROS 2 节点由 rclcpp 或 rclpy 起,一般不在应用层显式 mlockall;控制栈(ros2_control 的实时控制器)里预分配是常见做法。判据:grep VmLck /proc/<pid>/status 看锁了多少;RLIMIT_MEMLOCK 通过单元文件 LimitMEMLOCK=infinity 放开。

4.2 HugeTLB 与 swap

  • HugeTLB:2 MiB 或 1 GiB 大页,减少 TLB miss;实时程序若工作集大、访存局部性差可受益。启动参数 default_hugepagesz=2M hugepagesz=2M hugepages=512 预留;/proc/meminfo 的 HugePages_* 行核对。
  • swap / zram:机器人上位机的经验做法是保留 swap 但限制 swappiness(vm.swappiness=10 或不换出关键进程),直接关 swap 会让 mmap 大块 buffer 时无兜底、触发 OOM。候选靶子:"swap 有害应直接关闭"——zram(内存压缩为块设备再当 swap)在内存偏紧的上位机反而是缓解 OOM 的选项。

判据:是否关 swap 看是否用 mlockall + 明确内存预算,两者都做了才谈得上关。

4.3 频率调节与深度睡眠

CPU 自动调频(cpufreq)与 ACPI 深度睡眠(C 态)是唤醒延迟的两大源。

## 查当前 governor 与可用 C 态
cpupower frequency-info
cat /sys/devices/system/cpu/cpu2/cpufreq/scaling_governor
cat /sys/devices/system/cpu/cpu2/cpuidle/state*/name       # POLL / C1 / C6 ...
cat /sys/devices/system/cpu/cpu2/cpuidle/state*/latency    # 每档唤醒延迟(μs)

## 切 performance
sudo cpupower frequency-set -g performance
## 或写启动参数 intel_pstate=disable + governor=performance(Intel)
## 或 amd_pstate=disable(AMD)

禁深睡:intel_idle.max_cstate=1 或 processor.max_cstate=1 只允许 C1;C-state 与 package C-state 要一起禁,用 tuned 或 power-profiles-daemon 会覆盖,机器人上位机常卸载这两个守护。

TSC 时钟源:tsc=reliable clocksource=tsc hpet=disable 把高精度定时器固定到 CPU 内置计数器上,避免 hpet / acpi_pm 的抖动;配合 nohpet 与 notsc 反例。

4.4 休眠、看门狗与噪声源

  • 禁 suspend / hibernate:sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target;机器人上位机进入睡眠=失联。
  • 关 NMI watchdog:nmi_watchdog=0 nosoftlockup 减少一次周期采样。
  • audit:audit=0 关掉内核审计;rcu_nocb_poll=1024 让 RCU 回调轮询化、少一次中断。
  • PCIe ASPM:pcie_aspm=off 关掉 PCIe 电源状态迁移,避免网卡 / GPU 从低功耗唤醒时的一次性延迟。

每条都对应一次"某 100 μs 抖动来自哪儿"的排查证据;判据是逐项开关做 A/B、用 cyclictest 量化,不打包上一堆参数再说"这样更好"。

5. 要点

  • 内核变体与模块:-generic / -lowlatency / -realtime 语义不同,22.04 仓库三者均提供(RT 停在 5.15 线);改启动参数走 /etc/default/grub + update-grub,/proc/cmdline 是权威读数;保留两个可引导内核便于回退。
  • PREEMPT_RT 不单独成立:抢占模型 + SCHED_FIFO + CPU 隔离 + 频率与 C 态锁定四件一起;ROS2-note 第 6 章的控制侧配套在本章内核侧完成。
  • 测量命令 = 数字的锚:cyclictest -p <fifo> -a <isol> -i <period> -m -l <loops> 的形态固定,Max 与 99.9 分位一起报;换硬件与内核都要重跑。
  • 隔离三件套 isolcpus / nohz_full / rcu_nocbs + irqaffinity 让实时核"没人敲门";irqbalance 要 mask;中断线程化在 RT 下自动生效。
  • 内存锁、频率锁、睡眠关:mlockall 与 LimitMEMLOCK=infinity 保证缺页零;performance governor + 禁 C6 + TSC 时钟源保证唤醒延迟零;suspend 与 hibernate 一律 mask。
  • 每项开关单独对比:一次性上十个参数无法归因;以 cyclictest 结果决定参数留下还是退回。

参考文献与延伸阅读

  • Linux Foundation. PREEMPT_RT Wiki. 文档
  • cyclictest. 手册页. 文档
  • Linux Kernel. 管理指南. 文档