CFS负载均衡

负载均衡的架构:
Pasted image 20260223201010

负载均衡关键数据结构——调度域和调度组

调度域: 按 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[];
};

调度组是一个环形链表

负载均衡时机

  1. 当某个 CPU 队列为空的时候,CPU 队列为空说明当前 CPU 空闲,因此可以从其他 CPU 上拉取任务(CPU进入idle)
  2. 周期性检查,scheduler_tick -> rebalance_tick ,从当前 CPU 的调度领开始依次往上遍历,检查是否满足调用 load_balance 的条件,为根据当前 CPU 的负载来决定调度频率

Pasted image 20260223181928

除了上面的情况会触发负载均衡(load balance),还有以下两种场景也会使用负载均衡的逻辑:

  1. 任务放置(task placement)
  2. 主动均衡(active upmigration)

负载的计算

sched_entity 负载的计算:

  • 基于 CPU 运行队列长度任务优先级权重(nice 值)
  • 使用 PELT(Per-Entity Load Tracking) 算法跟踪每个任务的贡献
  • 瞬时、细粒度的调度决策依据

影响负载均衡的指标:任务负载,sched group 负载,CPU算力

跟踪任务负载的原因:

  1. 判断该任务是否适合当前 CPU 算力
  2. 如果判定需要均衡,那么需要在 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的负载均衡器有下面的几个类型:

Pasted image 20260224170045

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 的均衡情况,正确来说的步骤是下面几个:

  1. 找到 domain 中最忙的 sched group
  2. 找到最忙 group 中最忙的 CPU
  3. 从选中的那个最忙的 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 的逻辑大概是:

  1. should_we_balance,决定当前 CPU 是否应该负责执行负载均衡。在多 CPU 系统中,同一个调度组(sched_group)内有多个 CPU,但只需要一个 CPU 来执行负载均衡,避免重复工作。这个函数就是用来选择"谁来做这件事"
  2. find_busiest_group -> find_busiest_queue 找到最繁忙的队列
  3. detach_tasks -> attach_tasks 分离和添加任务
  4. 处理亲和性问题 LBF_DST_PINNED(任务无法迁移到 dst_cpu,但可以迁移到组内其他 CPU) 和 LBF_ALL_PINNED(所有任务都被 CPU 亲和性绑定,无法迁移)
  5. 处理主动负载均衡 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: 继续下次均衡
  1. 当前CPU本身就是 idle 的(rq->idle_balance == true) 就不需要触发 nohz idle balance
  2. 当CPU从idle状态醒来,第一个tick会更新全局变量nohz的状态以及sched domain的cpu busy状态。虽然nohz idle balance本质上是tick balance,但是它会发IPI,会唤醒idle的cpu,带来额外的开销,所以还是要控制触发nohz idle balance的频次。
  3. nr_cpus和idle_cpus_mask这两个成员可以让调度器了解当前系统idle CPU的情况,从而选择合适的CPU来执行nohz idle balance。如果系统中根本没有idle cpu,那么也就没有必要触发nohz idle load balance了。
  4. nohz idle balance有两部分的功能:(1)更新idle cpu上的blocked load(2)负载均衡。可以只更新blocked load,但是负载均衡必须要包括更新blocked load功能。如果当前idle的cpu上有需要衰减的负载,那么标记之。
  5. 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的时间点。
  6. 总之就是根据各种情况确认是否发起 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 来运行就是所谓的任务放置,以下三种情况会发生任务放置:

  1. fork 创建子进程
  2. 进程通过 exec 开始执行
  3. 阻塞的进程被唤醒

sched_domain.flag 中有几个跟任务放置相关的属性:SD_BALANCE_EXEC,SD_BALANCE_FORK,SD_BALANCE_WAKE,刚好对应上面三种情况

Pasted image 20260225112857

select_task_rq_fair 有一个参数是 wake_flags,select_task_rq_fair 会根据这个来进行选核:

标志 含义 调度器行为
0(默认) 普通唤醒 选择当前 CPU 或空闲 CPU,标准负载均衡
WF_SYNC 同步唤醒 必须尽快运行(如 pipe_writepipe_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_cpufind_idlest_cpuselect_idle_sibling

  1. 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
  2. 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时,由于此时的任务负载是不可信任的,无法预测其对系统能耗的影响,也会进入慢速路径。
  3. 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

posted @ 2026-02-25 11:02  uran0sh  阅读(33)  评论(0)    收藏  举报