简介

在 Linux 服务器、嵌入式实时系统、云计算集群、边缘计算节点的日常运维与内核调优工作中,CPU 负载是评判系统运行状态、定位性能瓶颈最核心的指标之一。我们日常通过 topuptimempstat 看到的系统负载值,其底层数据源均来自调度子系统对可运行任务的统计与度量,而 cpu_runnable 正是 Linux 内核调度层专门用于量化单 CPU 核心可运行任务负载强度的核心变量。

不同于单纯统计 CPU 使用率,cpu_runnable 聚焦于处于就绪态、等待被调度执行的任务数量,直接反映 CPU 队列的拥堵程度。当大量任务因 CPU 资源不足排队等待运行时,该数值会持续走高,进而引发调度延迟、业务响应变慢、上下文切换激增等一系列问题。

无论是后端运维工程师排查服务器卡顿、嵌入式工程师优化工控设备调度性能、云原生运维做集群负载调度,还是内核研发人员进行调度子系统二次开发、撰写技术报告与学术论文,都必须吃透 cpu_runnable 的计算逻辑、更新时机、数据流转链路。本文结合内核源码、实操命令、测试案例,从基础概念、环境搭建、源码拆解、实战压测、问题排查到工程最佳实践完整讲解,内容兼顾理论深度与落地性,新手可跟着步骤复现实验,资深开发可作为内核调研、论文撰写的参考资料。

一、核心概念与术语解析

1.1 任务运行状态与可运行队列

Linux 系统中,一个任务(进程 / 线程)存在多种调度状态,和本文强相关的主要分为两类:

  1. TASK_RUNNING(可运行态):任务已经准备就绪,具备 CPU 执行条件,分为两种情况:正在 CPU 上运行、在运行队列中排队等待调度。这部分任务是 cpu_runnable 的统计主体。
  2. 非可运行态:包含睡眠态、阻塞态、停止态等,任务主动放弃 CPU,不会进入调度运行队列,不计入负载统计。

内核为每一个物理 CPU 核心维护独立的运行队列 struct rq,所有当前 CPU 上的可运行任务都会挂载到该队列中。cpu_runnable 就内嵌在 rq 结构体内部,专门统计当前 CPU 队列内可运行任务的综合负载。

1.2 cpu_runnable 基础定义

cpu_runnable 是调度层的负载度量变量,在内核不同版本中命名与数据结构略有演进,主流 5.4、5.15、6.1 LTS 版本中,该指标隶属于调度负载统计体系,核心作用:

  • 统计当前 CPU 运行队列中可运行任务的负载权重总和,并非单纯的任务个数;
  • 为内核负载均衡、进程迁移、调度决策、CPU 过载判断提供原始数据;
  • 向上层用户态工具输出数据,转化为我们可见的系统 1 分钟 / 5 分钟 / 15 分钟平均负载。

1.3 相关配套核心字段

结合 rq 运行队列结构体,梳理关联字段(内核源码 kernel/sched/sched.h):

struct rq {
    /* 省略大量调度、时钟、中断相关成员 */
    
    /* CFS普通进程运行队列 */
    struct cfs_rq cfs;
    /* 实时进程运行队列 */
    struct rt_rq rt;
    /* Deadline实时任务运行队列 */
    struct dl_rq dl;

    /* 核心:当前CPU可运行任务负载度量值 */
    unsigned long cpu_runnable;
    /* 队列中可运行任务总个数 */
    int nr_running;
    /* 历史负载、平均负载、负载衰减系数,用于计算系统平均负载 */
    struct sched_avg avg;
};
  • nr_running:简单计数,代表当前 CPU 上可运行任务的总数量,只做个数累加,不区分任务权重;
  • cpu_runnable:加权负载值,不同类型任务(普通进程、实时进程、Deadline 进程)拥有不同权重,计算结果更贴合 CPU 实际压力;
  • 三类子队列 cfs_rq/rt_rq/dl_rq:分别管理公平调度、实时调度、硬实时 Deadline 任务,三者的负载都会汇总到 cpu_runnable

1.4 负载权重规则

Linux 对不同调度策略的任务设置了差异化权重,这也是 cpu_runnablenr_running 数值不一致的核心原因:

  1. SCHED_OTHER/SCHED_IDLE(普通 CFS 任务):基础权重最低,系统中绝大多数业务进程属于此类;
  2. SCHED_FIFO/SCHED_RR(实时 RT 任务):权重远高于普通进程,实时任务运行时会抢占普通进程 CPU,对负载影响更大;
  3. SCHED_DEADLINE(硬实时 DL 任务):优先级与权重最高,多用于工业实时场景,一旦就绪会优先占用 CPU。

