简介

在 Linux 内核 CFS 公平调度体系中,CONFIG_FAIR_GROUP_SCHED 组调度依托 cgroup 实现 CPU 资源按业务分组隔离,是容器、云服务器、嵌入式多业务隔离场景的底层核心支撑。传统单进程维度统计nr_running仅能统计单 CPU 就绪任务,无法区分分组内空闲调度实体,内核在struct cfs_rq中引入idle_h_nr_running字段,专门用于层级任务组内 SCHED_IDLE 类型空闲任务的向上汇总统计,是组负载核算、跨 CPU 负载均衡、cgroup 带宽限流三大机制的关键统计依据。

在云计算 K8s 容器集群、工业嵌入式多租户工控系统、服务器虚拟化场景中,业务常通过 cgroup 划分为前台业务组、后台批处理空闲任务组,后台任务统一配置 SCHED_IDLE 最低优先级,仅在 CPU 资源富余时调度运行。idle_h_nr_running精准统计每组空闲任务总量,帮助调度器区分 “组内全是空闲任务” 和 “组内存在活跃业务”,避免负载均衡时错误迁移低优先级空闲任务挤占核心业务算力;同时辅助 PELT 负载计算剔除无效空闲负载,保证 CPU 带宽配额精准管控。

对于内核驱动开发、容器底层研发、嵌入式实时 Linux 调优工程师,吃透idle_h_nr_running的计数更新、层级上浮、负载匹配全链路逻辑,是排查容器 CPU 限流异常、多核负载失衡、后台空闲任务异常抢占前台业务等疑难问题的必备基础,相关源码逻辑也是调度相关毕业论文、内核定制化改造的高频参考内容。本文从基础概念、环境搭建、源码拆解、实操验证、故障排查全流程落地,配套可复现代码与内核调试命令。

一、核心概念与术语解析

1.1 CFS 组调度基础架构

组调度基于task_group结构体绑定 cgroup cpu 子系统,树形层级组织任务组:根任务组(init_task_group)为顶层节点,用户通过 mkdir /sys/fs/cgroup/cpu/xxx 创建子任务组,任务通过 echo pid > xxx/cgroup.procs 划入分组。每个 CPU 上每个任务组对应独立cfs_rq运行队列,形成父 cfs_rq 包含多个子 cfs_rq的树形嵌套结构,idle_h_nr_running就是在该树形结构中逐层汇总空闲任务数量。

1.2 SCHED_IDLE 调度任务定义

SCHED_IDLE是 CFS 最低优先级调度策略,任务仅在 CPU 无普通 CFS、RT、DEADLINE 任务时获取 CPU 时间,多用于日志归档、数据备份等后台低优先级任务。内核中通过se_is_idle(struct sched_entity *se)判断单个调度实体是否为 IDLE 类型任务,也是idle_h_nr_running的统计对象。

1.3 cfs_rq 关键统计字段释义

截取kernel/sched/sched.h中 CFS 运行队列结构体核心成员(开启 CONFIG_FAIR_GROUP_SCHED 才生效):

struct cfs_rq {
    /* 当前队列普通就绪任务总数(非IDLE) */
    unsigned int nr_running;
    /* 本组+所有下级子组汇总的SCHED_IDLE任务数量,即idle_h_nr_running */
    unsigned int idle_h_nr_running;
    /* 本级cfs_rq自身持有的IDLE任务计数,仅统计当前队列直接挂载任务 */
    unsigned int idle;
    /* 父调度实体,用于向上逐层更新idle_h_nr_running */
    struct sched_entity *tg_se;
    /* 红黑树根,存放CFS就绪调度实体 */
    struct rb_root tasks_timeline;
};

字段区分关键点:

  1. idle本层 cfs_rq 自有 IDLE 任务数,仅当前队列新增 / 删除 IDLE 任务时变更;
  2. idle_h_nr_running层级汇总值 = 本层 idle + 所有子 cfs_rq 的 idle_h_nr_running,自底向上链式更新,h 代表 hierarchy 层级汇总;
  3. nr_running:常规就绪任务,不计入 idle_h_nr_running 统计。

1.4 idle_h_nr_running 三大核心用途

  1. 负载均衡决策:负载均衡时通过idle_h_nr_running>0判定目标分组存在空闲后台任务,优先迁移空闲任务,不动前台高优先级业务;
  2. PELT 负载折算:统计分组有效负载时剔除 SCHED_IDLE 任务权重,空闲任务不计入组有效负载;
  3. cgroup 带宽节流:组 CPU 配额耗尽时,仅阻塞普通任务,IDLE 任务依靠 idle_h_nr_running 判定可继续闲置等待资源。

二、环境准备

2.1 软硬件环境清单

