CFS负载均衡
负载均衡的架构:

负载均衡关键数据结构——调度域和调度组
调度域: 按 CPU 拓扑结构(NUMA 节点、物理核、逻辑核)分层组织
struct sched_domain {
/* These fields must be setup */
struct sched_domain __rcu *parent; /* top domain must be null terminated */
struct sched_domain __rcu *child; /* bottom domain must be null terminated */
struct sched_group *groups; /* the balancing groups of the domain */
unsigned long min_interval; /* Minimum balance interval ms */
unsigned long max_interval; /* Maximum balance interval ms */
unsigned int busy_factor; /* less balancing by factor if busy */
unsigned int imbalance_pct; /* No balance until over watermark */
unsigned int cache_nice_tries; /* Leave cache hot tasks for # tries */
unsigned int imb_numa_nr; /* Nr running tasks that allows a NUMA imbalance */
int nohz_idle; /* NOHZ IDLE status */
int flags; /* See SD_* */
int level;
/* Runtime fields. */
unsigned long last_balance; /* init to jiffies. units in jiffies */
unsigned int balance_interval; /* initialise to 1. units in ms. */
unsigned int nr_balance_failed; /* initialise to 0 */
/* idle_balance() stats */
u64 max_newidle_lb_cost;
unsigned long last_decay_max_lb_cost;
#ifdef CONFIG_SCHEDSTATS
/* load_balance() stats */
#endif
#ifdef CONFIG_SCHED_DEBUG
char *name;
#endif
union {
void *private; /* used during construction */
struct rcu_head rcu; /* used during destruction */
};
struct sched_domain_shared *shared;
unsigned int span_weight;
/*
* Span of all CPUs in this domain.
*
* NOTE: this field is variable length. (Allocated dynamically
* by attaching extra space to the end of the structure,
* depending on how many CPUs the kernel has booted up with)
*/
unsigned long span[];
};
调度域是一个树形结构,parent 和 child 分别指向上一层级和下一层级
均衡的时间间隔:busy_factor * balance_interval
| 调度域标记 | 描述 |
|---|---|
| SD_BALANCE_NEWIDLE | 标记domain是否支持newidle balance。 |
| SD_BALANCE_EXEC SD_BALANCE_FORK SD_BALANCE_WAKE |
在exec、fork、wake的时候,该domain是否支持指定类型的负载均衡。这些标记符号主要用来在确定exec、fork和wakeup场景下选核的范围。 |
| SD_WAKE_AFFINE | 是否在该domain上考虑进行wake affine,即满足一定条件下,让waker和wakee尽量靠近 |
| SD_ASYM_CPUCAPACITY | 该domain上的CPU上是否具有一样的capacity?MC domain上的CPU算力一样,但是DIE domain上会设置该flag |
| SD_SHARE_CPUCAPACITY | 该domain上的CPU上是否共享计算单元?例如SMT下,两个硬件线程被看做两个CPU core,但是它们之间不是完全独立的,会竞争一些硬件的计算单元。 手机上未使用该flag |
| SD_SHARE_PKG_RESOURCES | Domain中的cpu是否共享SOC上的资源(例如cache)。手机平台上,MC domain会设定该flag。 |
调度组:同一层级内的 CPU 分组
struct sched_group {
struct sched_group *next; /* Must be a circular list */
atomic_t ref;
unsigned int group_weight;
unsigned int cores;
struct sched_group_capacity *sgc;
int asym_prefer_cpu; /* CPU of highest priority in group */
int flags;
/*
* The CPUs this group covers.
*
* NOTE: this field is variable length. (Allocated dynamically
* by attaching extra space to the end of the structure,
* depending on how many CPUs the kernel has booted up with)
*/
unsigned long cpumask[];
};
调度组是一个环形链表
负载均衡时机
- 当某个 CPU 队列为空的时候,CPU 队列为空说明当前 CPU 空闲,因此可以从其他 CPU 上拉取任务(CPU进入idle)
- 周期性检查,scheduler_tick -> rebalance_tick ,从当前 CPU 的调度领开始依次往上遍历,检查是否满足调用 load_balance 的条件,为根据当前 CPU 的负载来决定调度频率