1.5 平均负载基础逻辑

用户态 uptimetop 展示的负载,是所有 CPU 核心 cpu_runnable 经过时间加权、衰减计算后得到的全局平均值。负载值 > CPU 核心数,代表系统存在任务排队,CPU 处于过载状态。例如 4 核 CPU,负载长期大于 4,说明持续有任务排队等待执行。

二、环境准备

2.1 软硬件环境清单

本文基于主流长期支持内核开发测试,兼顾个人虚拟机、物理机、服务器场景,环境统一如下:

分类 版本 / 配置要求 用途说明
操作系统 Ubuntu 20.04 / Ubuntu 22.04 x86_64 通用 Linux 发行版,兼容性强
内核版本 Linux 5.15 LTS / Linux 6.1 LTS 企业生产环境主流版本,cpu_runnable 逻辑稳定
硬件配置 双核 / 四核 CPU,4GB 以上内存 支持压力测试、内核调试、多进程并发
编译工具 gcc 9.4+、make、binutils 编译内核、编写测试程序
调试分析工具 gdb、perf、ftrace、trace-cmd、htop、mpstat 跟踪内核函数、观测负载变化、统计 CPU 状态
辅助工具 stress、stress-ng 压力测试工具,模拟大量可运行任务,拉高负载

2.2 基础依赖安装

执行以下命令一次性安装所有编译、调试、压测依赖,命令可直接复制使用:

# 更新软件源并安装基础编译、调试工具
sudo apt update && sudo apt install -y build-essential libncurses-dev \
bison flex libssl-dev libelf-dev gdb perf trace-cmd stress stress-ng htop

2.3 内核源码获取与配置

cpu_runnable 全部计算、更新逻辑集中在 kernel/sched/ 目录下,建议下载对应版本内核源码用于对照阅读:

# 下载 Linux 6.1 LTS 内核源码
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.tar.xz
# 解压源码包
tar -xf linux-6.1.tar.xz
cd linux-6.1
内核配置(开启调度调试)

为方便跟踪负载计算流程,开启调度调试、函数跟踪功能:

# 复用当前系统内核配置
cp /boot/config-$(uname -r) .config
# 图形化配置界面
make menuconfig

在配置界面中开启以下选项:

CONFIG_SCHED_DEBUG=y        # 调度器调试开关,查看调度内部状态
CONFIG_FTRACE=y             # 函数跟踪,跟踪cpu_runnable更新函数
CONFIG_DEBUG_KERNEL=y       # 基础内核调试
CONFIG_PROC_FS=y            # 保留proc文件系统,查看进程与负载信息

保存退出后,执行编译(虚拟机 / 多核设备可使用多核编译加速):

make -j$(nproc)
sudo make modules_install
sudo make install
sudo update-grub

重启服务器,在 GRUB 菜单中选择新编译的内核进入。

2.4 源码路径定位

核心文件汇总,后续源码分析均基于以下路径:

  1. 结构体定义:kernel/sched/sched.h(rq、cfs_rq、cpu_runnable 定义)
  2. 负载计算与更新:kernel/sched/fair.ckernel/sched/rt.ckernel/sched/deadline.c
  3. 全局负载汇总:kernel/sched/sched.c

三、应用场景

cpu_runnable 作为内核底层负载指标,广泛应用在服务器运维、云计算、嵌入式实时设备三大场景。在互联网后端服务器中,运维人员通过监控该指标衍生出的系统负载,判断 Web 服务、数据库、中间件是否因请求量突增导致 CPU 队列拥堵,及时扩容或限流。在云计算 K8s 集群中,调度组件会读取节点 CPU 可运行负载,实现 Pod 合理调度,避免节点过载引发容器卡顿。在工业嵌入式 Linux 设备上,工控、机器人、车载域控制器会依靠 cpu_runnable 判断 CPU 负载,动态调整实时任务调度策略,防止高负载下硬实时任务超时。同时该指标也是内核调优、性能测试、压力测试报告中必备的核心观测项。

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

4.1 第一步:拆解 cpu_runnable 核心计算逻辑