分类 参数配置
操作系统 Ubuntu22.04 LTS x86_64
内核源码 Linux 5.15.90 LTS(长期稳定版,idle_h_nr_running 逻辑无大幅改动)
硬件 x86_64 4 核 CPU、8G 内存
编译依赖 gcc11、make、libncurses-dev、bison、flex、libelf-dev
调试工具 trace-cmd、ftrace、perf、gdb、cgroup-tools

2.2 内核编译配置(必须开启组调度开关)

1. 环境依赖安装
# 一键安装内核编译+调试全套依赖
sudo apt update
sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev cgroup-tools trace-cmd
2. 下载解压内核源码
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.90.tar.xz
tar -xf linux-5.15.90.tar.xz
cd linux-5.15.90
3. 内核 config 关键配置
cp /boot/config-$(uname -r) .config
make menuconfig

必选内核开关(不开启则无组调度与 idle_h_nr_running):

CONFIG_FAIR_GROUP_SCHED=y      // 启用CFS组调度(核心)
CONFIG_CGROUP_CPU=y            // cgroup cpu控制器
CONFIG_DEBUG_KERNEL=y
CONFIG_SCHED_DEBUG=y           // 调度调试
CONFIG_FTRACE=y                // 函数跟踪,观测idle_h_nr_running变更函数
4. 编译安装内核
make -j$(nproc)
sudo make modules_install
sudo make install
sudo update-grub

重启系统,在 grub 菜单选择新编译内核启动。

2.3 源码路径定位

kernel/sched/fair.c     // idle_h_nr_running增减、层级上浮全部实现代码
kernel/sched/sched.h    // cfs_rq结构体定义、idle/idle_h_nr_running字段声明

三、应用场景(302 字)

在边缘工控机多业务 cgroup 隔离场景中,设备划分为控制业务组(伺服闭环、故障检测,SCHED_OTHER 普通优先级)与日志归档组(磁盘落盘、日志压缩,SCHED_IDLE 空闲优先级)。调度器依靠 idle_h_nr_running 统计归档组空闲任务总量,多核负载均衡时,仅在 CPU 资源富余时迁移归档任务,避免抢占实时控制业务算力。云主机租户隔离场景下,用户业务容器、后台定时任务分属不同 cgroup 分组,idle_h_nr_running 帮助内核区分分组负载构成,PELT 负载计算剔除后台空闲任务负载,精准管控租户 CPU 带宽上限;当租户配额用尽时,普通业务被限流暂停,空闲任务依托统计标识持续待机,兼顾资源利用率与业务稳定性。

四、实际案例与步骤(源码 + 实操代码分步落地)

4.1 源码一:idle 任务入队时 idle 与 idle_h_nr_running 自增逻辑

任务被设置 SCHED_IDLE 策略、加入 cfs_rq 就绪队列时,触发enqueue_task_fair中 IDLE 计数增加,底层调用update_idle_cfs_rq更新本级 idle,再链式向上刷新所有父 cfs_rq 的 idle_h_nr_running。

// kernel/sched/fair.c 源码节选,附带逐行注释
static void update_idle_cfs_rq(struct cfs_rq *cfs_rq, int delta)
{
    struct sched_entity *se;
    struct cfs_rq *parent_cfs_rq;

    // delta=+1:新增IDLE任务;delta=-1:移除IDLE任务
    cfs_rq->idle += delta;

    // 从当前cfs_rq向上遍历所有父级任务组,逐层修改idle_h_nr_running
    for (se = cfs_rq->tg_se; se; se = se->parent) {
        parent_cfs_rq = se->my_q;
        if (!parent_cfs_rq)
            break;
        // 上层汇总统计值同步增减,实现层级汇总
        parent_cfs_rq->idle_h_nr_running += delta;
    }
}

static void enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
    struct sched_entity *se = &p->se;
    struct cfs_rq *cfs_rq = task_cfs_rq(p);

    /* 常规入队红黑树逻辑省略 */
    if (se_is_idle(se)) {
        // 当前任务是SCHED_IDLE,计数+1
        update_idle_cfs_rq(cfs_rq, 1);
        cfs_rq->idle_h_nr_running += 1;
    }
    cfs_rq->nr_running++;
}

代码说明update_idle_cfs_rq是 idle_h_nr_running 向上汇总的核心函数,采用 for 循环顺着调度实体父指针逐级上浮,保证从叶子分组到根分组所有层级统计值同步变更,是实现层级汇总的关键。

4.2 源码二:idle 任务出队时统计值递减逻辑

SCHED_IDLE 任务阻塞、休眠、退出时,在dequeue_task_fair中调用同函数传入 delta=-1,反向扣减计数:

static void dequeue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
    struct sched_entity *se = &p->se;
    struct cfs_rq *cfs_rq = task_cfs_rq(p);

    /* 从红黑树移除调度实体逻辑省略 */
    if (se_is_idle(se)) {
        // IDLE任务出队,本级idle与全层级idle_h_nr_running减1
        update_idle_cfs_rq(cfs_rq, -1);
        cfs_rq->idle_h_nr_running -= 1;
    }
    cfs_rq->nr_running--;
}

