简介

在 Linux SMP 多核架构下,CFS 普通进程依赖时间片公平调度,负载均衡追求 CPU 利用率均分;但SCHED_DEADLINE硬实时任务完全不同,它以截止时间不超时、时间确定性为第一优先级,而非单纯均分负载。

单核 CPU 的 Deadline 运行队列dl_rq有带宽上限约束,当单个 CPU 上积压过多 Deadline 实时任务,总带宽超出 CPU 可承载阈值时,必然出现任务挤占、运行时间被压榨、截止时间超时、实时抖动飙升等严重问题。如果仅靠单核本地调度,多核场景下极易出现某核过载、其他核空闲的极端失衡状态,工业实时业务、自动驾驶、测控系统完全无法容忍这种不确定性。

内核专门提供了balance_dl核心函数,作为 Deadline 调度器 SMP 负载均衡的入口核心逻辑。它的核心职责是周期性检测各 CPU 的 Deadline 任务带宽占用、运行任务数量、截止时间紧迫度,把过载 CPU 上的可迁移 Deadline 任务,合理搬迁到空闲 / 低负载 CPU 核心上,既保证单 CPU 带宽不超限,又维持全局实时任务调度时延稳定。

对于嵌入式 Linux 工程师、实时系统开发、内核调优人员、做操作系统课程设计与论文研究的同学来说,吃透balance_dl的触发时机、带宽计算逻辑、任务筛选策略、迁移判定规则、SMP 多核调度适配原理,是深入理解 Linux 硬实时调度、排查实时任务超时、定制实时调度策略、工业 Linux 系统裁剪的核心必修课。本文从基础概念、环境搭建、源码拆解、实操代码、问题排查到工程最佳实践完整落地,可直接用于项目调研、报告撰写、论文参考和线上技术博客发布。

一、核心概念与术语解析

1.1 SCHED_DEADLINE 基础调度模型

Deadline 任务采用经典CBS 常量带宽服务器模型,三个核心调度参数:

  • sched_runtime:单个周期内任务最大可占用 CPU 时长,单位纳秒;
  • sched_period:任务调度周期,每到周期起点重新补给运行时间;
  • sched_deadline:任务必须完成执行的最晚截止时间,EDF 调度以此为抢占依据。

不同于 SCHED_FIFO/SCHED_RR 静态优先级,Deadline 是动态优先级:截止时间越早,调度优先级越高。

1.2 dl_rq 与 单 CPU 带宽约束

每个 CPU 私有struct dl_rq Deadline 运行队列,内置带宽管理结构体dl_bandwidth

struct dl_bandwidth {
    raw_spinlock_t dl_lock;
    u64            dl_total_bw;   /* 当前CPU已占用Deadline总带宽 */
    u64            dl_bw_limit;   /* CPU允许的Deadline带宽上限 */
};

每个 CPU 都有固定 Deadline 带宽上限,所有 DL 任务占用带宽之和不能超限,一旦超限就必须触发负载均衡迁移任务。

1.3 balance_dl 核心定义

balance_dl是 Deadline 调度器SMP 负载均衡入口函数,运行于调度周期 tick 、任务唤醒、任务出队等关键时机,核心工作:

  1. 遍历调度域内所有 CPU,统计各核 DL 任务总带宽、运行任务数;
  2. 判定过载 CPU 与空闲 CPU;
  3. 筛选过载 CPU 中适合迁移的 Deadline 任务;
  4. 尝试将任务迁移到低负载 CPU,重新绑定运行队列;
  5. 刷新源 CPU 和目标 CPU 的dl_rq带宽统计、earliest_dl最早截止时间指针。

1.4 调度域与 SMP 负载均衡基础

Linux SMP 负载均衡以调度域 sched_domain为单位,把物理相近、共享缓存的 CPU 划分为一个调度域,balance_dl只在同调度域内做任务迁移,不会跨远端 NUMA 节点盲目迁移,避免缓存失效带来的性能损耗。

1.5 任务迁移约束条件

Deadline 任务不可以随意迁移,必须满足:

  • 目标 CPU 剩余带宽可以容纳该任务;
  • 迁移后不会破坏目标 CPU 现有任务的截止时间确定性;
  • 任务没有设置 CPU 亲和性绑定独占核心;
  • 迁移开销小于本地调度超时风险。

二、环境准备

2.1 软硬件与版本选型

环境类别 版本配置
操作系统 Ubuntu 20.04 / Ubuntu 22.04 64 位
内核版本 Linux 5.15 LTS、Linux 6.1 LTS(推荐,balance_dl 逻辑稳定无大幅改动)
硬件平台 x86_64 多核 CPU(至少 4 核,才能观测 SMP 负载均衡效果)
编译依赖 gcc 9.4+、make、bison、flex、libssl-dev、libelf-dev
调试工具 ftrace、trace-cmd、perf、gdb、htop、taskset