除了上面的情况会触发负载均衡(load balance),还有以下两种场景也会使用负载均衡的逻辑:
- 任务放置(task placement)
- 主动均衡(active upmigration)
负载的计算
sched_entity 负载的计算:
- 基于 CPU 运行队列长度 和 任务优先级权重(nice 值)
- 使用 PELT(Per-Entity Load Tracking) 算法跟踪每个任务的贡献
- 是瞬时、细粒度的调度决策依据
影响负载均衡的指标:任务负载,sched group 负载,CPU算力
跟踪任务负载的原因:
- 判断该任务是否适合当前 CPU 算力
- 如果判定需要均衡,那么需要在 CPU 之间迁移多少任务才能达到平衡
misfit task 是在引入异构计算系统后的标记,表示该任务与当前 CPU 算力不匹配,比如在小核上运行,不匹配后会触发 active upmigration
跟踪内核负载均衡
# 查看调度统计(需内核配置 SCHED_DEBUG)
$ cat /proc/sched_debug
# 查看每个 CPU 的运行队列
$ cat /proc/schedstat
# 使用 bcc/eBPF 跟踪任务迁移
$ bpftrace -e 'tracepoint:sched:sched_migrate_task { printf("%s: %d -> %d\n", args->comm, args->orig_cpu, args->dest_cpu); }'
负载均衡
Linux的负载均衡器有下面的几个类型:

periodic balancer:周期性负载均衡,用于 busy cpu,从这个 cpu 开始自底向上做负载均衡
nohz idle balancer:其他的 CPU 已经进入 idle 状态,本 CPU 通过发起 ipi 中断唤醒其他 idle cpu来做负载均衡,被唤醒的idle cpu代表所有idle cpu来做负载均衡
new idle balancer:马上要进入 idle 状态的 CPU 进行负载均衡
periodic balance
触发:时钟中断 -> scheduler_tick -> trigger_load_balance -> raise_softirq(SCHED_SOFTIRQ); -> run_rebalance_domains
负载均衡的基本流程:
当周期性负载均衡触发的时候,由被触发的 CPU 开始自底向上做负载均衡。判断每个层级 domain 里面各个 sched group 的均衡情况,正确来说的步骤是下面几个:
- 找到 domain 中最忙的 sched group
- 找到最忙 group 中最忙的 CPU
- 从选中的那个最忙的 CPU 上拉取任务,具体拉多少任务由不均衡度来决定
在每个 domain 里面都会做一遍这个事情,sched group 的算力是整个 group 中算力之和,sched group 的负载是整个 group 中负载之和。
核心逻辑都在 rebalance_domains:
for_each_domain(cpu, sd) {
/*
* Decay the newidle max times here because this is a regular
* visit to all the domains.
*/
need_decay = update_newidle_cost(sd, 0);
max_cost += sd->max_newidle_lb_cost;
/*
* Stop the load balance at this level. There is another
* CPU in our sched group which is doing load balancing more
* actively.
*/
if (!continue_balancing) {
if (need_decay)
continue;
break;
}
interval = get_sd_balance_interval(sd, busy);
if (time_after_eq(jiffies, sd->last_balance + interval)) {
if (load_balance(cpu, rq, sd, idle, &continue_balancing)) {
/*
* The LBF_DST_PINNED logic could have changed
* env->dst_cpu, so we can't know our idle
* state even if we migrated tasks. Update it.
*/
idle = idle_cpu(cpu) ? CPU_IDLE : CPU_NOT_IDLE;
busy = idle != CPU_IDLE && !sched_idle_cpu(cpu);
}
sd->last_balance = jiffies;
interval = get_sd_balance_interval(sd, busy);
}
}
这就是从最底层调度域开始往上遍历,continue_balancing 用于判断是否继续往上层调度域遍历,因为越往上层调度域包含的 CPU 数量越多调度开销越大。每一层都判断是否满足了负载的时间间隔,满足了就调用 load_balance,并且计算下一次均衡的时间点。
need_decay 用于表示是否衰减,你可以看到就算 continue_balancing == 0,如果 need_decay != 0 也需要遍历上层调度域,因为 need_decay 表示衰减的是 max_newidle_cost,如果不进行衰减 avg_idle 会一直小于 max_newidle_cost,也就意味着 new idle balance 永远不会触发
load_balance 的逻辑大概是:
- should_we_balance,决定当前 CPU 是否应该负责执行负载均衡。在多 CPU 系统中,同一个调度组(sched_group)内有多个 CPU,但只需要一个 CPU 来执行负载均衡,避免重复工作。这个函数就是用来选择"谁来做这件事"
- find_busiest_group -> find_busiest_queue 找到最繁忙的队列
- detach_tasks -> attach_tasks 分离和添加任务
- 处理亲和性问题 LBF_DST_PINNED(任务无法迁移到
dst_cpu,但可以迁移到组内其他 CPU) 和 LBF_ALL_PINNED(所有任务都被 CPU 亲和性绑定,无法迁移) - 处理主动负载均衡 need_active_balance
均衡间隔控制:
sched_domain 里面相关的字段:
| 成员 | 描述 |
|---|---|
| last_balance | 最近在该sched domain上执行均衡操作的时间点。判断sched domain是否需要进行均衡的标准是对比当前jiffies值和last_balance+interval 这里的interval是get_sd_balance_interval实时获取的 |
| min_interval max_interval |
做均衡也是需要开销的,我们不能时刻去检查调度域的均衡状态,这两个参数定义了检查该sched domain均衡状态的时间间隔的范围。min_interval缺省设定为sd weight,即sched domain内CPU的个数。max_interval等于2倍的min_interval。 |
| balance_interval | 定义了该sched domain均衡的基础时间间隔,一方面,该值和sched domain所处的层级有关,层级越高,覆盖的CPU越多,balance_interval越大。另外一方面,在调用load_balance的时候,会根据实际的均衡情况对其进行加倍或者保持原值。 |
| busy_factor | 正常情况下,balance_interval定义了均衡的时间间隔,如果cpu繁忙,那么均衡要时间间隔长一些,即时间间隔定义为busy_factor x balance_interval。缺省值是32。 |
时间间隔可以简单认为是 \(busy\_factor \times balance\_interval\)
上层调度域的时间间隔和下层的会错开
nohz idle balance
nohz_balancer_kick:决定是否需要触发空闲负载均衡器 (ILB) 来处理系统中的负载不均衡
nohz_balancer_kick(struct rq *rq) [fair.c:11847]
│
├── 1. if (unlikely(rq->idle_balance)) → return [fair.c:11852]
│ └── 已经在进行空闲均衡,直接返回
│
├── 2. nohz_balance_exit_idle(rq) [fair.c:11858]
│ └── 退出空闲模式,更新繁忙统计
│
├── 3. if (likely(!atomic_read(&nohz.nr_cpus))) → return [fair.c:11864]
│ └── 没有 CPU 处于 NOHZ 模式,无需均衡
│
├── 4. 检查是否需要更新统计 [fair.c:11866-11869]
│ └── if (READ_ONCE(nohz.has_blocked) &&
│ time_after(now, READ_ONCE(nohz.next_blocked)))
│ └── flags = NOHZ_STATS_KICK
│
├── 5. if (time_before(now, nohz.next_balance)) → goto out [fair.c:11871]
│ └── 未到下次均衡时间
│
├── 6. if (rq->nr_running >= 2) [fair.c:11873-11876]
│ ├── flags = NOHZ_STATS_KICK | NOHZ_BALANCE_KICK
│ └── goto out (当前 CPU 有多个任务运行,触发均衡)
│
├── 7. rcu_read_lock() [fair.c:11878]
│
├── 8. 检查 CPU 容量不足 [fair.c:11880-11890]
│ ├── sd = rcu_dereference(rq->sd)
│ ├── if (sd && rq->cfs.h_nr_running >= 1 && check_cpu_capacity(rq, sd))
│ │ └── flags = NOHZ_STATS_KICK | NOHZ_BALANCE_KICK
│ │ └── goto unlock (当前 CPU 容量降低,需要迁移任务)
│
├── 9. 检查 ASYM_PACKING(不对称打包)[fair.c:11892-11909]
│ ├── sd = rcu_dereference(per_cpu(sd_asym_packing, cpu))
│ └── if (sd)
│ └── for_each_cpu_and(i, sched_domain_span(sd), nohz.idle_cpus_mask)
│ └── if (sched_asym(sd, i, cpu))
│ └── flags = NOHZ_STATS_KICK | NOHZ_BALANCE_KICK
│ └── goto unlock (有更优先的 CPU 空闲,需要迁移任务)
│
├── 10. 检查 ASYM_CPUCAPACITY(不对称 CPU 容量)[fair.c:11911-11930]
│ ├── sd = rcu_dereference(per_cpu(sd_asym_cpucapacity, cpu))
│ └── if (sd)
│ ├── if (check_misfit_status(rq, sd))
│ │ └── flags = NOHZ_STATS_KICK | NOHZ_BALANCE_KICK
│ │ └── goto unlock (有不匹配任务,需要更高容量 CPU)
│ └── goto unlock (不对称系统,跳过 LLC 逻辑)
│
├── 11. 检查 LLC 域不平衡(Last Level Cache)[fair.c:11932-11952]
│ ├── sds = rcu_dereference(per_cpu(sd_llc_shared, cpu))
│ └── if (sds)
│ └── nr_busy = atomic_read(&sds->nr_busy_cpus)
│ └── if (nr_busy > 1)
│ └── flags = NOHZ_STATS_KICK | NOHZ_BALANCE_KICK
│ └── goto unlock (LLC 域有多个繁忙 CPU,可优化缓存利用率)
│
├── 12. unlock: rcu_read_unlock() [fair.c:11954]
│
├── 13. out: [fair.c:11956]
│ └── if (READ_ONCE(nohz.needs_update))
│ └── flags |= NOHZ_NEXT_KICK (需要更新,设置下次踢标志)
│
└── 14. if (flags) → kick_ilb(flags) [fair.c:11959]
└── 触发空闲负载均衡器(Idle Load Balancer)
├── NOHZ_STATS_KICK: 更新统计信息
├── NOHZ_BALANCE_KICK: 执行负载均衡
└── NOHZ_NEXT_KICK: 继续下次均衡
- 当前CPU本身就是 idle 的(
rq->idle_balance == true) 就不需要触发 nohz idle balance - 当CPU从idle状态醒来,第一个tick会更新全局变量nohz的状态以及sched domain的cpu busy状态。虽然nohz idle balance本质上是tick balance,但是它会发IPI,会唤醒idle的cpu,带来额外的开销,所以还是要控制触发nohz idle balance的频次。
- nr_cpus和idle_cpus_mask这两个成员可以让调度器了解当前系统idle CPU的情况,从而选择合适的CPU来执行nohz idle balance。如果系统中根本没有idle cpu,那么也就没有必要触发nohz idle load balance了。
- nohz idle balance有两部分的功能:(1)更新idle cpu上的blocked load(2)负载均衡。可以只更新blocked load,但是负载均衡必须要包括更新blocked load功能。如果当前idle的cpu上有需要衰减的负载,那么标记之。
- next_balance是用来控制触发nohz idle balance的时间点,这个时间点应该是和系统中所有idle cpu的rq->next_balance相关的,也就是说,如果系统中所有idle cpu都还没有到达均衡时间点,那么根本也就没有必要触发nohz idle balance。在执行nohz idle balance的时候,调度器实际上会遍历idle cpu找到rq->next_balance最小的(即最近需要均衡的)赋值给nohz.next_balance,这个值作为触发nohz idle balance的时间点。
- 总之就是根据各种情况确认是否发起 nohz idle balance
如果确认发起,则调用 kick_ilb 发送 IPI 中断给对应的 CPU,并且设置被kick的cpu的runqueue的nohz_flags,来告诉被 kick 的 cpu 要执行 nohz idle balance。 这个 cpu 是代表所有的 idle cpu 来做负载均衡的,也就是可以拉取busy cpu的任务到所有的idle cpus上面。
kick_ilb(unsigned int flags) [fair.c:11812]
│
├── 1. if (flags & NOHZ_BALANCE_KICK) [fair.c:11817-11818]
│ └── nohz.next_balance = jiffies+1
│ └── 设置下次负载均衡时间为下一 jiffies
│
├── 2. ilb_cpu = find_new_ilb() [fair.c:11820]
│ └── 查找空闲负载均衡器(ILB)CPU
│ ├── 遍历 nohz.idle_cpus_mask
│ ├── 检查 CPU 是否适合作为 ILB
│ └── 返回选定的 CPU 号或 -1(未找到)
│
├── 3. if (ilb_cpu < 0) → return [fair.c:11821-11822]
│ └── 没有找到合适的 ILB CPU,直接返回
│
├── 4. atomic_fetch_or(flags, nohz_flags(ilb_cpu)) [fair.c:11828]
│ └── 原子地设置目标 CPU 的 nohz 标志
│ └── 返回旧标志值
│
├── 5. if (flags & NOHZ_KICK_MASK) → return [fair.c:11829-11830]
│ └── 已经有其他请求在处理该 CPU,避免重复触发
│ └── NOHZ_KICK_MASK = NOHZ_STATS_KICK | NOHZ_BALANCE_KICK
│
├── 6. smp_call_function_single_async(ilb_cpu, &cpu_rq(ilb_cpu)->nohz_csd) [fair.c:11838]
│ └── 向目标 ILB CPU 发送异步 IPI(处理器间中断)
│ ├── 触发目标 CPU 的 nohz_csd_func() 回调
│ ├── 目标 CPU 必须处于空闲状态
│ └── 软中断将在此 IPI 返回前执行
│
└── return
find_new_ilb() [fair.c:11787]
│
├── for_each_cpu_wrap(cpu, nohz.idle_cpus_mask, smp_processor_id())
│ └── 遍历空闲 CPU 列表
│
├── if (!cpu_ilb_idle(cpu))
│ └── continue (跳过不适合的 CPU)
│
├── if (cpumask_test_cpu(cpu, nohz.idle_cpus_mask))
│ └── return cpu (找到合适的 ILB CPU)
│
└── return nr_cpu_ids (未找到)
smp_call_function_single_async()
│
└── 发送 IPI 到 ilb_cpu
│
└── 触发 nohz_csd_func() 回调 [在目标 CPU 上执行]
│
└── raise_softirq_irqoff(SCHED_SOFTIRQ)
│
└── 触发 run_rebalance_domains()
│
└── nohz_idle_balance() [fair.c:12093]
│
├── update_blocked_averages()
├── rebalance_domains()
└── 清除 nohz_flags()
new idle balance
发生在 schedule() 的时候,如果调度完 rq 为空就会调用 newidle_balance()
schedule()
└── pick_next_task_fair() [fair.c:8539]
└── newidle_balance() [fair.c:8520]
└── load_balance() [fair.c:12329]
└── 尝试从其他 CPU 拉取任务到空闲 CPU
newidle_balance 的代码逻辑:
newidle_balance(struct rq *this_rq, struct rq_flags *rf) [fair.c:12289]
│
├── 1. update_misfit_status(NULL, this_rq) [fair.c:12312]
│
├── 2. if (this_rq->ttwu_pending) → return 0 [fair.c:12319]
│ └── 有任务等待运行,无需搜索
│
├── 3. this_rq->idle_stamp = rq_clock(this_rq) [fair.c:12325]
│ └── 记录空闲开始时间
│
├── 4. if (!cpu_active(this_cpu)) → return 0 [fair.c:12331]
│ └── CPU 未激活,不拉取任务
│
├── 5. rq_unpin_lock(this_rq, rf) [fair.c:12339]
│ └── 释放 rq 锁(为 load_balance 做准备)
│
├── 6. 检查是否需要负载均衡 [fair.c:12341-12352]
│ ├── rcu_read_lock()
│ ├── sd = rcu_dereference_check_sched_domain(this_rq->sd)
│ ├── if (!this_rq->rd->overload ||
│ │ (sd && this_rq->avg_idle < sd->max_newidle_lb_cost))
│ │ ├── update_next_balance(sd, &next_balance)
│ │ ├── rcu_read_unlock()
│ │ └── goto out
│ └── rcu_read_unlock()
│
├── 7. raw_spin_rq_unlock(this_rq) [fair.c:12354]
│ └── 完全释放 rq 锁
│
├── 8. update_blocked_averages(this_cpu) [fair.c:12357]
│ └── 更新阻塞任务的平均负载
│
├── 9. 遍历调度域 for_each_domain(this_cpu, sd) [fair.c:12360-12395]
│ │
│ ├── update_next_balance(sd, &next_balance)
│ │
│ ├── if (this_rq->avg_idle < curr_cost + sd->max_newidle_lb_cost)
│ │ └── break (空闲时间不足,停止)
│ │
│ ├── if (sd->flags & SD_BALANCE_NEWIDLE) [fair.c:12371]
│ │ │
│ │ ├── load_balance(this_cpu, this_rq, sd, CPU_NEWLY_IDLE, &continue_balancing) [fair.c:12373]
│ │ │ ├── find_busiest_group() - 寻找最忙的调度组
│ │ │ ├── find_busiest_queue() - 寻找最忙的运行队列
│ │ │ ├── detach_tasks() - 从目标队列分离任务
│ │ │ └── attach_tasks() - 将任务附加到本地队列
│ │ │
│ │ ├── 计算域成本 domain_cost = t1 - t0
│ │ ├── update_newidle_cost(sd, domain_cost)
│ │ └── curr_cost += domain_cost
│ │
│ └── if (pulled_task || this_rq->nr_running > 0 || this_rq->ttwu_pending)
│ └── break (已有任务,停止搜索)
│
├── 10. raw_spin_rq_lock(this_rq) [fair.c:12398]
│ └── 重新获取 rq 锁
│
├── 11. if (curr_cost > this_rq->max_idle_balance_cost)
│ └── this_rq->max_idle_balance_cost = curr_cost [fair.c:12400]
│
├── 12. 检查是否有新任务入队 [fair.c:12406-12409]
│ ├── if (this_rq->cfs.h_nr_running && !pulled_task)
│ │ └── pulled_task = 1 (假装拉取了任务)
│ └── if (this_rq->nr_running != this_rq->cfs.h_nr_running)
│ └── pulled_task = -1 (高优先级类任务)
│
├── 13. out: 更新下次平衡时间 [fair.c:12412-12413]
│ └── if (time_after(this_rq->next_balance, next_balance))
│ └── this_rq->next_balance = next_balance
│
├── 14. if (pulled_task)
│ ├── this_rq->idle_stamp = 0 (清除空闲时间戳)
│ └── nohz_newidle_balance(this_rq) [fair.c:12416]
│ └── 触发 NOHZ 空闲均衡
│
├── 15. rq_repin_lock(this_rq, rf) [fair.c:12418]
│
└── return pulled_task
├── 0: 未拉取任务
├── 1: 成功拉取任务
└── -1: 有高优先级任务
this_rq->rd->overload :表示 CPU 是否是 overload 状态,overload 状态满足两个条件:1. 大于1个runnable task 2. 只有一个正在运行的任务,但是是misfit task
需要注意的是就算是不做 balance 也需要去更新 blocked load
new idle balance 的频次控制:
sched_domain 中跟 new idle balance 相关的字段:
| 成员 | 描述 |
|---|---|
| u64 max_newidle_lb_cost | 在该domain上进行newidle balance的最大时间长度(即newidle balance的开销)。 每次在该domain上进行new idle balance的时候都会记录时长,然后把最大值记录在这个成员中。 这个值会随着时间衰减,防止一次极值会造成永久的影响。 |
| unsigned long next_decay_max_lb_cost |
max_newidle_lb_cost会记录最近在该sched domain上进行newidle balance的最大时间长度,这个max cost不是一成不变的,它有一个衰减过程,每秒衰减1%,这个成员就是用来控制衰减的。 |
rq 中的字段:
| 成员 | 描述 |
|---|---|
| idle_stamp | 记录CPU进入idle状态的时间点,用于计算avg_idle。在该CPU执行任务期间,该值等于0 |
| avg_idle | 记录CPU的平均idle时间 |
| max_idle_balance_cost | 该CPU进行new idle balance的最大开销。 |
avg_idle 的计算:当前的avg_idle = 上次avg_idle + (本次idle time - 上次avg_idle)/8。为了防止CPU一次idle太久时间带来的影响,我们限制了avg_idle的最大值,即计算出来avg_idle的值不能大于2倍的max_idle_balance_cost值。
max_idle_balance_cost最小值sysctl_sched_migration_cost
任务放置
当任务被创建后需要选择一个合适的 CPU 来运行就是所谓的任务放置,以下三种情况会发生任务放置:
- fork 创建子进程
- 进程通过 exec 开始执行
- 阻塞的进程被唤醒
sched_domain.flag 中有几个跟任务放置相关的属性:SD_BALANCE_EXEC,SD_BALANCE_FORK,SD_BALANCE_WAKE,刚好对应上面三种情况