cpu_runnable 并非单一变量赋值,而是三类调度子队列负载的汇总值,内核会在任务入队、出队、状态切换时实时更新。

4.1.1 基础汇总逻辑源码

内核中更新 cpu_runnable 的通用逻辑(kernel/sched/sched.c):

/*
 * 功能:汇总当前CPU所有调度队列的可运行负载,更新 cpu_runnable
 * 调用时机:任务状态变更、入队、出队、周期更新时触发
 */
static void update_cpu_runnable(struct rq *rq)
{
    unsigned long cfs_load, rt_load, dl_load;

    /* 1. 获取CFS普通进程队列负载 */
    cfs_load = rq->cfs.runnable_load_avg;
    /* 2. 获取RT实时进程队列负载 */
    rt_load = rq->rt.rt_load;
    /* 3. 获取Deadline硬实时进程队列负载 */
    dl_load = rq->dl.nr_running * SCHED_DL_WEIGHT;

    /* 加权汇总,赋值给核心变量 cpu_runnable */
    rq->cpu_runnable = cfs_load + rt_load + dl_load;
}

代码说明

  1. 普通 CFS 进程使用平均运行负载 runnable_load_avg,该值会随任务运行、休眠动态衰减,更贴合瞬时负载;
  2. RT 实时进程使用独立实时负载统计值;
  3. Deadline 任务权重固定,直接用任务数量乘以预设权重;
  4. 最终三者相加,得到当前 CPU 完整的可运行负载 cpu_runnable
4.1.2 任务入队时更新逻辑(CFS 队列示例)

当一个休眠任务被唤醒,进入可运行队列时,内核会增加负载并刷新 cpu_runnable,源码 kernel/sched/fair.c

/*
 * 功能:CFS任务加入运行队列,更新负载与cpu_runnable
 * @rq: 当前CPU运行队列
 * @p: 待入队任务结构体
 */
static void enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
    struct cfs_rq *cfs_rq = task_cfs_rq(p);

    /* 将任务加入CFS红黑树队列 */
    cfs_enqueue_task(cfs_rq, p, flags);
    /* CFS队列可运行任务计数+1 */
    cfs_rq->nr_running++;

    /* 更新队列平均负载 */
    update_load_avg(cfs_rq, p, UPDATE_TG);
    /* 全局刷新当前CPU的 cpu_runnable */
    update_cpu_runnable(rq);
}

代码说明:任务入队 → 更新子队列负载 → 调用汇总函数刷新 cpu_runnable,保证数据实时性。

4.1.3 任务出队时更新逻辑

当任务阻塞、退出、主动休眠,离开可运行队列时,逻辑相反,减少负载:

/* kernel/sched/fair.c */
static void dequeue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
    struct cfs_rq *cfs_rq = task_cfs_rq(p);

    /* 任务移出CFS队列 */
    cfs_dequeue_task(cfs_rq, p, flags);
    cfs_rq->nr_running--;

    /* 递减负载,刷新 cpu_runnable */
    update_load_avg(cfs_rq, p, UPDATE_TG);
    update_cpu_runnable(rq);
}

核心规则总结:只要任务在「可运行态」和「非可运行态」之间切换,必然触发 cpu_runnable 更新

4.2 第二步:编写用户态测试程序,模拟可运行任务

编写死循环进程,持续占用 CPU,制造大量可运行任务,直观观测负载变化。代码可直接编译运行。

4.2.1 压力测试代码(create_runnable_task.c)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

/* 单个子进程:死循环,持续占用CPU,处于可运行态 */
void busy_task(void)
{
    while(1)
    {
        /* 空循环,无休眠,持续占用CPU */
        __asm__("nop");
    }
}

int main(int argc, char *argv[])
{
    int task_num = 0;
    int i;
    pid_t pid;

    /* 传入参数:创建的可运行任务数量 */
    if(argc != 2)
    {
        printf("用法: %s <任务数量>\n", argv[0]);
        return -1;
    }
    task_num = atoi(argv[1]);
    printf("开始创建 %d 个CPU密集型可运行任务...\n", task_num);

    for(i = 0; i < task_num; i++)
    {
        pid = fork();
        if(pid < 0)
        {
            perror("fork failed");
            return -1;
        }
        else if(pid == 0)
        {
            /* 子进程执行死循环 */
            busy_task();
            exit(0);
        }
    }

    /* 父进程等待所有子进程,保持程序运行 */
    while(wait(NULL) > 0);
    return 0;
}