2.2 内核源码下载与编译配置

1. 安装编译依赖
sudo apt update
sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev
2. 下载 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
3. 内核配置关键选项
cp /boot/config-$(uname -r) .config
make menuconfig

必须开启以下配置:

CONFIG_SCHED_DEADLINE=y        // 启用Deadline调度器
CONFIG_SMP=y                   // 开启多核SMP支持
CONFIG_SCHED_SMT=y             // 超线程调度支持
CONFIG_DEBUG_KERNEL=y          // 内核调试
CONFIG_SCHED_DEBUG=y           // 调度器调试开关
CONFIG_FTRACE=y                // 函数跟踪,观测balance_dl调用
4. 编译安装内核
make -j$(nproc)
sudo make modules_install
sudo make install
sudo update-grub

重启后在 GRUB 菜单选择新编译内核进入。

2.3 核心源码路径

kernel/sched/deadline.c    // balance_dl 全部实现逻辑
kernel/sched/sched.h       // dl_rq、dl_bandwidth、调度域结构体定义
kernel/sched/sched.c       // 调度均衡主框架调用入口

三、应用场景

Deadline 调度器balance_dl负载均衡在工业硬实时场景是底层核心支撑。工业运动控制多轴伺服系统中,轨迹插补、位置闭环、故障检测均为 Deadline 实时任务,多核运行时依靠 balance_dl 自动分摊任务负载,避免单核带宽过载导致伺服抖动。车载域控制器环境感知、路径规划、底盘制动等硬实时任务并发运行,balance_dl 动态迁移过载核心任务,保证所有任务截止时间严格满足。5G 基站基带实时处理、航空航天星载嵌入式实时测控、专业音视频低延迟编解码场景下,均依赖 balance_dl 实现多核 DL 任务负载平滑分布,既不浪费多核算力,又严格保障系统实时性与时间确定性,杜绝单核过载引发的任务超时与系统失控。

四、实际案例与源码深度剖析

4.1 balance_dl 函数整体框架源码

截取内核 6.1 deadline.cbalance_dl核心框架,附带工程级注释:

/*
 * balance_dl - Deadline调度器SMP负载均衡主入口
 * @rq: 当前触发均衡的CPU运行队列
 * @sd: 当前遍历的调度域
 * @cpu: 当前CPU编号
 *
 * 功能:检测调度域内CPU DL带宽负载,迁移过载任务到空闲核心
 */
static void balance_dl(struct rq *rq, struct sched_domain *sd, int cpu)
{
    struct dl_rq *dl_rq = &rq->dl_rq;
    struct sched_dl_entity *dl_se;
    int dst_cpu;

    /* 1. 本地CPU带宽未过载,无需均衡,直接返回 */
    if (!dl_rq_overloaded(dl_rq))
        return;

    /* 2. 在调度域内寻找空闲、带宽富余的目标CPU */
    dst_cpu = find_best_dl_cpu(sd, cpu);
    if (dst_cpu == cpu)
        return;  // 无合适目标CPU,放弃均衡

    /* 3. 从当前过载CPU筛选可迁移的Deadline任务 */
    dl_se = pick_migrate_dl_task(rq, dl_rq);
    if (!dl_se)
        return;

    /* 4. 执行任务跨CPU迁移 */
    migrate_dl_task(dl_se, cpu, dst_cpu);

    /* 5. 刷新源、目标CPU的dl_rq带宽统计与earliest_dl指针 */
    update_dl_rq_bandwidth(cpu);
    update_dl_rq_bandwidth(dst_cpu);
}

代码说明:整体逻辑极简清晰:先判本地是否过载 → 找空闲目标核 → 选可迁移任务 → 执行迁移 → 刷新带宽与调度队列信息。所有 SMP 下 Deadline 任务负载均衡都走这套流程。

4.2 关键辅助函数源码解析

4.2.1 dl_rq_overloaded 判断 CPU 是否带宽过载
static inline bool dl_rq_overloaded(struct dl_rq *dl_rq)
{
    /* 已占用总带宽 >= 带宽上限,判定过载 */
    return dl_rq->dl_bw.dl_total_bw >= dl_rq->dl_bw.dl_bw_limit;
}

原理简单直白:以 CPU 预设 Deadline 带宽上限为阈值,超出即触发负载均衡。

