☰
hinas负载虚高修复_重编译4.4.35内核屏蔽D线程
2026/10/8 19:03:17 网站建设 项目流程

记一次真实的负载虚高修复:为 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 'NR==1 || $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 average

msleep()底层是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,delta=0;nr_active=this_rq->nr_running;nr_active+=(long)this_rq->nr_uninterruptible;// ← D 线程在这里被数进来if(nr_active!=this_rq->calc_load_active){delta=nr_active-this_rq->calc_load_active;this_rq->calc_load_active=nr_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*1884+running*164)>>11))load5=$(((load5*2014+running*34)>>11))load15=$(((load15*2037+running*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。整个流程如下:

  1. 准备工具链:SDK 自带tools/交叉编译工具链(armv7,glibc),解压即用,无需额外安装。
  2. 解包 SDK 源码,进入source/kernel/,确认版本:
    $ head -3 Makefile VERSION = 4 PATCHLEVEL = 4 SUBLEVEL = 35
  3. 应用补丁:把上面的 diff 打进kernel/sched/loadavg.c。
  4. 配置:用 SDK 的hi3798mv100_defconfig作为基准配置(沿用厂商全部驱动与海思模块,CONFIG_IKCONFIG等保持原样,不引入任何新功能,只带一行行为变更)。
  5. 编译:
    $ make ARCH=arm hi3798mv100_defconfig $ make ARCH=arm CROSS_COMPILE=arm-hisiv500-linux- -j4 zImage modules
    4 核机器全量编译约 40~60 分钟。老内核编译很快,瓶颈只在磁盘 IO。
  6. 打包替换:生成新的zImage与内核模块,更新 rootfs 对应目录,旧内核镜像与旧模块目录完整保留(见下节回滚),改好启动参数后重启。
  7. 验证:见下一节。

七、验证:负载回归正常

重启后:

$ 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.08
D 状态线程6 个(照常存在)6 个(照常存在,不再计入负载)
CPU 利用率个位数 %个位数 %(无变化)
监控告警loadavg 误报刷屏全部消失

注意 D 线程依然存在、依然在轮询——我们修的是统计口径,不是干掉它们,行为零改变,只是它们不再污染负载数字。运行一周,监控稳定,无异常。

八、回滚与风险控制

  • 回滚:启动时保留旧内核镜像(zImage.bak)与旧模块目录,出问题改回启动参数即可,30 秒内回到原状。
  • 风险:补丁只改一处加法运算,不涉及调度、内存、驱动;最坏情况是负载数值口径变化,不影响任何内核功能。
  • 已知代价:负载将不再反映 IO 饥饿(纯 D 状态等 IO 的场景),对 NAS 这种本机盘阵场景影响可忽略,监控改为叠加磁盘 IO 指标兜底。

九、结语

这轮排查的本质:海思 SDK 内核把"轮询延时"用不可中断睡眠实现,导致loadavg口径被 D 线程污染,连累 hinas 这类机顶盒 NAS 的监控体系。修复只花了一行补丁 + 一次重编译——定位问题比解决问题难得多。

对 hinas 社区:希望这个修复能合并进后续版本。设备底子是华为开源的 HiSTBLinuxV100R005C00,任何基于它的盒子(NAS、电视盒、软路由)只要负载常年虚高,都可以照这个思路处理:

  1. ps -eo state,comm,wchan找 D 线程,数一下个数,和 loadavg 对一下;
  2. 对上了就是口径问题,不是性能问题;
  3. 一行补丁,重编译,完事。

相关开源参考:

  • 海思 HiSTBLinux SDK:github.com/JasonFreeLab/HiSTBLinuxV100R005C00SPC060(Hi3798MV100/200/300)
  • 4.4.35 完整内核(支持 Docker):github.com/07bug/HiSTBLinuxV100R005C00SPC060

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询