12 · 部署与调优(ARM64):让 RT 内核真正拿到 10 µs 级延迟
结论:RT 内核提供的是「可抢占的内核」,不是「不被打扰的 CPU」。ARM64 上要把最坏延迟压到 10–50 µs,还需要四层配合:固件与电源管理收口(ARM64 没有 SMI,但有 EL3 安全世界、PSCI 深度 idle、DVFS/SCMI 这些等价噪声源)、CPU 分区(
isolcpus/nohz_full/rcu_nocbs/irqaffinity,big.LITTLE 平台选大核簇)、优先级栅格(内核自建 FIFO 线程一律 50,必须重排)、应用侧(锁内存、预分配、PI 互斥锁、钉核)。x86 的intel_idle.max_cstate、processor.max_cstate、tsc=、idle=poll在 arm64 上不存在,别照抄。
第一层:部署形态
先看结论:RT 内核 + CPU 分区 + 优先级栅格三者叠加才有 10 µs 级;缺任何一个都会被固件 / 中断 / 内核线程的噪声吃掉。
第二层:抖动来源与检查清单
先看结论:按固件 → 配置 → 启动参数 → 运行时 → 优先级 → 应用 → 验证七层逐层落实,每层都有对应的验证命令。
1. 固件 / SoC(ARM64 的「SMI 等价物」)
| 噪声源 | 做法 | 验证 |
|---|---|---|
| EL3 安全世界(TF-A、OP-TEE、SCP 固件) | 热路径不调用 OP-TEE / SCMI 同步接口;固件升级到厂商 RT 推荐版本;关闭固件侧周期性任务(温控、DVFS 轮询) | hwlatdetect --duration=1h --threshold=20:hwlat tracer 架构无关,能抓到内核看不见的固件停顿 |
| PSCI idle 状态 | 只留最浅态:启动参数 cpuidle.off=1,或运行时 echo 1 > /sys/devices/system/cpu/cpuN/cpuidle/stateM/disable(drivers/cpuidle/sysfs.c:302-325);ARM_PSCI_CPUIDLE 在分层 idle 布局下 RT 只能用 per-CPU 状态(drivers/cpuidle/Kconfig.arm:27-33,_DOMAIN 默认 y);PSCI 默认 PC 模式、OSI 探测到也不启用(drivers/firmware/psci/psci.c:736-740),进入深态的 SMC(:469-480)之后的唤醒延迟由固件决定、内核看不见 |
cpupower idle-info;/sys/devices/system/cpu/cpu*/cpuidle/state*/{usage,time} 在测试期间不增长 |
| DVFS / SCMI | cpufreq.default_governor=performance 或 cpupower frequency-set -g performance;SCMI 平台确认 SCP 固件不会自行降频 |
cpupower frequency-info;scaling_cur_freq 稳定 |
| big.LITTLE / DynamIQ | RT 线程钉在大核簇;隔离集选大核;kernel.sched_energy_aware=0(kernel/sched/topology.c:211, 317-322, 432,默认 1)减少 CFS 侧能效迁移噪声 |
/sys/devices/system/cpu/cpu*/cpu_capacity(drivers/base/arch_topology.c:215,来自 DT capacity-dmips-mhz :321-322);lscpu -e |
| EFI runtime(UEFI 服务器) | RT 默认已禁(drivers/firmware/efi/Kconfig:240-249);U-Boot 嵌入式板无此项 |
dmesg | grep -i efi |
| RAS / SDEI | firmware-first RAS 通过 SDEI 事件打断 normal world;生产环境评估是否关闭轮询式 RAS 扫描 | dmesg | grep -i sdei,hwlatdetect |
| GIC 伪 NMI | CONFIG_ARM64_PSEUDO_NMI + irqchip.gicv3_pseudo_nmi=1 让 perf 采样与硬死锁检测用 NMI;NMI 处理函数必须只用 raw 锁;调试期开、生产按需 |
dmesg | grep -i pseudo-NMI |
2. 内核配置(arm64)
CONFIG_PREEMPT_RT=y(EXPERT=y;arch/arm64/Kconfig:105 提供 ARCH_SUPPORTS_RT)、HZ_1000、NO_HZ_FULL=y、RCU_NOCB_CPU=y、HIGH_RES_TIMERS=y;页大小按需选 ARM64_16K_PAGES/ARM64_64K_PAGES(补偿 THP 缺失);可选 ARM64_PSEUDO_NMI;ARM_PSCI_CPUIDLE 留意 RT 限制。关 DEBUG_PREEMPT、LOCKDEP、KASAN、DEBUG_RT_MUTEXES、SLUB_CPU_PARTIAL(help 建议)。验证:zcat /proc/config.gz | grep -E 'PREEMPT_RT|NO_HZ_FULL|RCU_NOCB|ARM64_.*_PAGES|PSEUDO_NMI'。
3. 启动参数(arm64)
isolcpus=domain,managed_irq,4-7 nohz_full=4-7 rcu_nocbs=4-7 irqaffinity=0-3 skew_tick=1
cpuidle.off=1 cpufreq.default_governor=performance rcutree.kthread_prio=3
nowatchdog nosoftlockup
(示例为 8 核 big.LITTLE:0–3 小核做 housekeeping,4–7 大核隔离;同构核按需分配。)
| 参数 | 作用 | 相关源码/默认 |
|---|---|---|
isolcpus=domain,… |
隔离 CPU 退出调度域,负载均衡不再碰它 | kernel/sched/isolation.c |
isolcpus=managed_irq,… |
managed 中断(NVMe、多队列网卡)不落在隔离 CPU | 同上 |
nohz_full= |
隔离 CPU 单任务时停 tick(arch timer 仍在,只是不再周期触发) | 需 NO_HZ_FULL |
rcu_nocbs= |
RCU 回调 offload 到 rcuo 线程(在 housekeeping) |
第 08 章 |
irqaffinity= |
默认中断亲和性;GIC SPI 全部落 housekeeping | 第 03 章 |
skew_tick=1 |
错开各 CPU tick | 第 05 章 |
cpuidle.off=1 |
完全关 cpuidle,CPU 只做 WFI;耗电换延迟 | drivers/cpuidle/cpuidle.c:40-45, 811-818;kernel-parameters.txt:894-898 |
cpufreq.default_governor=performance |
固定最高频 | drivers/cpufreq/cpufreq.c:51, 3078-3084;kernel-parameters.txt:903-906 |
rcutree.kthread_prio= |
rcuc/rcub 优先级(默认 1,kernel/rcu/tree.c:168) |
第 08 章 |
irqchip.gicv3_pseudo_nmi=1 |
GICv3 伪 NMI(可选;开启后伪 NMI 返回不抢占、关中断改走 PMR) | arch/arm64/kernel/cpufeature.c:1093-1097;kernel-parameters.txt:2417-2420 |
kpti=off |
仅对 kpti_safe_list 外且 CSV3=0 的核(如 A75)有意义;A53/A55/A72/A73/A76+ 本就关闭 |
arch/arm64/kernel/cpufeature.c:1737, 1982;kernel-parameters.txt:2657 |
mitigations=off / nospectre_bhb / nospectre_v2 / ssbd=force-off |
去掉 Spectre 系列入口开销(几十 ns/次) | kernel/cpu.c:3230;arch/arm64/kernel/proton-pack.c:90, 413, 1027;kernel-parameters.txt:3515, 4108, 4116 — 需安全评审 |
arm64.nosve / arm64.nomte / arm64.nobti / arm64.nopauth |
关闭对应扩展 | kernel-parameters.txt:446-461;无 SVE 用户时关 SVE 可省切换开销 |
iommu.strict=0 / iommu.passthrough=1 |
SMMU 惰性失效 / 直通 | drivers/iommu/iommu.c:44, 207-208;kernel-parameters.txt:2345, 2360 |
cma=512M(按需) |
预留连续内存,配合启动期一次性申请 | kernel/dma/contiguous.c:91;kernel-parameters.txt:728 |
threadirqs / preempt= |
RT 上不存在 | kernel/irq/manage.c:27-35;kernel/Kconfig.preempt:98 |
intel_idle.* / processor.max_cstate / tsc= / idle=poll |
x86 专用,arm64 无效 | kernel-parameters.txt:2001-2002, 2260-2262, 4883, 7013-7016(标 [X86]) |
验证:cat /sys/devices/system/cpu/isolated、cat /sys/devices/system/cpu/nohz_full、cat /proc/interrupts 隔离列应几乎全 0(只剩 arch timer 与 IPI)。
4. 运行时
sysctl -w kernel.sched_rt_runtime_us=-1 # 取消 95% 限流(kernel/sched/rt.c:17-21 默认 950000)
sysctl -w kernel.timer_migration=0
sysctl -w kernel.sched_energy_aware=0 # big.LITTLE 平台
sysctl -w vm.stat_interval=120
sysctl -w kernel.hung_task_timeout_secs=0
sysctl -w net.core.busy_poll=50 -w net.core.busy_read=50 # 雷达/相机 UDP 高 pps 时(RT 上 NET_RX_BUSY_POLL 默认开)
sysctl -w kernel.printk='3 4 1 3' # 降低控制台输出量
systemctl stop irqbalance # 或 IRQBALANCE_BANNED_CPUS 掩码
echo 0-3 > /proc/irq/default_smp_affinity
for i in /proc/irq/*/smp_affinity_list; do echo 0-3 > $i 2>/dev/null; done
for s in /sys/devices/system/cpu/cpu[4-7]/cpuidle/state[1-9]*/disable; do echo 1 > $s; done # 未用 cpuidle.off 时
RHEL 用 tuned-adm profile realtime;Ubuntu Pro real-time(有 arm64 版)用对应 tuned profile,两者做的就是上面这些。
5. 线程与中断优先级
内核自己创建的 FIFO 线程一律在 50(kernel/sched/syscalls.c:836-861 的注释:「管理员必须配置系统」)。arm64 上 arch timer(per-CPU)与 IPI 不会被线程化,其余 GIC SPI 中断都有 irq/N-name 线程。建议栅格:
| 优先级 | 谁 |
|---|---|
| 99 | migration/N、watchdog/N(内核固定,别动) |
| 85 | RT 应用真正依赖的设备的 irq/* 线程(如实时以太网、CAN、SPI 从设备) |
| 80 | RT 应用线程 |
| 50 | 其余 irq/*(默认) |
| 3 | rcuc/N、rcub/N(rcutree.kthread_prio=3) |
| OTHER | ksoftirqd、kworker、pr/legacy、napi/*、普通任务 |
chrt -f -p 85 $(pgrep -f 'irq/.*-eth1')
ps -eo pid,cls,rtprio,psr,comm | grep -E 'irq/|rcuc|ksoftirqd|napi/|pr/'
不要用 99:那是 stopper 与 watchdog 的位置,抢了它们系统会卡死。
6. 应用
mlockall(MCL_CURRENT | MCL_FUTURE);启动期触碰全部栈(pthread_attr_setstacksize+ 写一遍)与堆;mallopt(M_TRIM_THRESHOLD, -1); mallopt(M_MMAP_MAX, 0)。- 周期循环用
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME)(RT 任务走 HARD sleeper,第 05 章;arm64 的 arch timer 分辨率通常 1–24 MHz,精度足够)。 - 线程间互斥用
PTHREAD_PRIO_INHERIT(内核 PI futex);条件变量配 PI 互斥锁。 - 热路径避免:
malloc、printf、文件 I/O、fork、页错误、非确定性系统调用;不要调用会进安全世界的接口(OP-TEE 客户端、SCMI)。 taskset/pthread_setaffinity_np把 RT 线程钉在隔离的大核;一颗 CPU 一个主 RT 线程最稳。- 内核模块作者:不要依赖
local_bh_disable()/spin_lock_irqsave()的旧语义(RT 上变了,第 02/04 章);GIC NMI 处理函数只能用 raw 锁。
7. 验证与验收
cyclictest -m -Sp90 -i200 -h400 -D24h:最大值稳定在目标以内(Neoverse/A76+ 通常 < 50 µs;A53/A55 SoC < 100 µs)。osnoise/timerlat跑隔离 CPU:确认没有 IRQ/softirq/线程噪声;hwlatdetect确认没有固件停顿。- 背景压力下(网络、I/O、内存、GPU/显示)重复;升级 TF-A/SCP 固件或驱动后重复。
housekeeping / 隔离的原理、内核实现与友商做法见 第 13 章;自动驾驶 SoC 的完整参数集、三个分区样板与 hypervisor / 功能安全取舍见 第 14 章。
常见坑(ARM64)
| 现象 | 原因 | 章节 |
|---|---|---|
| RT 线程每秒卡 50 ms | 95% 限流没关 | 01 |
| 隔离 CPU 偶发 100–300 µs 尖峰,内核 trace 里看不到 | PSCI 深度 idle 退出,或安全世界/SCMI 调用;用 hwlatdetect 抓 |
12 |
| 小核上跑 RT 线程延迟大 | 隔离集选了 LITTLE 核;频率低、缓存小 | 12 |
| 网络吞吐掉一半 | 网卡 irq 线程/ksoftirqd 被 RT 应用压住,或没开硬件合并 | 03, 04 |
| RCU stall 告警 | RT 线程 > kthread_prio 且长跑,没 rcu_nocbs |
08 |
| 模块卸载/netns 销毁很慢 | 加急 GP 被禁 | 08 |
| 驱动在 RT 上 lockdep 报「原子上下文睡眠」 | spin_lock_irqsave 不关中断 / hardirq 里 kmalloc / raw 锁里拿 spinlock_t |
02, 06 |
| 开了 pseudo-NMI 后驱动崩溃 | NMI 处理函数里用了 spinlock_t(RT 下会睡) |
02, 03 |
hrtimer_cancel 死锁 / 睡眠告警 |
RT 上它可能睡眠,不能在 raw 区调用 | 05 |
| 64K 页内核下某些用户态程序/驱动异常 | 假设 4K 页(mmap 对齐、DMA 缓冲) | 06 |
小结
- ARM64 的「固件噪声」不是 SMI,而是 EL3 安全世界、PSCI 深度 idle、DVFS/SCMI、SDEI/RAS——用
hwlatdetect抓,用cpuidle.off/性能调速器/固件配置收口。 - 四件必做:
sched_rt_runtime_us=-1、CPU 分区(big.LITTLE 选大核)、irq 线程优先级重排、应用mlockall+ 预分配。 - x86 参数(
intel_idle、tsc=、idle=poll)在 arm64 上无效,别照搬。 - 承上启下:本章是 ARM64 通用方法;第 13 章专讲 housekeeping/隔离(内核实现与友商做法),第 14 章落到自动驾驶 SoC(干扰源、三个分区样板、完整参数集),第 15 章是源码索引,第 16 章是全文小结。