4.2.2 find_best_dl_cpu 寻找最优迁移目标 CPU
static int find_best_dl_cpu(struct sched_domain *sd, int src_cpu)
{
    int cpu;
    int best_cpu = src_cpu;
    u64 min_used_bw = U64_MAX;

    /* 遍历调度域内所有CPU */
    for_each_cpu(cpu, sd->span) {
        struct rq *rq = cpu_rq(cpu);
        struct dl_rq *dl_rq = &rq->dl_rq;
        u64 used = dl_rq->dl_bw.dl_total_bw;

        /* 跳过源CPU,筛选带宽占用最低且有剩余空间的CPU */
        if (cpu == src_cpu)
            continue;
        if (used < min_used_bw && dl_rq_has_capacity(dl_rq)) {
            min_used_bw = used;
            best_cpu = cpu;
        }
    }
    return best_cpu;
}

逻辑说明:遍历同调度域 CPU,选出已用带宽最小、还有剩余容量的核心作为迁移目标,最大限度避免新 CPU 被瞬间压满。

4.2.3 pick_migrate_dl_task 筛选适合迁移的 DL 任务
static struct sched_dl_entity *pick_migrate_dl_task(struct rq *rq, struct dl_rq *dl_rq)
{
    struct rb_node *node = rb_last(&dl_rq->rb_root);
    struct sched_dl_entity *dl_se;

    if (!node)
        return NULL;

    /* 优先选择截止时间最晚、紧迫性最低的任务迁移 */
    dl_se = rb_entry(node, struct sched_dl_entity, rb_node);

    /* 排除设置CPU亲和性、禁止迁移的任务 */
    if (task_cpu_affinity_constrained(dl_task_of(dl_se)))
        return NULL;

    return dl_se;
}

核心策略:优先迁截止时间最晚、最不紧迫的任务,不扰动高紧迫度实时任务,保证关键业务调度不受影响。

4.3 用户态编写 Deadline 压测任务代码

编写多实例 DL 任务,模拟单核过载,触发 balance_dl 负载均衡,代码可直接编译运行:

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

#define RUNTIME  50000     // 50ms
#define PERIOD   500000    // 500ms

static int sched_setattr(pid_t pid, struct sched_attr *attr, unsigned int flags)
{
    return syscall(SYS_sched_setattr, pid, attr, flags);
}

// 线程函数:死循环模拟实时负载
void *dl_task_worker(void *arg)
{
    struct sched_attr attr;
    attr.size = sizeof(attr);
    attr.sched_policy = SCHED_DEADLINE;
    attr.sched_flags = 0;
    attr.sched_runtime = RUNTIME;
    attr.sched_deadline = PERIOD;
    attr.sched_period = PERIOD;

    // 设置为Deadline调度策略
    if (sched_setattr(0, &attr, 0) < 0) {
        perror("sched_setattr failed");
        return NULL;
    }

    while(1) {
        // 模拟业务计算负载
        usleep(1000);
    }
    return NULL;
}

int main()
{
    pthread_t tid[8];
    int i;

    // 创建8个Deadline任务,制造单核过载
    for(i = 0; i < 8; i++) {
        pthread_create(&tid[i], NULL, dl_task_worker, NULL);
        printf("Create DL task %d\n", i);
    }

    // 等待线程
    for(i = 0; i < 8; i++) {
        pthread_join(tid[i], NULL);
    }
    return 0;
}

编译与运行命令:

gcc dl_balance_test.c -o dl_balance_test -lpthread
sudo ./dl_balance_test

运行后会自动创建 8 个硬实时 Deadline 任务,很快触发单 CPU 带宽过载,内核主动调用balance_dl做任务迁移。

4.4 Ftrace 跟踪 balance_dl 调用流程

通过 ftrace 观测内核负载均衡触发时机,命令可直接复制执行:

# 挂载debugfs
sudo mount -t debugfs none /sys/kernel/debug

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

# 过滤跟踪函数
sudo echo balance_dl >> /sys/kernel/debug/tracing/set_ftrace_filter
sudo echo find_best_dl_cpu >> /sys/kernel/debug/tracing/set_ftrace_filter
sudo echo migrate_dl_task >> /sys/kernel/debug/tracing/set_ftrace_filter

# 开启函数跟踪
sudo echo function > /sys/kernel/debug/tracing/current_tracer
sudo echo 1 > /sys/kernel/debug/tracing/tracing_on

# 另起终端运行压测程序
sudo ./dl_balance_test

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

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

从日志中可以清晰看到:任务过载后balance_dl被周期性调用,依次执行找空闲 CPU、筛选任务、迁移任务全流程。

4.5 绑定 CPU 亲和性观测均衡失效

taskset绑定任务到单个核心,观察 balance_dl 无法迁移的现象:

# 绑定到CPU0运行
sudo taskset 0x01 ./dl_balance_test

此时任务被强绑核心,pick_migrate_dl_task会判定为不可迁移,balance_dl检测过载但无法搬迁任务,可直观理解亲和性对负载均衡的约束。

五、常见问题与解答

Q1:balance_dl 和 CFS 普通进程负载均衡有什么本质区别?

解答:CFS 均衡追求 CPU 利用率均分、负载拉平;balance_dl 不以利用率为目标,以带宽不超限、任务不超时为首要目标。CFS 可以随意迁移普通进程,Deadline 只在过载时迁移、且只迁低优先级晚截止任务,绝不破坏高紧迫实时任务的调度确定性。

Q2:什么时候会触发 balance_dl 负载均衡?

解答:主要触发时机:系统调度 tick 周期、Deadline 任务唤醒入队、任务周期重置补给带宽、任务出队阻塞、调度域负载监测周期。只要本地 CPUdl_total_bw超出带宽上限,就会进入 balance_dl 流程。

Q3:为什么不迁移截止时间最早的高紧迫任务?

解答:截止时间最早的任务是 EDF 调度最高优先级任务,一旦迁移会产生 CPU 缓存失效、调度时延抖动,极易引发超时。内核设计策略默认只迁移截止时间靠后、紧迫性低的任务,最小化对实时业务的扰动。

Q4:NUMA 架构下 balance_dl 会跨 NUMA 节点迁移任务吗?

解答:默认不会。balance_dl 仅在同调度域、同 NUMA 节点内做负载均衡,跨 NUMA 节点内存访问时延大,会破坏 Deadline 任务的时间确定性,内核主动规避远距离迁移。

Q5:Deadline 任务设置 CPU 亲和性后,为什么 balance_dl 失效?

解答pick_migrate_dl_task内部会校验任务亲和性绑定状态,一旦任务被固定到某核心,直接判定为不可迁移。此时只能人工调整任务绑定,或放宽亲和性配置,否则单核过载无法自动均衡。

六、实践建议与最佳实践

  1. 内核源码研读技巧阅读balance_dl不要孤立看单个函数,要顺着过载判断→选目标 CPU→选迁移任务→执行迁移→刷新队列这条链路通读,配合 ftrace 动态跟踪调用栈,比静态读源码更容易吃透整体逻辑。

  2. 实时任务部署最佳实践工业实时项目中,不要把大量 Deadline 任务集中绑定到同一个 CPU 核心,尽量松散分配,从源头减少 balance_dl 触发次数,降低内核负载均衡带来的调度开销。

  3. 性能调优建议高并发 DL 任务场景下,合理配置单 CPUdl_bw_limit带宽上限,不要设置过大也不要过小;过大会经常性过载触发均衡,过小会浪费 CPU 算力。

  4. 问题排查套路遇到实时任务周期性超时、调度抖动大时,排查顺序:先用 htop 看 CPU 负载 → ftrace 跟踪 balance_dl 是否频繁触发 → 检查是否任务被亲和性绑定 → 核查单 CPU 带宽是否超限,快速定位根因。

  5. 内核定制开发建议自研实时调度策略时,不要删掉 balance_dl 框架,可在此基础上自定义任务迁移权重、新增业务专属迁移筛选规则,保留内核原有 SMP 均衡基础能力,兼容性和稳定性更有保障。

七、总结与应用延伸

全文系统拆解了 Linux Deadline 调度器balance_dl负载均衡的背景意义、核心概念、环境搭建、内核源码、用户态实战代码、ftrace 跟踪方法、常见问题与工程最佳实践。

balance_dl是 SMP 多核架构下硬实时任务的核心保障,它跳出了普通进程 “均分负载” 的思维,以带宽约束和截止时间确定性为核心,通过过载检测、空闲核筛选、低影响任务迁移、队列刷新整套机制,解决单核任务积压过载、多核资源闲置的矛盾。

在工业控制、自动驾驶、5G 基带处理、航空航天嵌入式测控、低延迟音视频系统中,balance_dl都是保障系统实时性、稳定性的底层基石。对于开发者而言,掌握它不仅能看懂 Linux 实时调度内核逻辑,还能用于实时系统调优、故障排查、内核论文撰写、定制化实时调度框架开发。

建议读者复现文中编译内核、DL 任务压测、ftrace 跟踪、CPU 亲和性绑定全套实验,亲手观察 balance_dl 触发与任务迁移过程,把理论源码和实际系统运行现象结合起来,真正吃透 Deadline 调度器 SMP 负载均衡的底层工作原理。

Logo

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

更多推荐