Linux 组调度的 h_nr_running:组内任务数的层次化统计
一、简介1.1 技术背景与项目意义在未开启CONFIG_FAIR_GROUP_SCHED组调度配置时Linux CFS 调度仅以单进程为调度单元cfs_rq-nr_running只统计本级就绪队列内的可运行任务时间片基于单个队列任务数量分配。随着容器、云原生、嵌入式实时系统普及基于 cgroup 的层级资源隔离成为刚需Docker/K8s 容器、systemd 服务分组、Android 前台 / 后台应用分组、工业实时工控进程分组全部依赖 CFS 组调度实现 CPU 算力配额隔离Linux Kernel。组调度引入分层调度实体单个进程为叶子调度实体cgroup 任务组为父调度实体形成树状层级结构。此时原有nr_running只能统计单层队列任务无法统计整个 cgroup 子树内全量就绪任务内核新增h_nr_runninghierarchy nr_running层次化运行任务计数字段专门递归汇总当前 cfs_rq 对应任务组下所有子孙层级 cgroup 内全部就绪任务总数是组调度时间片计算、带宽限流、负载均衡三大核心逻辑的底层数据源。1.2 落地应用场景与开发者价值生产侧落地场景覆盖三大领域云原生容器K8s 通过 cpu.shares 配置 Pod 权重内核依靠 h_nr_running 计算分组时间片实现多容器 CPU 公平抢占工业嵌入式 Linux工控系统拆分采集任务组、控制任务组、日志任务组依托 h_nr_running 做分组算力限流避免日志进程抢占实时控制进程 CPU服务器运维systemd 对系统服务分组system.slice/user.slice利用 h_nr_running 监控分组负载防止恶意进程耗尽整机算力。对内核开发、驱动工程师、性能优化工程师而言吃透 h_nr_running 统计逻辑才能精准排查容器 CPU 配额不生效、分组抢占异常、负载均衡失衡等疑难线上故障同时是撰写 Linux 调度相关毕业论文、内核性能课题报告的必备基础知识点也是自主裁剪实时 LinuxPREEMPT_RT调度框架的关键环节。二、核心概念详解2.1 CFS 组调度基础架构task_group任务组结构体一个 cgroup 对应一个 task_group内部为每个 CPU 维护独立cfs_rqCFS 就绪队列形成树形层级父 task_group 挂载若干子 task_group对应 cgroup 目录父子层级关系。cfs_rq-nr_running单层计数仅统计当前本级就绪队列中直接入队的调度实体进程 / 分组 se不递归向下统计子分组任务入队 1、出队 - 1变更仅作用本级 cfs_rq。cfs_rq-h_nr_running分层全量计数hhierarchy 层级统计当前 task_group 对应 cfs_rq 下自身 所有子孙 task_group 全量就绪进程总和子分组任务增减会递归向上逐层修改所有父级 cfs_rq 的 h_nr_running是本文核心研究对象。sched_entity 调度实体分为 task se普通进程、group se任务组代理调度实体group se 作为子分组入口挂载在父分组 cfs_rq 就绪队列实现分层调度串联。cpu.sharescgroup 权重配置文件内核依靠 h_nr_running 结合 shares在sched_slice()函数中计算每个分组单次调度获得的 CPU 时间片长度。2.2 h_nr_running 与 nr_running 关键区别汇总字段统计范围更新规则核心用途nr_running本级 cfs_rq 直接挂载任务不包含子 cgroup仅当前队列 enqueue/dequeue 自增减单层队列调度选核、本级负载统计h_nr_running本级 全层级子孙 cgroup 所有就绪进程子分组任务变动递归向上逐层修改所有父分组 h_nr_running组调度时间片计算、CFS 带宽限流、分组负载均衡2.3 关键内核配置开关# 开启CFS组调度h_nr_running字段才会在内核结构体中生效 CONFIG_FAIR_GROUP_SCHEDy # 依赖cgroup基础框架 CONFIG_CGROUPSy关闭CONFIG_FAIR_GROUP_SCHED时内核编译阶段裁剪 h_nr_running 相关代码字段不存在组调度逻辑失效。三、环境准备3.1 软硬件环境清单环境项版本参数用途宿主机 OSUbuntu 22.04 LTS / CentOS Stream9编译内核、用户态 cgroup 实操内核源码Linux 5.15.80LTS 稳定版企业主流/ Linux6.2源码阅读、内核模块调试、单步跟踪 h_nr_running 变更编译工具链gcc-11、make、libncurses-dev、bison flex内核编译与配置调试工具perf、gdbqemu、trace-cmd、ftrace动态跟踪 h_nr_running 修改函数、抓取内核调用栈硬件x86_64 双核 CPU2C4G虚拟机编译内核避免真机环境故障3.2 环境部署分步操作步骤 1源码下载与依赖安装# Ubuntu依赖安装 apt update apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev git perf trace-cmd -y # 下载5.15 LTS内核源码 git clone https://kernel.googlesource.com/pub/scm/linux/kernel/git/stable/linux.git linux-5.15 cd linux-5.15 git checkout v5.15.80步骤 2内核配置开启组调度# 复制本机原有内核配置 cp -v /boot/config-$(uname -r) .config make menuconfig # 菜单路径开启配置 # General setup → Control Group support → Group CPU scheduler → 勾选 FAIR_GROUP_SCHED # 保存退出后续编译内核步骤 3编译安装自定义内核make -j$(nproc) make modules_install make install update-grub # 重启系统在grub菜单选择新编译内核启动 reboot步骤 4用户态 cgroup v1 挂载实操 h_nr_running 必备# 临时挂载cpu子系统cgroup mkdir -p /sys/fs/cgroup/cpu mount -t tmpfs none /sys/fs/cgroup/cpu mount -o remount,rw -t cgroup -o cpu none /sys/fs/cgroup/cpu四、应用场景302 字在 K8s 容器生产集群中运维通过kubectl resources.cpu100m配置 Pod CPU 配额底层依托 cgroup cpu.shares 完成权重绑定h_nr_running 承担分组任务统计。当一个命名空间下创建 20 个业务 Pod每个 Pod 内部起 5 个工作进程子 cgroup 进程唤醒入队时内核逐层累加各级父分组 h_nr_running内核调度器sched_slice读取顶层分组 h_nr_running 数值结合 shares 权重拆分单个 Pod 可分得的 CPU 时间片保证高任务量分组按权重公平占用 CPU。同时 CFS 带宽控制器依靠 h_nr_running 判断分组瞬时任务量当分组瞬时就绪任务过多触发带宽节流throttle限制分组 CPU 最大使用率避免单个命名空间业务雪崩抢占全节点算力运维通过 ftrace 抓取 h_nr_running 变更事件定位 PodCPU 利用率异常突增根源是容器资源调度与故障排查的底层数据支撑。五、实际案例与步骤源码 用户态实操双案例附可运行代码分两大实战①用户态 cgroup 创建进程观测 h_nr_running 随进程启停变化②内核源码逐层拆解 h_nr_running 增减源码逻辑附带关键内核函数代码、注释与调用链路。案例一用户态实操cgroup 分层创建进程观测 h_nr_running 动态变化前置原理创建三级 cgroup 树形结构父组 root_cg → 一级子组 sub_cg1 → 二级子组 sub_sub_cg在最深层 sub_sub_cg 创建就绪进程观察三级分组对应 cfs_rq 的 h_nr_running 全部同步 1杀死进程后三层 h_nr_running 同步 - 1直观验证 h_nr_running向上递归统计特性。步骤 1批量创建三级层级 cgroup可直接复制脚本运行#!/bin/bash # cgroup_hier_demo.sh CG_ROOT/sys/fs/cgroup/cpu # 构建三级树形分组 mkdir -p ${CG_ROOT}/root_cg/sub_cg1/sub_sub_cg echo 创建三级cgroup完成root_cg→sub_cg1→sub_sub_cg # 配置各级shares不影响h_nr_running计数仅用于时间片权重 echo 1024 ${CG_ROOT}/root_cg/cpu.shares echo 512 ${CG_ROOT}/root_cg/sub_cg1/cpu.shares echo 256 ${CG_ROOT}/root_cg/sub_cg/sub_sub_cg/cpu.shares脚本用途生成嵌套 cgroup 目录对应内核 task_group 树形结构每个目录在内核生成独立 task_group 与 per-cpu cfs_rq。执行chmod x cgroup_hier_demo.sh ./cgroup_hier_demo.sh。步骤 2后台死循环进程永久就绪占用 CPU加入最底层 cgroup// forever_run.c 编译gcc forever_run.c -o forever_run #include unistd.h #include stdio.h int main(void) { // 空循环持续占用CPU进程处于TASK_RUNNING就绪态 while(1); return 0; }编译指令gcc forever_run.c -o forever_run生成可执行文件。步骤 3将进程 PID 写入最底层 cgroup.procs挂载分组# 后台启动进程 ./forever_run PID$! # 进程加入二级子分组sub_sub_cg echo $PID /sys/fs/cgroup/cpu/root_cg/sub_cg1/sub_sub_cg/cgroup.procs echo 进程PID$PID已加入最深层cgroup步骤 4查看各级分组 h_nr_running内核导出节点不同 CPU 分开存储# cpu0的各级分组h_nr_running查看内核将h_nr_running导出至cgroup调试节点 cat /sys/fs/cgroup/cpu/root_cg/sub_cg1/sub_sub_cg/cpu.cfs_rq/cpu0/h_nr_running cat /sys/fs/cgroup/cpu/root_cg/sub_cg1/cpu.cfs_rq/cpu0/h_nr_running cat /sys/fs/cgroup/cpu/root_cg/cpu.cfs_rq/cpu0/h_nr_running现象三层分组 h_nr_running 全部 1执行kill -9 $PID杀死进程三层 h_nr_running 全部变为 0验证任务变更向上递归更新全父分组 h_nr_running。案例二内核源码拆解 h_nr_running 层级更新逻辑Linux5.15 kernel/sched/fair.c原理链路进程入队 (enqueue)→本级 nr_running1→递归向上遍历父 task_group→所有父级 h_nr_running1进程出队 (dequeue) 反向逐层 h_nr_running-1步骤 1底层入口函数 account_entity_enqueue单任务入队触发计数变更// kernel/sched/fair.c 5.15源码添加工程注释 static void account_entity_enqueue(struct cfs_rq *cfs_rq, struct sched_entity *se) { // 1.本级cfs_rq单层nr_running自增仅当前队列1 cfs_rq-nr_running; // 2.关键向上递归更新全层级h_nr_runninghhierarchy update_h_nr_running(cfs_rq, 1); // PELT负载统计非h_nr_running逻辑略 update_load_avg(cfs_rq, se, UPDATE_TG); }函数作用任一调度实体进程 / 分组 se入队就绪队列时调用1代表新增就绪任务传入 update_h_nr_running 实现全层级 h_nr_running 累加。步骤 2核心函数 update_h_nr_runningh_nr_running 递归更新主逻辑// 核心从当前cfs_rq向上回溯所有祖先task_group逐层修改h_nr_running static void update_h_nr_running(struct cfs_rq *cfs_rq, int delta) { struct task_group *tg cfs_rq-tg; struct rq *rq rq_of(cfs_rq); // 循环向上只要存在父task_group就修改父分组对应同CPU的cfs_rq-h_nr_running for (; tg; tg tg-parent) { // 获取父task_group在当前CPU对应的CFS就绪队列 struct cfs_rq *parent_cfs_rq tg_cfs_rq(tg, cpu_of(rq)); // delta1入队加一delta-1出队减一 parent_cfs_rq-h_nr_running delta; } }源码逻辑拆解tg cfs_rq-tg拿到当前队列所属 task_grouptg tg-parent循环迭代父 task_group直到顶层 root cgroupparentNULL 终止循环每个父分组同 CPU 的 cfs_rq-h_nr_running 统一叠加 delta实现从叶子→顶层全层级同步计数进程出队调用account_entity_dequeue(cfs_rq,se)内部调用update_h_nr_running(cfs_rq, -1)逐层所有父 h_nr_running 减 1。步骤 3h_nr_running 落地sched_slice 时间片计算源码组调度核心落地// 依据h_nr_running计算分组单次调度CPU时间片h_nr_running的最终业务落地 static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se) { unsigned int nr_running; u64 slice; // ALT_PERIOD调度特性开启时使用全层级h_nr_running替代单层nr_running if (sched_feat(ALT_PERIOD)) nr_running rq_of(cfs_rq)-cfs.h_nr_running; else nr_running cfs_rq-nr_running; // 基础调度周期 slice __sched_period(nr_running !se-on_rq); // 按分组权重均分时间片cpu.shares在这里生效 slice calc_delta_fair(slice, se-load); return slice; }代码说明组调度模式下内核采用 h_nr_running全分组任务总数参与时间片拆分是 h_nr_running 最核心的使用场景直接决定分组 CPU 配额大小。步骤 3ftrace 动态跟踪 h_nr_running 修改函数可直接执行命令# 挂载ftrace跟踪update_h_nr_running函数调用 echo function /sys/kernel/debug/tracing/current_tracer echo update_h_nr_running /sys/kernel/debug/tracing/set_ftrace_filter # 新开终端运行之前的forever_run进程查看trace日志 cat /sys/kernel/debug/tracing/trace预期输出进程创建入队瞬间捕获update_h_nr_running0xXX调用栈显示逐级遍历各级 task_group直观跟踪 h_nr_running 变更。六、常见问题与解答贴合实操与源码问题Q1创建多层 cgroup子分组进程启动顶层 h_nr_running 数值不变原因 1内核未开启CONFIG_FAIR_GROUP_SCHEDh_nr_running 字段未编译进内核分组调度失效解决方案重新编译内核开启组调度配置重启新内核原因 2进程未写入对应 cgroup.procs进程仍挂载在 root 默认 cgroup解决方案确认 echo $PID xxx/cgroup.procs 执行成功核对 PID 正确性。Q2同一进程跨 CPU 迁移后对应 CPU 的 h_nr_running 没有自动增减原理h_nr_running 是per-CPU 字段每个 CPU 独立一套 cfs_rq 与 h_nr_running进程从 CPU0 迁移至 CPU1 时CPU0 对应各级分组 h_nr_running-1CPU1 各级分组 h_nr_running1排查使用taskset -c 1 ./forever_run绑定 CPU 运行分别查看两个 CPU 的 h_nr_running 节点数值。Q3源码修改 update_h_nr_running 函数后编译内核启动崩溃踩坑点循环遍历 tg-parent 时未做空指针判断顶层 root cgroup 的 parentNULL越界访问修复沿用内核原生 for (;tg;tgtg-parent) 循环写法禁止删除终止条件。Q4cpu.shares 修改后 h_nr_running 数值发生变化结论cpu.shares 只影响时间片权重不改变 h_nr_running 计数h_nr_running 仅跟随就绪任务数量变动shares 与任务计数无耦合出现数值变动大概率是刚好有进程启停触发计数变更。七、实践建议与最佳实践7.1 源码调试优化技巧定点打印调试修改update_h_nr_running添加printk(tg:%p h_nr:%d delta:%d\n,tg,parent_cfs_rq-h_nr_running,delta)编译内核后 dmesg 抓取日志精准定位层级计数变更生产内核禁止频繁 printk改用 ftrace 动态埋点。分 CPU 隔离调试使用 taskset 绑定进程固定单核运行规避多核任务迁移干扰 h_nr_running 观测新手优先单 CPU 验证逻辑。7.2 线上生产调优最佳实践cgroup 层级不要过深cgroup 层级超过 5 层时任务启停需要循环 5 次以上修改 h_nr_running高频进程启停场景会带来微小内核开销容器环境建议层级≤3 层。带宽限流配合 h_nr_running 监控基于 perf 定期采样顶层分组 h_nr_running 数值当瞬时 h_nr_running 突增数十倍判定分组进程异常 fork触发告警限流防止进程风暴。PREEMPT_RT 实时 Linux 裁剪优化自研实时内核时可根据业务需求简化 h_nr_running 递归逻辑固定层级深度减少循环次数降低实时调度延迟。7.3 论文 / 报告调研优化撰写内核相关论文时可基于原生内核修改代码新增 proc 节点导出全层级 h_nr_running 统计数据批量压测进程启停绘制 h_nr_running 变化曲线图作为实验数据。八、总结与落地应用拓展8.1 全文要点回顾字段本质nr_running单层计数、h_nr_running树形全层级递归汇总计数依托 task_group 父子链表实现向上逐层更新更新链路进程唤醒入队→account_entity_enqueue→update_h_nr_running (delta1) 逐层父分组 1进程休眠出队→account_entity_dequeue→update_h_nr_running (delta-1) 逐层父分组 - 1核心价值h_nr_running 是组调度时间片sched_slice、CFS 带宽节流 throttle、分组负载均衡三大模块的基础数据源是 cgroup CPU 资源隔离的底层基石。8.2 多领域落地拓展云原生 K8skubelet 底层依赖 cgrouph_nr_running 实现容器 CPU 配额管控是容器调度的内核底座车载嵌入式 Linux智能座舱拆分多媒体组、仪表实时任务组通过 h_nr_running 实现分组算力隔离保障仪表高优先级任务 CPU 资源国产化实时操作系统国产工业 OS 基于 Linux CFS 二次开发复用 h_nr_running 分层统计逻辑完成多业务分区资源隔离。掌握 h_nr_running 统计逻辑后读者可基于本章节代码继续拓展自研简易 cgroup 资源监控工具、修改内核源码实现自定义分组调度算法将理论落地至项目开发与课题研究中。