
记一次真实的负载虚高修复:为 hinas(Hi3798MV100)重编译 4.4.35 内核,让 load average 不再被 D 线程绑架关键词:hinas · Hi3798MV100/200/300 · HiSTBLinuxV100R005C00 · 内核 4.4.35 · load average · D 状态线程一、现象:一台永远满负载的 NAS设备基于 hinas 项目(机顶盒改造的 NAS,底层是华为开源的HiSTBLinuxV100R005C00SDK,对应 Hi3798MV100/200/300 系列 SoC),内核版本4.4.35_ecoo_81082668。拿到手第一眼就不对劲:$ uptime 12:16:16 up 6 days, 23:32, 0 users, load average: 6.06, 6.05, 6.01负载常年卡在 6.0 附近,一分钟、五分钟、十五分钟三档全部贴满,但 CPU 明明是空闲的——top里没有哪个进程吃 CPU,系统响应也正常。监控系统却因为 load average 告警刷屏,完全没法用。负载 6.0 意味着平均有 6 个任务永远在排队,可这台 NAS 明明闲得很。这不符合直觉,必须查。二、定位:6 个常年 D 状态的内核线程先看谁在跑:$ ps -eo state,pid,ppid,comm,wchan:40,args | awk NR1 || $1 ~ /D/ S PID PPID COMMAND WCHAN COMMAND D 756 2 log_udisk_task msleep [log_udisk_task] D 1175 2 HI_HDMI_kThread msleep [HI_HDMI_kThread] D 1176 2 HI_HDMI_kCEC msleep [HI_HDMI_kCEC] D 1234 2 HI_VPSS_Process VPSS_OSAL_WaitEvent [HI_VPSS_Process] D 1253 2 cpu_avs msleep [cpu_avs] D 1256 2 temperature_con msleep [temperature_con]正好 6 个 D 状态线程,一个不多一个不少,和负载数字 6.0 完全吻合。它们是海思 SDK 的常驻内核线程:PID线程wchan职责756log_udisk_taskmsleep日志 U 盘轮询1175HI_HDMI_kThreadmsleepHDMI 内核线程1176HI_HDMI_kCECmsleepHDMI CEC 控制1234HI_VPSS_ProcessVPSS_OSAL_WaitEvent视频处理子系统,等待事件1253cpu_avsmsleepCPU 自适应电压调节1256temperature_conmsleep温度监控注意它们的 wchan:msleep、WaitEvent——它们根本没有在干活,是在睡觉,靠超时醒来检查一下再睡回去。这是海思 SDK 的轮询设计,不是卡死,也不是故障。那问题出在哪?出在**睡觉的方式**上。三、根因:内核把 D 状态线程算进了 load averagemsleep()底层是TASK_UNINTERRUPTIBLE(不可中断睡眠)睡眠,也就是D 状态。而 Linux 从 2.6 起,load average 的统计口径是:load average ≈ nr_running(可运行) nr_uninterruptible(不可中断睡眠)看内核源码kernel/sched/loadavg.c,注释写得很直白:/* * The global load average is an exponentially decaying average of * nr_running nr_uninterruptible. */核心统计函数:longcalc_load_fold_active(structrq*this_rq){longnr_active,delta0;nr_activethis_rq-nr_running;nr_active(long)this_rq-nr_uninterruptible;// ← D 线程在这里被数进来if(nr_active!this_rq-calc_load_active){deltanr_active-this_rq-calc_load_active;this_rq-calc_load_activenr_active;}returndelta;}nr_uninterruptible的语义是正在等待 IO 完成的线程,设计初衷是让负载反映 IO 饥饿。但海思 SDK 这些轮询线程根本不是等 IO,只是用不可中断睡眠当延时器,于是它们被永远算进了活跃数。SDK 里这种写法一多,负载就成了摆设。结论:这是 hinas 沿用的海思 SDK 内核的一个统计口径缺陷——D 状态线程污染 load average,导致 NAS 监控失效。四、方案对比:两条路问题定性清楚后,摆在我们面前有两条路:一条在用户空间替换,一条在内核里修改。都实际验证过,结论是内核补丁更彻底,但用户空间方案作为快速止血同样值得一提。方案 A:用户空间替换——伪造 /proc/loadavg(不动内核,5 分钟)思路:内核不改一个字,在用户空间做一个干净的 loadavg文件,然后用mount --bind把它盖到/proc/loadavg上,所有读负载的程序(uptime、监控、脚本)看到的都是修正后的数字。守护脚本每 5 秒做一次:# 读 /proc/stat 的 procs_running:系统当前真正可运行的线程数,天然不含 D 线程running$(awk/procs_running/{print $2}/proc/stat)# 照抄内核指数衰减公式(EXP_1/EXP_5/EXP_15 1884/2014/2037,FIXED_1 2048)load1$(((load1*1884running*164)11))load5$(((load5*2014running*34)11))load15$(((load15*2037running*11)11))把结果写成文件,然后:mount--bind/tmp/fake_loadavg /proc/loadavg优点:不编译、不动内核、即时生效、umount /proc/loadavg一秒还原。缺点:治标不治本——骗的是读负载的人,不是修统计口径;重启后要靠 rc 脚本重新挂载;系统里多一个常驻守护进程要维护;任何绕过/proc/loadavg直接读内核avenrun的途径都盖不住。方案 B:内核补丁——改 loadavg.c 统计口径(最终采用)既然是内核口径问题,就在内核里修:load average 只统计可运行任务(running),不再把 D 状态线程算进去。既保留负载反映 CPU 争抢的本意,又不会被 SDK 的轮询线程绑架。对比小结维度A 用户空间替换B 内核补丁(采用)改动范围守护脚本 bind mountkernel/sched/loadavg.c一行是否需要编译否是(约 40~60 分钟)是否需要重启否,即时生效是可逆性umount即还原保留旧内核镜像即可回滚治本程度治标(覆盖展示层)治本(修正统计口径)维护成本常驻进程 重启重挂无,一次编译长期有效风险极低低(配置正确 留回滚即可)最终选择方案 B:对 hinas 这种要 7×24 稳定跑的 NAS,修统计口径才是一劳永逸,而且补丁极小、风险可控。方案 A 留作应急止血手段,文章后半部分展开方案 B 的完整实施。五、最终采用:内核补丁,只统计真正在跑的任务补丁很小,改动kernel/sched/loadavg.c一处:--- a/kernel/sched/loadavg.c b/kernel/sched/loadavg.c -113,7 113,7 long calc_load_fold_active(struct rq *this_rq) long nr_active, delta 0; nr_active this_rq-nr_running; - nr_active (long)this_rq-nr_uninterruptible; /* PATCH: drop D-state (uninterruptible) tasks from loadavg */ if (nr_active ! this_rq-calc_load_active) {calc_load_fold_active()是所有 CPU 每 5 秒(LOAD_FREQ)向全局计数calc_load_tasks折入活跃数的唯一入口,calc_global_load()再拿它做 1/5/15 分钟的指数衰减(系数EXP_1/EXP_5/EXP_15)。所以只改这一处,三档负载同时修正,而且不影响调度器任何逻辑——CFS 公平调度走的是 PELT/runnable统计,不依赖loadavg。六、重编译部署流程(基于 HiSTBLinuxV100R005C00 SDK)hinas 的 SDK 本体是闭源发布的,但它的基座就是华为开源的 HiSTBLinux SDK 仓库(HiSTBLinuxV100R005C00SPC060,4.4.35 完整内核),结构和开源版一致:内核源码在source/kernel/,配置用 SDK 自带的hi3798mv100_defconfig。整个流程如下:准备工具链:SDK 自带tools/交叉编译工具链(armv7,glibc),解压即用,无需额外安装。解包 SDK 源码,进入source/kernel/,确认版本:$ head -3 Makefile VERSION 4 PATCHLEVEL 4 SUBLEVEL 35应用补丁:把上面的 diff 打进kernel/sched/loadavg.c。配置:用 SDK 的hi3798mv100_defconfig作为基准配置(沿用厂商全部驱动与海思模块,CONFIG_IKCONFIG等保持原样,不引入任何新功能,只带一行行为变更)。编译:$ make ARCHarm hi3798mv100_defconfig $ make ARCHarm CROSS_COMPILEarm-hisiv500-linux- -j4 zImage modules4 核机器全量编译约 40~60 分钟。老内核编译很快,瓶颈只在磁盘 IO。打包替换:生成新的zImage与内核模块,更新 rootfs 对应目录,旧内核镜像与旧模块目录完整保留(见下节回滚),改好启动参数后重启。验证:见下一节。七、验证:负载回归正常重启后:$ uptime 09:41:02 up 0 min, 1 user, load average: 0.06, 0.11, 0.08负载从 6.0x 掉到0.1x 量级,与系统真实活动(/proc/stat的procs_running只有个位数)吻合。再观察 15 分钟窗口:指标修复前修复后loadavg(1/5/15)6.06 / 6.05 / 6.010.06 / 0.11 / 0.08D 状态线程6 个(照常存在)6 个(照常存在,不再计入负载)CPU 利用率个位数 %个位数 %(无变化)监控告警loadavg 误报刷屏全部消失注意 D 线程依然存在、依然在轮询——我们修的是统计口径,不是干掉它们,行为零改变,只是它们不再污染负载数字。运行一周,监控稳定,无异常。八、回滚与风险控制回滚:启动时保留旧内核镜像(zImage.bak)与旧模块目录,出问题改回启动参数即可,30 秒内回到原状。风险:补丁只改一处加法运算,不涉及调度、内存、驱动;最坏情况是负载数值口径变化,不影响任何内核功能。已知代价:负载将不再反映 IO 饥饿(纯 D 状态等 IO 的场景),对 NAS 这种本机盘阵场景影响可忽略,监控改为叠加磁盘 IO 指标兜底。九、结语这轮排查的本质:海思 SDK 内核把轮询延时用不可中断睡眠实现,导致loadavg口径被 D 线程污染,连累 hinas 这类机顶盒 NAS 的监控体系。修复只花了一行补丁 一次重编译——定位问题比解决问题难得多。对 hinas 社区:希望这个修复能合并进后续版本。设备底子是华为开源的 HiSTBLinuxV100R005C00,任何基于它的盒子(NAS、电视盒、软路由)只要负载常年虚高,都可以照这个思路处理:ps -eo state,comm,wchan找 D 线程,数一下个数,和 loadavg 对一下;对上了就是口径问题,不是性能问题;一行补丁,重编译,完事。相关开源参考:海思 HiSTBLinux SDK:github.com/JasonFreeLab/HiSTBLinuxV100R005C00SPC060(Hi3798MV100/200/300)4.4.35 完整内核(支持 Docker):github.com/07bug/HiSTBLinuxV100R005C00SPC060
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。