代码作用:通过 fork 创建指定数量的 CPU 密集型子进程,子进程无休眠、持续运行,全部处于TASK_RUNNING可运行态,拉高 CPU 队列负载。

4.2.2 编译与执行命令
# 编译测试程序
gcc create_runnable_task.c -o runnable_test

# 创建8个可运行任务(根据CPU核心数调整)
./runnable_test 8

4.3 第三步:用户态工具观测负载变化

新开终端,使用系统工具观测负载、可运行任务数量,验证 cpu_runnable 作用。

4.3.1 使用 uptime 查看全局平均负载
uptime

输出解读

14:30:00 up 2h 10min,  2 users,  load average: 7.82, 4.15, 2.03

load average 三个数值依次为 1 分钟、5 分钟、15 分钟平均负载,数据来源于内核 cpu_runnable 加权计算结果。4 核 CPU 下负载接近 8,说明 CPU 队列严重拥堵。

4.3.2 使用 top 查看实时任务状态
top

操作按键:按下 H 展开线程,1 查看每一个 CPU 核心使用率。可以看到:

  1. 大量进程处于 R(可运行态);
  2. CPU 核心占用率接近 100%;
  3. 队列排队任务增多,对应内核 cpu_runnable 持续升高。
4.3.3 使用 mpstat 分核心观测负载
# 每1秒输出一次CPU统计信息
mpstat -P ALL 1

该命令可以区分每一个 CPU 核心的忙碌程度,判断负载是否均衡,对应不同 CPU 的 cpu_runnable 数值差异。

4.4 第四步:使用 ftrace 跟踪 cpu_runnable 更新函数

通过内核 ftrace 跟踪 update_cpu_runnable 调用时机,直观验证任务入队 / 出队时负载更新逻辑,命令全套可直接复制:

# 1. 挂载debugfs(内核跟踪文件系统)
sudo mount -t debugfs none /sys/kernel/debug

# 2. 清空历史跟踪日志
sudo echo > /sys/kernel/debug/tracing/trace

# 3. 指定需要跟踪的内核函数
sudo echo update_cpu_runnable >> /sys/kernel/debug/tracing/set_ftrace_filter

# 4. 开启函数跟踪模式
sudo echo function > /sys/kernel/debug/tracing/current_tracer

# 5. 启动跟踪
sudo echo 1 > /sys/kernel/debug/tracing/tracing_on

此时回到测试程序终端,终止部分测试进程(Ctrl+C),再重新启动,然后执行查看日志:

# 查看跟踪日志
sudo cat /sys/kernel/debug/tracing/trace

# 停止跟踪
sudo echo 0 > /sys/kernel/debug/tracing/tracing_on

日志解读:日志中会大量出现 update_cpu_runnable 调用记录,对应进程创建、销毁、状态切换的每一次负载更新,完全匹配前文源码逻辑。

4.5 第五步:读取内核 proc 文件,查看 nr_running

nr_running(可运行任务个数)是 cpu_runnable 的辅助指标,通过 proc 文件直接读取:

# 查看当前系统所有CPU的可运行任务总数
cat /proc/stat | grep procs_running

输出数值即为全局可运行任务总数,和 cpu_runnable 加权负载形成对照。

五、常见问题与解答

Q1:nr_running 和 cpu_runnable 数值为什么不一样?

解答nr_running 是单纯的任务个数计数,不区分任务类型;cpu_runnable加权负载值,实时进程、Deadline 进程权重远高于普通进程。同一个 CPU 上,1 个实时进程带来的 cpu_runnable 负载,可能等同于多个普通进程,因此两者数值必然不同。

Q2:CPU 使用率很低,但 load 负载很高,是什么原因?

解答:这是典型的CPU 队列拥堵。CPU 使用率低说明硬件 CPU 没有跑满,但 cpu_runnable 数值很高,代表大量任务处于可运行排队状态。常见原因:IO 阻塞唤醒风暴、大量短时进程频繁创建销毁、调度优先级不合理导致任务扎堆排队。

Q3:执行压力测试后,负载下降很慢,为什么?

解答:内核中 cpu_runnable 衍生的平均负载采用指数衰减算法,不会任务退出后立刻清零。系统会按照时间梯度逐步降低负载数值,目的是平滑瞬时压力波动,更真实反映一段时间内的系统压力,属于正常设计。

Q4:多核 CPU 场景下,cpu_runnable 是全局统计还是单 CPU 统计?

