第 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 不自动等于硬实时。硬实时的完整前提是四件一起:
- 内核抢占模型切到
preempt=rt(或装-rt变体)。 - 实时进程用
SCHED_FIFO/SCHED_RR起,RLIMIT_RTTIME允许它长占 CPU。 - 关键 CPU 做隔离(
isolcpus/nohz_full/rcu_nocbs),中断走别的核。 - 频率与 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 结果决定参数留下还是退回。