select_task_rq_fair 有一个参数是 wake_flags,select_task_rq_fair 会根据这个来进行选核:
| 标志 | 含义 | 调度器行为 |
|---|---|---|
| 0(默认) | 普通唤醒 | 选择当前 CPU 或空闲 CPU,标准负载均衡 |
| WF_SYNC | 同步唤醒 | 必须尽快运行(如 pipe_write → pipe_read),倾向于立即抢占当前任务 |
| WF_FORK | fork 创建 | 新任务优先放在父进程所在 CPU(利用缓存热度),但允许迁移 |
| WF_EXEC | exec 执行 | 镜像已更换,缓存失效,可任意选择最优 CPU(负载均衡更激进) |
| WF_MIGRATED | 已迁移 | 跳过再次迁移检查,避免乒乓效应 |
| WF_ON_CPU | 正在运行 | 被唤醒者还在其他 CPU 上跑,需特殊处理竞争条件 |
select_task_rq_fair:
select_task_rq_fair() [fair.c:8156]
│
├─ 检查 wake_flags & WF_TTWU
│ ├─ 记录 wakee: record_wakee(p)
│ ├─ 如果 WF_CURRENT_CPU 且 CPU 允许 → 返回当前 CPU
│ ├─ 如果启用 sched_energy_enabled()
│ │ └─ find_energy_efficient_cpu() → 如果找到则返回
│ └─ 设置 want_affine = !wake_wide(p) && 当前 CPU 在 cpus_ptr 中
│
├─ rcu_read_lock()
│
├─ 遍历 sched_domain (for_each_domain)
│ │
│ ├─ 如果 want_affine && SD_WAKE_AFFINE
│ │ └─ wake_affine() → 设置 new_cpu,跳出循环
│ │
│ └─ 如果 SD_BALANCE_WAKE 等标志
│ └─ 记录 sd
│
├─ 判断路径
│ │
│ ├─ [慢路径] 如果 sd 存在
│ │ └─ find_idlest_cpu(sd, p, cpu, prev_cpu, sd_flag) [fair.c:6618]
│ │ │
│ │ ├─ 遍历 sched_domain
│ │ │ └─ 查找最空闲的 sched_group
│ │ │ ├─ 计算组负载
│ │ │ └─ find_idlest_group()
│ │ │ └─ find_idlest_cpu()
│ │ │ ├─ 检查 idle CPU
│ │ │ ├─ 检查 sched_idle CPU
│ │ │ └─ 返回最空闲 CPU
│ │ │
│ │ └─ 返回选定的 CPU
│ │
│ └─ [快路径] 如果 WF_TTWU
│ └─ select_idle_sibling(p, prev_cpu, new_cpu) [fair.c:7509]
│ │
│ ├─ 1. 检查 target CPU
│ │ ├─ available_idle_cpu(target) || sched_idle_cpu(target)
│ │ └─ asym_fits_cpu() → 如果符合则返回 target
│ │
│ ├─ 2. 检查 prev CPU (cache affine)
│ │ ├─ cpus_share_cache(prev, target)
│ │ ├─ available_idle_cpu(prev) || sched_idle_cpu(prev)
│ │ └─ asym_fits_cpu() → 如果符合则返回 prev
│ │
│ ├─ 3. 检查 per-cpu kthread 情况
│ │ └─ 如果是 per-cpu kthread 且条件满足 → 返回 prev
│ │
│ ├─ 4. 检查 recent_used_cpu
│ │ ├─ cpus_share_cache(recent_used_cpu, target)
│ │ └─ 如果空闲且符合 → 返回 recent_used_cpu
│ │
│ ├─ 5. 异构容量系统
│ │ └─ select_idle_capacity(p, sd, target) [fair.c:7427]
│ │ ├─ 遍历 sd_asym_cpucapacity 域
│ │ ├─ 检查 idle CPU
│ │ └─ util_fits_cpu() → 找到最匹配容量的 CPU
│ │
│ ├─ 6. SMT 优化
│ │ ├─ 如果 sched_smt_active()
│ │ └─ select_idle_smt(p, sd, prev) [fair.c:7326]
│ │ └─ 遍历 SMT siblings
│ │ └─ 找到空闲的 sibling CPU
│ │
│ ├─ 7. 扫描 LLC domain
│ │ └─ select_idle_cpu(p, sd, has_idle_core, target) [fair.c:7374]
│ │ │
│ │ ├─ 获取扫描限制 nr (基于 SIS_UTIL feature)
│ │ │
│ │ ├─ 如果 sched_cluster_active
│ │ │ └─ 扫描 cluster 内 CPUs
│ │ │ ├─ has_idle_core: select_idle_core()
│ │ │ └─ 否则: __select_idle_cpu()
│ │ │
│ │ └─ 扫描整个 LLC domain
│ │ └─ for_each_cpu_wrap 从 target 开始
│ │ ├─ has_idle_core: select_idle_core()
│ │ │ └─ 扫描整个 core 找空闲
│ │ └─ 否则: __select_idle_cpu()
│ │ └─ 限制扫描次数 nr
│ │
│ └─ 8. fallback
│ ├─ 如果 prev_aff 有效 → 返回 prev_aff
│ ├─ 如果 recent_used_cpu 有效 → 返回 recent_used_cpu
│ └─ 否则返回 target
│
└─ rcu_read_unlock()
└─ 返回 new_cpu
这里面有三个关键函数:find_energy_efficient_cpu,find_idlest_cpu,select_idle_sibling
- EAS选核路径
find_energy_efficient_cpu()当传入参数sd_flag为SD_BALANCE_WAKE,并且系统配置key值sched_energy_present(即考虑性能和功耗的均衡),调度器就会进入EAS选核路径进行CPU的查找。这里涉及到内核中Energy Aware Scheduling(EAS)机制。总之,EAS路径在保证任务能正常运行的前提下,为任务选取使系统整体能耗最小的CPU。通常情况下,EAS总是能如愿找到符合要求的CPU,但如果当前平台不是异构系统,或者系统中存在超载(Over-utilization)的CPU,EAS就直接返回-1 find_idlest_cpu是慢速路径。传入参数sd_flag为SD_BALANCE_WAKE,且EAS没有使能或者返回-1时,如果该任务不是wake affine类型,就会进入慢速路径;传入参数sd_flag为SD_BALANCE_FORK、SD_BALANCE_EXEC时,由于此时的任务负载是不可信任的,无法预测其对系统能耗的影响,也会进入慢速路径。搜索流程:传入参数sd_flag为SD_BALANCE_WAKE,且EAS没有使能或者返回-1时,如果该任务不是wake affine类型,就会进入慢速路径;传入参数sd_flag为SD_BALANCE_FORK、SD_BALANCE_EXEC时,由于此时的任务负载是不可信任的,无法预测其对系统能耗的影响,也会进入慢速路径。select_idle_sibling是快速路径。传入参数sd_flag为SD_BALANCE_WAKE,但EAS又无法发挥作用时,若该任务为wake affine类型任务,调度器就会进入快速路径来选取放置的CPU,该路径在CPU的选择上,主要考虑共享cache且idle的CPU。在满足条件的情况下,优先选择任务上一次运行的CPU(prev cpu),hot cache的CPU是wake affine类型任务所青睐的。其次是唤醒任务的CPU(wake cpu),即waker所在的CPU。当该次唤醒为sync唤醒时(传入参数wake_flags为WF_SYNC),对wake cpu的idle状态判定将会放宽,比如waker为wake cpu唯一的任务,由于sync唤醒下的waker很快就进入阻塞状态,也可当做idle处理。
参考:
https://mp.weixin.qq.com/s/vIhKGhIRGRBiSIGpalZeHA
http://www.wowotech.net/process_management/load_balance.html
http://www.wowotech.net/process_management/load_balance_detail.html
http://www.wowotech.net/process_management/task_placement.html

浙公网安备 33010602011771号