解答单 CPU 独立统计。每个物理 CPU 核心的 struct rq 都有独立的 cpu_runnable,内核负载均衡模块会对比各个核心的该指标,将高负载核心上的任务迁移到低负载核心,实现负载均衡。

Q5:修改任务调度策略为 SCHED_FIFO 实时任务,cpu_runnable 会如何变化?

解答:实时任务权重更高,同等任务数量下,cpu_runnable 数值会显著上升。同时实时任务会抢占普通进程 CPU,进一步加剧单核心负载压力,生产环境中不建议大量部署实时任务。

Q6:ftrace 无法捕获到 update_cpu_runnable 函数调用?

解答:优先排查两点:1. 内核是否开启 CONFIG_FTRACECONFIG_SCHED_DEBUG;2. 函数名是否输入错误(不同内核小版本函数名略有差异);3. 确认当前运行的是重新编译后的新内核,而非系统默认内核。

六、实践建议与最佳实践

6.1 运维监控最佳实践

  1. 负载阈值设定:物理 CPU 核心数为 N,常规业务场景下,长期负载 > N 代表系统过载,需要及时扩容或优化业务;交互式服务建议负载控制在 0.7*N 以内,保证响应延迟。
  2. 区分负载与 CPU 使用率:监控同时采集 cpu_runnable 衍生负载和 CPU 使用率,使用率低、负载高优先排查队列排队问题;使用率高、负载高则是纯 CPU 密集型压力。
  3. 多维度监控:结合 mpstat 分核心监控,避免单核心负载过高、多核闲置的负载不均问题。

6.2 程序开发优化建议

  1. 避免大量短时 CPU 密集进程:频繁创建、销毁可运行任务会反复触发 cpu_runnable 更新,增加调度开销,建议使用线程池复用任务。
  2. 慎用高权重实时任务:嵌入式实时场景按需使用 SCHED_FIFO/SCHED_DEADLINE,不要盲目提升任务优先级,防止拉高整体负载。
  3. 减少任务频繁状态切换:进程在 “可运行态 - 阻塞态” 之间频繁切换,会持续触发负载更新与调度运算,对系统性能造成损耗。

6.3 内核调试与调优技巧

  1. 问题排查顺序:负载异常 → 查看 /proc/stat 确认 nr_running → ftrace 跟踪 update_cpu_runnable → 分析任务类型与调度策略。
  2. 负载均衡调优:若出现单核心 cpu_runnable 过高,可调整内核负载均衡调度域参数,加快任务迁移。
  3. 内核裁剪:嵌入式设备若无需精细负载统计,可关闭部分负载平均计算逻辑,减少 CPU 开销。

6.4 压力测试规范

做性能测试、撰写测试报告时,必须同时记录:CPU 使用率、load averagenr_runningcpu_runnable 相关数据,多指标交叉验证系统压力,保证测试结论严谨。

七、总结与应用延伸

本文完整讲解了 Linux 调度子系统中 cpu_runnable 可运行负载度量的全部内容,从基础概念、环境搭建、内核源码解析、实操压测、函数跟踪到问题排查、工程最佳实践形成完整闭环。

cpu_runnable 是 Linux 调度系统的核心负载底座,其本质是对单 CPU 可运行任务进行加权统计,结合三类不同调度策略任务的权重规则,实时反映 CPU 运行队列的拥堵程度。它不仅是 topuptime 等工具负载数据的来源,更是内核负载均衡、进程迁移、调度决策的核心依据。

在工程落地层面,该指标贯穿服务器运维、云计算集群调度、嵌入式实时设备开发三大主流场景:互联网后端依靠它判断服务压力,云原生平台依靠它实现容器调度,工业嵌入式 Linux 依靠它保障实时任务稳定运行。在内核研究、论文撰写、技术报告场景中,cpu_runnable 的计算逻辑、更新时机、数据流转也是调度子系统调研的核心内容。

建议读者基于本文提供的源码、测试程序、ftrace 命令,在虚拟机或测试服务器中完整复现实验,尝试修改任务数量、调度策略,观察 cpu_runnable 和系统负载的变化。深入理解该机制后,不仅可以快速排查 Linux 负载异常问题,也能为后续学习 Linux 负载均衡、实时调度、内核调优打下扎实基础,真正做到理论结合实战,将知识应用到线上项目与技术研究中。

Logo

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

更多推荐