逻辑要点:无论任务在树形分组的哪一层级,单个 IDLE 任务删除仅触发一条向上链路修改,时间复杂度 O (分组层级深度),远低于全组遍历统计。

4.3 源码三:负载均衡中 idle_h_nr_running 实际调用场景

负载均衡load_balance通过该字段判断分组是否存在空闲任务:

// fair.c load_balance片段
if (busiest_cfs_rq->idle_h_nr_running > 0) {
    // 源分组有空闲IDLE任务,优先迁移空闲任务,不挪普通业务进程
    env->flags |= LB_MIGRATE_IDLE_ONLY;
}

作用说明:负载均衡优先搬运 SCHED_IDLE 后台任务,最大程度保护普通业务 CPU 时间确定性。

4.4 用户态实操:cgroup 创建 + SCHED_IDLE 测试程序(可直接复制编译运行)

步骤 1:挂载 cgroup cpu 控制器
# 创建cgroup挂载目录并挂载cpu子系统
sudo mkdir -p /sys/fs/cgroup/cpu
sudo mount -t cgroup -o cpu none /sys/fs/cgroup/cpu

# 新建两个分组:biz_group(前台普通任务)、backup_group(后台IDLE归档任务)
sudo mkdir /sys/fs/cgroup/cpu/biz_group
sudo mkdir /sys/fs/cgroup/cpu/backup_group
步骤 2:编写测试程序,分别创建普通任务与 IDLE 任务

文件名sched_idle_test.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/syscall.h>
#include <linux/sched.h>

// 设置进程调度策略为SCHED_IDLE
int set_sched_idle(void)
{
    struct sched_attr attr;
    attr.size = sizeof(attr);
    attr.sched_policy = SCHED_IDLE;
    attr.sched_flags = 0;
    return syscall(SYS_sched_setattr, getpid(), &attr, 0);
}

// 死循环模拟CPU密集型任务
void cpu_busy_loop(void)
{
    unsigned long cnt = 0;
    while(1){
        cnt++;
        if(cnt % 10000000 == 0)
            usleep(1000);
    }
}

int main(int argc,char *argv[])
{
    if(argc == 2 && argv[1][0] == 'i'){
        printf("创建SCHED_IDLE后台任务,pid=%d\n",getpid());
        set_sched_idle();
    }else{
        printf("创建SCHED_OTHER前台普通任务,pid=%d\n",getpid());
    }
    cpu_busy_loop();
    return 0;
}

编译命令:

gcc sched_idle_test.c -o idle_test
步骤 3:启动进程并划入对应 cgroup 分组
# 1. 启动2个普通前台进程,加入biz_group
sudo ./idle_test &
echo $! | sudo tee /sys/fs/cgroup/cpu/biz_group/cgroup.procs
sudo ./idle_test &
echo $! | sudo tee /sys/fs/cgroup/cpu/biz_group/cgroup.procs

# 2. 启动3个IDLE后台进程,划入backup_group
sudo ./idle_test i &
echo $! | sudo tee /sys/fs/cgroup/cpu/backup_group/cgroup.procs
sudo ./idle_test i &
echo $! | sudo tee /sys/fs/cgroup/cpu/backup_group/cgroup.procs
sudo ./idle_test i &
echo $! | sudo tee /sys/fs/cgroup/cpu/backup_group/cgroup.procs
步骤 4:ftrace 跟踪 idle_h_nr_running 变更函数,验证计数逻辑
# 开启debugfs跟踪
sudo mount -t debugfs none /sys/kernel/debug
echo > /sys/kernel/debug/tracing/trace
# 筛选跟踪idle计数更新核心函数
echo update_idle_cfs_rq >> /sys/kernel/debug/tracing/set_ftrace_filter
echo enqueue_task_fair >> /sys/kernel/debug/tracing/set_ftrace_filter
echo dequeue_task_fair >> /sys/kernel/debug/tracing/set_ftrace_filter
# 开启函数跟踪
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on

新开终端 kill 掉一个 IDLE 进程,查看跟踪日志:

sudo kill -9 $(pgrep -f "./idle_test i" | head -n1)
cat /sys/kernel/debug/tracing/trace

日志验证现象:日志出现update_idle_cfs_rq delta=-1调用记录,对应 backup_group 本级 idle-1、父分组 idle_h_nr_running 同步递减,和源码逻辑完全吻合。

步骤 5:查看内核调度参数,观测负载均衡行为
# 查看系统调度域配置
cat /proc/sys/kernel/sched_domain/cpu0/domain0/flags
# 查看当前各进程调度策略
ps -eo pid,policy,cmd | grep idle_test

五、常见问题与解答

