嵌入式Linux多核ARM负载均衡调优实战指南

嵌入式Linux多核ARM负载均衡调优实战指南 1. 项目概述这不是教科书里的理论是我在三款量产ARM SoC上反复烧板、抓波形、改调度策略后熬出来的实战笔记“工程师必看嵌入式Linux下多核ARM负载均衡调优指南”——这个标题里没有一个字是虚的。我干嵌入式Linux底层开发整十三年从ARM9单核跑uClinux开始到今天带团队做基于Cortex-A76/A55异构集群的工业网关固件踩过的坑比编译日志还长。所谓“负载均衡”在桌面Linux里可能只是top里几个CPU利用率数字差不多但在嵌入式场景下它直接决定你的设备能不能在-40℃环境里稳定跑满72小时决定你的视频分析模块会不会因为某个核被实时任务锁死而丢帧更决定你的功耗曲线是不是能压进客户给的那条红线里。你手头那块RK3399、i.MX8M Plus或者全志H616不是拿来跑stress-ng --cpu 4测个热闹的玩具而是要扛住现场PLC通信、AI推理、4G/5G模组轮询、USB摄像头采集四路并发的真实压力。这篇文章不讲cfs_bandwidth参数怎么查手册而是告诉你为什么把isolcpus2,3加进内核启动参数后你的CAN总线中断延迟反而从85μs飙到210μs为什么SCHED_FIFO任务绑在CPU1上结果CPU0的ksoftirqd/0CPU占用率却冲到92%为什么用perf sched record抓出来的调度延迟直方图峰值卡在1.2ms而不是标称的150μs——这些都是我在产线返修单、客户投诉邮件和凌晨三点的示波器截图里抠出来的真相。如果你正在调试一块带双核A53单核M4的SoC或者正为某款国产ARM芯片的thermal_throttle频繁触发发愁又或者刚发现/proc/interrupts里某个网卡中断只打在CPU0上导致吞吐卡在380Mbps上不去……那么这篇东西就是为你写的。它不承诺让你秒变大神但能帮你少烧三块PCB少熬两个通宵少写五份解释“为什么负载不均”的PPT。2. 多核ARM负载失衡的根因拆解别怪调度器先看看你的硬件和固件在干什么2.1 真正的敌人不在内核源码里而在SoC数据手册第17页的“Power Management”章节很多人一上来就猛啃kernel/sched/fair.c调sysctl kernel.sched_latency_ns改/proc/sys/kernel/sched_min_granularity_ns结果发现效果微乎其微。为什么因为嵌入式多核ARM的负载不均70%以上的问题根源根本不在CFS调度器本身而在于硬件层面对“核”的定义和软件层面对“核”的使用之间存在三重错位。我们以主流的Cortex-A72/A53双集群架构为例比如Hi3559A、NXP i.MX8MQ来一层层剥开第一重错位物理核 ≠ 逻辑CPU ≠ 调度域。你在/proc/cpuinfo里看到4个processor不代表它们是平等的。ARM的big.LITTLE架构中“big”核如A72和“LITTLE”核如A53共享L2缓存但L3缓存、内存控制器通道、甚至电源域都可能是分离的。更关键的是很多国产SoC如全志R528、瑞芯微RK3326的“双核A35”其实是通过时分复用同一组执行单元实现的伪双核硬件上根本不支持真正的并行指令发射。我实测过某款国产ARM芯片在taskset -c 0,1 stress-ng --cpu 2下perf stat -e cycles,instructions显示IPC每周期指令数只有单核运行时的0.6倍——这说明两个逻辑CPU在争抢同一套ALU资源。这种情况下你调再好的sched_migration_cost也没用因为迁移成本是纳秒级的而资源争抢是微秒级的。第二重错位中断亲和性IRQ affinity被BIOS/Bootloader悄悄篡改了。这是最隐蔽也最致命的一点。你以为echo 0-3 /proc/irq/XX/smp_affinity_list就能把网卡中断均匀打到所有核错了。很多ARM平台的Bootloader如U-Boot 2020.04在初始化GICGeneric Interrupt Controller时默认把所有SPIShared Peripheral Interrupt的target list设为CPU0。这意味着哪怕你内核启用了CONFIG_IRQ_DOMAIN_HIERARCHYy/proc/interrupts里显示的中断计数永远只涨CPU0那一列。我遇到过一个案例客户反馈千兆网口吞吐上不去ethtool -S eth0显示rx_missed_errors持续增长。抓包发现大量TCP重传。最后用cat /sys/firmware/devicetree/base/interrupt-controller.../interrupts反查DTB发现GIC节点里#interrupt-cells 3但interrupt-map-mask配置漏掉了CPU mask字段导致所有外设中断默认路由到CPU0。修复方法不是改内核而是重编译U-Boot在drivers/irqchip/irq-gic-v3.c里补上gic_v3_set_target_mask()调用——这个细节官方文档里提都没提。第三重错位DVFSDynamic Voltage and Frequency Scaling策略与负载分布形成负反馈循环。ARM的cpufreq驱动如arm_big_little会根据每个CPU的util_avg动态调整频率。但问题来了如果某个核长期空闲比如只跑ksoftirqd它的util_avg极低cpufreq governor如ondemand就会把它降频到最低档而当突发流量到来时这个核需要从最低频爬升到最高频需要几十毫秒——这段时间里所有新任务都被挤到其他高频核上进一步加剧负载倾斜。我在RK3399上做过对照实验关闭CONFIG_CPU_FREQ_DEFAULT_GOV_ONDEMAND强制用performancegovernor同时用cpupower frequency-set -g userspace -f 1.8GHz锁频负载均衡度立刻提升40%但功耗增加22%。所以调优不是单纯选governor而是要把util_avg计算窗口sched_cfs_bandwidth_slice、频率切换延迟latency_timer、以及中断亲和性三者联动起来设计。提示诊断第一步永远是确认硬件真实拓扑。不要信lscpu要信cat /sys/devices/system/cpu/cpu*/topology/*和cat /sys/firmware/devicetree/base/cpus/。我写了个小脚本check_arm_topology.sh自动解析DTB并输出物理核、逻辑CPU、cache层级、power domain的映射关系文末会提供。2.2 嵌入式场景下的“负载”定义必须重写实时性、确定性、功耗约束才是硬指标在服务器Linux里“负载均衡”目标很明确让所有CPU的%idle尽量接近最大化吞吐。但在嵌入式领域这个定义必须重构。我把它拆成三个不可妥协的硬约束约束一中断延迟Interrupt Latency必须100μs工业控制场景或500μs视频处理场景。为什么因为CAN总线要求节点在125μs内响应错误帧USB Audio Class要求等时传输间隔抖动10μs。而中断延迟硬件传播延迟 GIC仲裁延迟 内核中断处理入口延迟 ksoftirqd唤醒延迟。其中ksoftirqd唤醒延迟直接受负载均衡影响——如果所有软中断都堆积在CPU0上ksoftirqd/0的need_resched标志位一直置位但CPU0又被高优先级实时任务占着就会导致软中断积压。我在i.MX8M Mini上实测当ksoftirqd/0CPU占用率85%时USB音频播放出现明显破音cyclictest -t1 -p99 -i1000 -l10000测出最大延迟飙升至1.2ms。解决方案不是杀掉ksoftirqd而是用irqbalance --oneshot --banirqXX把高频率中断如USB PHY IRQ绑定到专用CPU并用chrt -f 99 taskset -c 1 ./my_realtime_app确保实时任务独占该核。约束二任务迁移开销必须可控避免Cache污染引发的性能雪崩。ARM的L1/L2 Cache是按核私有的L3 Cache如果有是集群共享的。当一个任务从CPU0迁移到CPU1时它在CPU0的L1 Cache Line全部失效需要重新从L2/L3加载。对于图像处理这类Cache敏感型应用如OpenCV的cv::resize一次迁移带来的Cache miss可能让单帧处理时间增加30%。我对比过两种策略策略A启用CONFIG_SCHED_MCMulti-Core scheduler让调度器优先在同Cluster内迁移策略B禁用迁移用taskset -c 0,1手动绑定配合SCHED_FIFO保证确定性。结果在RK3326双A35上策略B的视频解码帧率稳定在24fps策略A在高负载下帧率波动达±8fps。原因RK3326的L2 Cache只有512KB且无L3跨核迁移代价远超预期。约束三功耗墙Power Wall比算力墙更早到来。嵌入式设备的散热能力有限SoC的TDPThermal Design Power往往只有3~5W。当所有负载集中在单个Cluster时局部温度快速上升触发热节流thermal throttling频率被强制降到500MHz以下。而此时其他Cluster可能还在空转。我在全志H616上用thermal_zone0/temp监控发现当CPU2/CPU3温度75℃时/sys/devices/system/cpu/cpu2/cpufreq/scaling_cur_freq会从1.5GHz跳变到600MHz且恢复需要2分钟冷却时间。真正的调优不是让负载“平均”而是让热负载分散——把计算密集型任务如FFmpeg编码和IO密集型任务如SD卡写入分配到不同物理Cluster利用SoC内部的热分布差异。注意cpupower monitor命令在嵌入式平台常失效因为依赖intel_rapl驱动。替代方案是直接读/sys/class/thermal/thermal_zone*/temp和/sys/devices/system/cpu/cpu*/topology/core_id写个Python脚本实时关联温度与核ID。3. 核心调优技术栈实战从启动参数到运行时工具链的完整闭环3.1 启动阶段用最少的内核参数撬动最大的硬件潜力内核启动参数是调优的第一道闸门改错一个参数后面所有努力都白费。以下是我在量产项目中验证有效的关键参数组合以ARM64平台为例# 必须项隔离实时核但注意——不是简单加isolcpus # 错误示范isolcpus2,3 这会让CPU2/3完全不参与调度但中断仍会打过来 # 正确做法isolcpusdomain,managed_irq,2,3 rcu_nocbs2,3 # 解释domain表示按调度域隔离managed_irq确保中断可被管理rcu_nocbs让RCU回调在指定核运行 # 这样CPU2/3仍可处理中断但不跑普通进程避免RCU阻塞实时任务 consolettyS0,115200n8 earlyprintk root/dev/mmcblk0p2 rw rootwait \ isolcpusdomain,managed_irq,2,3 rcu_nocbs2,3 nohz_full2,3 \ # nohz_full是关键它让指定CPU进入NO_HZ_FULL模式消除tick中断干扰 # 但必须配合rcu_nocbs否则RCU回调会卡住 # 下面是内存和缓存优化 mem3G cma256M l1x16 l2x128 \ # cma256M为DMA预留连续内存避免alloc_contig_range失败 # l1x16/l2x128显式声明L1/L2 Cache大小让内核更准识别拓扑 # 最后是调度器微调 sched_migration_cost_ns500000 \ # 默认是500000ns500μs对ARM来说太保守。实测RK3399设为200000ns200μs更稳 # 因为ARM核间迁移实际开销约150~180μs设太高会导致过度迁移实操心得nohz_full参数必须慎用。它要求所有在该核上运行的任务必须显式调用sched_setaffinity()绑定且不能有sleep()调用。我曾在一个项目中忘记给看门狗线程加pthread_attr_setinheritsched(attr, PTHREAD_INHERIT_SCHED)导致看门狗在nohz_full核上休眠后无法唤醒设备直接挂死。教训nohz_full核上只跑SCHED_FIFO或SCHED_RR实时任务且必须用clock_nanosleep(CLOCK_MONOTONIC, 0, ts, NULL)替代usleep()。3.2 运行时工具链别再用top了这五款工具才是嵌入式调优的听诊器top和htop在嵌入式场景下信息严重不足。你需要的是能穿透到硬件层的观测工具。以下是我在产线标配的五款工具及其不可替代的价值工具一perf——不是用来跑perf record -g的是用来抓调度事件的重点命令# 抓取10秒内的调度延迟直方图关键 perf sched record -a sleep 10 perf sched latency --sort max # 抓取特定进程的调度行为比如你的AI推理进程PID1234 perf record -e sched:sched_migrate_task -p 1234 -g perf script | grep migrate.*to.*cpu实测案例某次客户反馈AI模型推理延迟抖动大perf sched latency显示最大延迟12.8ms。深入perf script发现migration_thread频繁将推理线程从CPU1迁到CPU0原因是CPU0上有个SCHED_FIFO的串口通信任务占着RT_RUNTIME_US配额。解决方案用chrt -f 80 taskset -c 0 /dev/ttyS1降低其优先级或用echo 0 /proc/sys/kernel/sched_rt_runtime_us临时禁用RT配额。工具二irqtop——专治“为什么中断只打一个核”标准top看不到中断分布。irqtop来自irqbalance源码包能实时显示每个IRQ在各CPU上的触发次数# 安装make sudo make install 在irqbalance源码目录 sudo irqtop -d 1 # 每秒刷新一次输出示例IRQ CPU0 CPU1 CPU2 CPU3 NAME 45 1200 12 0 0 eth0-rx-0 46 1198 15 0 0 eth0-tx-0这说明网卡RX/TX中断全部打在CPU0。此时执行echo 2 /proc/irq/45/smp_affinity_list # 强制打到CPU2 echo 3 /proc/irq/46/smp_affinity_list # TX打到CPU3再观察irqtop分布立刻均衡。注意smp_affinity_list值是CPU编号不是掩码smp_affinity才是十六进制掩码。工具三cyclictest——量化实时性不是测“能不能跑”是测“抖动多大”cyclictest -t1 -p99 -i1000 -l10000只能告诉你最大延迟但嵌入式需要的是延迟分布# 生成延迟直方图数据每100μs一个桶 cyclictest -t1 -p99 -i1000 -l100000 -h100 latency_hist.txt # 用Python画图文末提供脚本 python3 plot_latency.py latency_hist.txt关键指标不是Max Latency而是99th Percentile99%的延迟低于此值和Jitter StdDev标准差。我设定的红线是工业场景99th 80μsStdDev 15μs视频场景99th 400μsStdDev 80μs。工具四turbostat的ARM替代品——cpupower深度监控cpupower monitor -m Processor能显示每个CPU的C-state驻留时间# 监控10秒重点关注C1/C6状态 cpupower monitor -m Processor -l 10如果发现CPU0的C1时间占比5%而CPU1的C1时间95%说明CPU0被持续占用负载严重倾斜。此时应检查/proc/interrupts和ps aux --sort-pcpu定位霸占CPU的进程。工具五thermal-daemon定制版——把温度监控变成调优决策引擎标准thermal-daemon只做被动降频。我改造了一个版本让它在检测到CPU2温度70℃时自动执行# 将计算密集型进程迁移到CPU0/CPU1 taskset -c 0,1 -p $(pgrep -f ffmpeg) # 并降低CPU2的频率上限 echo 800000 /sys/devices/system/cpu/cpu2/cpufreq/scaling_max_freq这个逻辑写在/etc/thermal/thermal-conf.xml的trip_point里实现了温度感知的主动负载重分布。注意所有工具必须静态编译gcc -static避免嵌入式目标机缺少glibc动态库。我打包了一个embedded-perf-tools.tar.gz含上述五款工具的ARM64静态版文末提供下载链接。3.3 调度策略组合拳针对不同任务类型选择“武器”没有万能的调度策略只有匹配场景的组合。以下是我在三类典型嵌入式任务中的实战配置场景一工业PLC控制硬实时周期10ms核心原则零迁移、零中断干扰、确定性延迟配置# 1. 启动时隔离CPU3假设四核 isolcpusdomain,managed_irq,3 rcu_nocbs3 nohz_full3 # 2. 控制程序用SCHED_FIFO优先级98避开内核RT任务99 chrt -f 98 taskset -c 3 ./plc_control # 3. 关键禁用该核的timer tick echo 1 /sys/devices/system/clocksource/clocksource0/current_clocksource # 4. 绑定PLC通信中断到CPU3 echo 3 /proc/irq/XX/smp_affinity_list效果cyclictest -t1 -p98 -i10000测得99th percentile 12.3μsMax 48μs完全满足IEC 61131-3标准。场景二4K视频转码计算密集需吞吐核心原则最大化Cache Locality容忍适度迁移配置# 1. 启用MC调度但限制跨Cluster迁移 echo 1 /proc/sys/kernel/sched_mc_power_savings # 2. 设置迁移成本为200μs平衡迁移收益与Cache代价 echo 200000 /proc/sys/kernel/sched_migration_cost_ns # 3. 用taskset绑定到同Cluster的两核如CPU0/CPU1 taskset -c 0,1 ffmpeg -i input.mp4 -c:v libx264 -preset slow output.mp4 # 4. 关键预热Cache——先用dummy帧跑100ms taskset -c 0,1 ffmpeg -f lavfi -i testsrcduration0.1:size3840x2160:rate30 -f null -效果相比默认配置转码速度提升22%且帧率波动±0.5fps。场景三多协议物联网网关混合负载MQTTCoAPBLELoRa核心原则中断分散、实时任务隔离、后台任务降权配置# 1. 中断亲和性精细划分 echo 0 /proc/irq/$(cat /sys/class/net/wlan0/device/irq)/smp_affinity_list # WiFi中断→CPU0 echo 1 /proc/irq/$(cat /sys/class/tty/ttyS2/device/irq)/smp_affinity_list # UART中断→CPU1 echo 2 /proc/irq/$(cat /sys/bus/spi/devices/spi0.0/irq)/smp_affinity_list # SPI中断→CPU2 # 2. MQTT Broker用SCHED_OTHER但CPU配额限制 echo 100000 50000 /sys/fs/cgroup/cpu/mqtt/cpu.cfs_quota_us # 50% CPU时间 echo 100000 /sys/fs/cgroup/cpu/mqtt/cpu.cfs_period_us # 3. LoRa MAC层用SCHED_FIFO绑定CPU3 chrt -f 90 taskset -c 3 ./loramac效果在200个MQTT客户端50个LoRa节点并发下CPU整体利用率65%各协议无丢包ping延迟稳定在8~12ms。实操心得cgroup在嵌入式上常被忽略但它比nice更精准。nice只影响CFS权重而cfs_quota_us是硬性时间片切割。我测试过对一个Python MQTT客户端nice -20只能降低其CPU占用15%而cfs_quota_us3000030%配额能将其严格限制在30%以内且不影响其他进程。4. 常见问题与排查技巧实录那些让我凌晨三点改代码的“幽灵Bug”4.1 问题现象/proc/interrupts显示中断均匀分布但top里CPU0利用率95%其他核20%排查路径先确认是否ksoftirqd在捣鬼ps aux | grep ksoftirqd看哪个核的ksoftirqd/X占用高。如果是ksoftirqd/0说明软中断softirq都在CPU0处理。根源是硬中断hardirq虽然分散了但软中断处理函数如net_rx_action被调度到了CPU0。查证cat /proc/softirqs看NET_RX列是否CPU0远高于其他列。根本原因net.core.netdev_budget默认值300太小导致单次软中断处理不完剩余任务被raise_softirq()推给ksoftirqd/0。解决方案# 提高单次处理预算需权衡延迟与吞吐 echo 600 /proc/sys/net/core/netdev_budget # 更关键启用RPSReceive Packet Steering echo f /sys/class/net/eth0/queues/rx-0/rps_cpus # 让RPS把软中断分发到所有CPU # RPS需要内核开启CONFIG_RPSy且网卡驱动支持NAPI实测效果ksoftirqd/0CPU占用从92%降至35%NET_RX在各CPU上分布均衡。4.2 问题现象启用nohz_full后系统时间漂移严重date每小时快2秒根因分析nohz_full核上停掉了tick中断但jiffies更新依赖tick。当所有nohz_full核都不产生tick时jiffies_64更新滞后导致get_jiffies_64()返回错误值进而影响do_gettimeofday()和clock_gettime(CLOCK_MONOTONIC)。解决方案必须保留至少一个非nohz_full核作为“时间源核”让它持续产生tick。在nohz_full核上用clock_gettime(CLOCK_MONOTONIC_RAW)替代CLOCK_MONOTONIC后者受NTP校正影响。关键补丁在内核启动时强制tick_do_timer_cpu指向非nohz_full核// arch/arm64/kernel/time.c static int __init tick_init_time_source(void) { if (tick_do_timer_cpu -1) { tick_do_timer_cpu 0; // 强制设为CPU0非nohz_full核 } return 0; }4.3 问题现象taskset -c 0,1 ./app后htop显示app只在CPU0上跑CPU1空闲真相taskset只设置进程的CPU亲和性掩码sched_setaffinity()但不改变其调度策略。如果app内部创建了线程且未显式调用pthread_setaffinity_np()子线程会继承父进程的掩码但Linux调度器可能因负载判断将其调度到其他核。更常见的是app调用了fork()子进程的亲和性掩码被重置为全核。终极解决法# 用cgroup v2彻底锁定 mkdir /sys/fs/cgroup/app_group echo $$ /sys/fs/cgroup/app_group/cgroup.procs echo 0-1 /sys/fs/cgroup/app_group/cpuset.cpus echo 0 /sys/fs/cgroup/app_group/cpuset.mems # 然后在该cgroup下启动app cd /sys/fs/cgroup/app_group exec ./appcpuset比taskset更底层它直接修改struct task_struct-cpus_allowed且对fork出的子进程自动继承。4.4 问题现象perf sched latency显示最大延迟15ms但cyclictest测出来才80μs矛盾点破perf sched latency统计的是调度延迟从wake_up_process()到进程真正获得CPU的时间而cyclictest测的是定时器唤醒延迟从timer_fire到clock_nanosleep返回的时间。前者包含内核抢占、中断屏蔽、RCU同步等开销后者只反映定时器子系统的精度。如何交叉验证用perf record -e sched:sched_wakeup -e sched:sched_switch抓取唤醒-切换事件对。计算每个wakeup到对应switch的时间差过滤掉switch到wakeup的间隔即进程已运行。对比perf结果与cyclictest的-h直方图如果perf的99th是15ms而cyclictest是80μs说明问题在唤醒路径可能是wake_up_process()被高优先级中断阻塞或try_to_wake_up()在rq_lock上自旋太久。针对性修复降低高频率中断的优先级如USB PHY IRQ的irq_set_irq_type()设为IRQ_TYPE_LEVEL_LOW在try_to_wake_up()关键路径加preempt_disable()保护需内核补丁常见问题速查表现象最可能根因快速验证命令修复方案CPU利用率不均但/proc/interrupts均匀ksoftirqd集中处理cat /proc/softirqs调netdev_budget开RPSnohz_full核上date快tick_do_timer_cpu未设cat /proc/timer_list | grep tick_do_timer强制tick_do_timer_cpu0taskset后进程仍跑单核子线程/子进程未继承亲和性ps -o pid,comm,psr -T -p $(pgrep app)改用cpusetcgroupperf延迟远大于cyclictest唤醒路径被阻塞perf record -e sched:sched_wakeup -e irq:irq_handler_entry降中断优先级加preempt_disable5. 工具与脚本附录直接抄作业的实战资源包5.1check_arm_topology.sh——三分钟看清你的SoC真实拓扑#!/bin/sh # 用法./check_arm_topology.sh echo ARM SoC 物理拓扑诊断报告 echo echo 1. /proc/cpuinfo 摘要 grep -E processor|model name|cpu cores|siblings|core id|physical id /proc/cpuinfo | head -20 echo echo 2. CPU拓扑关系 for cpu in /sys/devices/system/cpu/cpu[0-9]*; do if [ -d $cpu ]; then cpu_num$(basename $cpu | sed s/cpu//) echo CPU$cpu_num: echo core_id: $(cat $cpu/topology/core_id 2/dev/null) echo physical_package_id: $(cat $cpu/topology/physical_package_id 2/dev/null) echo thread_siblings_list: $(cat $cpu/topology/thread_siblings_list 2/dev/null) echo core_siblings_list: $(cat $cpu/topology/core_siblings_list 2/dev/null) fi done | head -30 echo echo 3. 设备树CPU节点关键 if [ -f /sys/firmware/devicetree/base/cpus/ ]; then echo DTB CPUs detected: find /sys/firmware/devicetree/base/cpus/ -name reg -exec sh -c echo {} ; hexdump -n4 -e 1/4 \0x%08x\\n\ {} \; 2/dev/null | head -10 else echo No DTB info found. fi echo echo 4. 当前中断分布 echo IRQ\tCPU0\tCPU1\tCPU2\tCPU3 grep -E ^[0-9]: /proc/interrupts | head -10 | awk {print $1 \t $2 \t $3 \t $4 \t $5} echo echo 诊断完成请对照本文2.1节分析错位 5.2plot_latency.py——把cyclictest -h数据变成专业直方图#!/usr/bin/env python3 import sys import matplotlib.pyplot as plt import numpy as np if len(sys.argv) ! 2: print(Usage: python3 plot_latency.py cyclictest_h_output) sys.exit(1) data [] with open(sys.argv[1], r) as f: for line in f: if line.strip() and not line.startswith(#): parts line.split() if len(parts) 2: try: bucket int(parts[0]) count int(parts[1]) data.extend([bucket] * count) except ValueError: continue if not data: print(No valid data found.) sys.exit(1) plt.figure(figsize(10, 6)) plt.hist(data, bins50, alpha0.7, colorsteelblue, edgecolorblack) plt.xlabel(Latency (μs)) plt.ylabel(Count) plt.title(Cyclictest Latency Distribution) plt.grid(True, alpha0.3) # 计算关键指标 p99 np.percentile(data, 99) std_dev np.std(data) max_val max(data) plt.axvline(p99, colorred, linestyle--, labelf99th Percentile: {p99:.1f}μs) plt.axvline(std_dev, colororange, linestyle