Q1:开启 CONFIG_FAIR_GROUP_SCHED 后,idle_h_nr_running 数值和实际 IDLE 进程数不一致?

:两个高频诱因:①进程跨 CPU 迁移,任务从 CPU0 的 cfs_rq 迁移至 CPU1,原 CPU 执行 dequeue 扣减计数,目标 CPU 执行 enqueue 增加计数,瞬间读取会短暂不一致;②子分组嵌套,子分组 IDLE 任务会汇总至父分组 idle_h_nr_running,父分组统计值 = 所有子分组 + 自身 IDLE 总和,不能和单一分组进程数对标。排查:用 ftrace 跟踪update_idle_cfs_rq核对增减时机。

Q2:SCHED_IDLE 任务大量存在,但 idle_h_nr_running 始终为 0?

:①进程未划入 cgroup 分组,运行在 root 根分组默认统计正常,若手动关闭组调度 CONFIG_FAIR_GROUP_SCHED=n,字段失效;②任务调度策略配置错误,实际是 SCHED_OTHER 而非 SCHED_IDLE,se_is_idle()判定失败不会计入统计,用 ps 查看 policy 字段校验。

Q3:负载均衡不会自动迁移 backup_group 的 IDLE 任务?

:idle_h_nr_running>0 仅代表具备空闲迁移条件,还受sched_idle_balance_cost参数管控,参数为 0 关闭空闲任务均衡,修改命令:echo 200 > /proc/sys/kernel/sched_idle_balance_cost放开空闲任务迁移阈值。

Q4:销毁 cgroup 分组后,idle_h_nr_running 残留脏数据?

:分组销毁前必须清空 cgroup.procs 内所有进程,进程退出触发 dequeue 自动扣减全层级统计,若直接 rmdir 分组导致进程残留,内核 cfs_rq 销毁逻辑缺失计数回收,造成统计值异常,规范操作先 kill 分组内所有进程再删除目录。

六、实践建议与最佳实践

6.1 内核调试排错规范

  1. 空闲任务统计异常排查顺序:ps 确认进程 SCHED_IDLE 策略→ftrace 跟踪 update_idle_cfs_rq 增减→核对 cgroup 归属→gdb 断点查看 cfs_rq->idle 与 idle_h_nr_running 内存值;
  2. 定制内核调试:新增 proc 节点输出指定 cgroup 的 idle/idle_h_nr_running 数值,快速定位层级汇总异常。

6.2 业务 cgroup 配置优化

  1. 后台日志、备份任务统一配置 SCHED_IDLE 放入独立 cgroup,依靠 idle_h_nr_running 天然隔离,避免闲时任务抢占前台业务;
  2. 不频繁动态修改进程调度策略,频繁切换 SCHED_IDLE<->SCHED_OTHER 会反复触发 idle_h_nr_running 全链路更新,带来不必要调度开销。

6.3 内核定制开发注意事项

  1. 自研 CFS 调度变种时,修改 IDLE 任务统计逻辑必须同步维护 idle_h_nr_running 向上汇总链路,破坏 update_idle_cfs_rq 上浮逻辑会直接导致负载均衡、带宽控制失效;
  2. 禁止直接在模块中篡改 cfs_rq->idle_h_nr_running 内存值,所有计数变更统一通过 enqueue/dequeue 正规路径修改,防止统计和实际任务脱节。

6.4 性能优化技巧

大批量后台 IDLE 任务场景,绑定任务至固定 CPU 核心,减少跨 CPU 迁移带来的 idle_h_nr_running 反复增减开销,降低调度软中断耗时。

七、总结与应用延伸

全文从结构体定义、计数更新源码、用户态 cgroup 实操、故障排查完整梳理了idle_h_nr_running空闲层级统计的全链路原理,该字段本质是 CFS 组调度分层负载轻量化统计方案:通过本级 idle 记录直属空闲任务,依靠 for 循环链式上浮汇总得到 idle_h_nr_running,以极低开销实现树形分组空闲任务全局统计,规避遍历整棵任务组树的 O (N) 开销,是 cgroup 资源隔离、多核负载均衡、PELT 负载计算三大机制的底层数据底座。

工程落地层面,云原生容器集群、工控多租户系统、虚拟化宿主机均依托该统计字段实现前后台业务算力隔离,保障核心业务运行确定性;学术研究层面,idle_h_nr_running 的层级统计设计思路是调度算法论文中分层统计优化的经典案例,可作为调度改进方案的参考原型。

建议读者基于本文测试代码自行增减 IDLE 进程、嵌套多层子 cgroup 分组,配合 ftrace 跟踪字段变化,修改update_idle_cfs_rq源码屏蔽向上汇总逻辑,对比修改前后负载均衡行为差异,直观理解该字段对系统调度的实际影响,将理论落地到项目内核调优与定制开发。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