Linux-进程的管理与调度29(基于6.1内核)---CFS调度器之唤醒抢占
一、 wake_affine 机制
1.1、 引入 WAKE_AFFINE 的背景
当进程被唤醒的时候(try_to_wake_up),需要用 select_task_rq_fair为该 task 选择一个合适的CPU(runqueue), 接着会通过 check_preempt_wakeup 去看被唤醒的进程是否要抢占所在 CPU 的当前进程。
这个选核的过程一般称之为 BALANCE_WAKE. 为了能清楚的描述这个场景,定义:
-
执行唤醒的那个进程是 waker。
-
而被唤醒的进程是 wakee。
Wakeup有两种,一种是sync wakeup,另外一种是non-sync wakeup。
-
所谓 sync wakeup 就是 waker 在唤醒 wakee 的时候就已经知道自己很快就进入 sleep 状态,而在调用 try_to_wake_up 的时候最好不要进行抢占,因为 waker 很快就主动发起调度了。此外,一般而言,waker和wakee会有一定的亲和性(例如它们通过share memory进行通信),在SMP场景下,waker和wakee调度在一个CPU上执行的时候往往可以获取较佳的性能。而如果在try_to_wake_up的时候就进行调度,这时候wakee往往会调度到系统中其他空闲的CPU上去。这时候,通过sync wakeup,我们往往可以避免不必要的CPU bouncing。
-
对于non-sync wakeup而言,waker和wakee没有上面描述的同步关系,waker在唤醒wakee之后,它们之间是独立运作,因此在唤醒的时候就可以尝试去触发一次调度。
当然,也不是说sync wakeup就一定不调度,假设waker在CPU A上唤醒wakee,而根据wakee进程的cpus_allowed成员发现它根本不能在CPU A上调度执行,那么管他sync不sync,这时候都需要去尝试调度(调用reschedule_idle函数),反正waker和wakee命中注定是天各一方(在不同的CPU上执行)。
select_task_rq_fair 的原型如下: kernel/sched/fair.c
int select_task_rq_fair(struct task_struct *p, int prev_cpu, int sd_flag, int wake_flags)
在 try_to_wake_up 场景其中 p 是待唤醒进程, prev_cpu 是进程上次运行的 CPU, 一般 sd_flag 是 BALANCE_WAKE, 因此其实wakeup 的过程也可以理解为一次主动 BALANCE 的过程, 成为 WAKEUP BALANCE, 只不过只是为一个进程选择唤醒到的 CPU. wake_flags 用于表示是 sync wakeup 还是 non-sync wakeup。
我们首先看看UP上的情况。这时候waker和wakee在同一个CPU上运行(当然系统中也只有一个CPU,哈哈),这时候谁能抢占CPU资源完全取决于waker和wakee的动态优先级(调度类优先级, 或者 CFS 的 vruntime 等, 依照进程的调度类而定),如果wakee的动态优先级大于waker,那么就标记waker的need_resched标志,并在调度点到来的时候调用schedule函数进行调度。
SMP情况下,由于系统的CPU资源比较多,waker和wakee没有必要争个你死我活,wakee其实也可以选择去其他CPU执行,但是这时候要做决策:
-
因为跑到 prev_cpu 上, 那么之前如果 cache 还是 hot 的是很有意义的;
-
同时按照之前的假设 waker 和 wakee 之间有资源共享, 那么唤醒到 waker CPU 上也有好处;
-
如果 prev_cpu, waker cpu 都很忙, 那放上来可以并不一定好, 唤醒延迟之类的都是一个考量。
1.2、 WAKE_AFFINE 机制简介
select_task_rq_fair选核其实是一个优选的过程, 通常会有限选择一个 cache-miss 等开销最小的一个:
-
根据 wake_affine 选择调度域并确定 new_cpu。
-
根据调度域及其调度域参数选择兄弟 idle cpu 根据调度域及其调度域参数选择兄弟 idle cpu。
-
根据调度域选择最深idle的cpu根据调度域选择最深idle的cpu find_idest_cpu。
在进程唤醒的过程中为进程选核时, wake_affine 倾向于将被唤醒进程尽可能安排在 waking CPU 上, 这样考虑的原因是: 有唤醒关系的进程是相互关联的, 尽可能地运行在具有 cache 共享的调度域中, 这样可以获得一些 chache-hit 带来的性能提升. 这时 wake_affine 的初衷, 但是这也是一把双刃剑。
将 wakee 都唤醒在 waker CPU 上, 必然造成 waker 和 wakee 的资源竞争. 特别是对于 1:N 的任务模型, wake_affine 会导致 waker 进程饥饿。
62470419e993f8d9d93db0effd3af4296ecb79a5 sched: Implement smarter wake-affine logic
因此后来 (COMMIT 62470419e993 “sched: Implement smarter wake-affine logic”), 实现了一种智能 wake-affine 的优化机制. 用于 wake_flips 的巧妙方式, 识别出 1:N 等复杂唤醒模型, 只有在认为 wake_affine 能提升性能时(want_affine)才进行 wake_affine。
二、 wake_affine 机制分析
根据 want_affine 变量选择调度域并确定 new_cpu:
-
进程p的调度域参数设置了SD_BALANCE_WAKE。
-
当前cpu的唤醒次数没有超标。
-
当前task p消耗的capacity * 1138小于min_cap * 1024。
-
当前cpu在task p的cpu亲和数里面的一个。
kernel/sched/fair.c
/*
* select_task_rq_fair: Select target runqueue for the waking task in domains
* that have the relevant SD flag set. In practice, this is SD_BALANCE_WAKE,
* SD_BALANCE_FORK, or SD_BALANCE_EXEC.
*
* Balances load by selecting the idlest CPU in the idlest group, or under
* certain conditions an idle sibling CPU if the domain has SD_WAKE_AFFINE set.
*
* Returns the target CPU number.
*/
static int
select_task_rq_fair(struct task_struct *p, int prev_cpu, int wake_flags)
{
int sync = (wake_flags & WF_SYNC) && !(current->flags & PF_EXITING);
struct sched_domain *tmp, *sd = NULL;
int cpu = smp_processor_id();
int new_cpu = prev_cpu;
int want_affine = 0;
/* SD_flags and WF_flags share the first nibble */
int sd_flag = wake_flags & 0xF;
/*
* required for stable ->cpus_allowed
*/
lockdep_assert_held(&p->pi_lock);
if (wake_flags & WF_TTWU) {
record_wakee(p);
if ((wake_flags & WF_CURRENT_CPU) &&
cpumask_test_cpu(cpu, p->cpus_ptr))
return cpu;
if (sched_energy_enabled()) {
new_cpu = find_energy_efficient_cpu(p, prev_cpu);
if (new_cpu >= 0)
return new_cpu;
new_cpu = prev_cpu;
}
want_affine = !wake_wide(p) && cpumask_test_cpu(cpu, p->cpus_ptr);
}
rcu_read_lock();
for_each_domain(cpu, tmp) {
/*
* If both 'cpu' and 'prev_cpu' are part of this domain,
* cpu is a valid SD_WAKE_AFFINE target.
*/
if (want_affine && (tmp->flags & SD_WAKE_AFFINE) &&
cpumask_test_cpu(prev_cpu, sched_domain_span(tmp))) {
if (cpu != prev_cpu)
new_cpu = wake_affine(tmp, p, cpu, prev_cpu, sync);
sd = NULL; /* Prefer wake_affine over balance flags */
break;
}
/*
* Usually only true for WF_EXEC and WF_FORK, as sched_domains
* usually do not have SD_BALANCE_WAKE set. That means wakeup
* will usually go to the fast path.
*/
if (tmp->flags & sd_flag)
sd = tmp;
else if (!want_affine)
break;
}
if (unlikely(sd)) {
/* Slow path */
new_cpu = find_idlest_cpu(sd, p, cpu, prev_cpu, sd_flag);
} else if (wake_flags & WF_TTWU) { /* XXX always ? */
/* Fast path */
new_cpu = select_idle_sibling(p, prev_cpu, new_cpu);
}
rcu_read_unlock();
return new_cpu;
}
- wake_wide 和 wake_cap 为调度器提供决策, 当前进程是否符合 wake_affine 的决策模型. 如果他们返回 1, 则说明如果采用 wake_affine 进行决策, 大概率是无效的或者会降低性能, 则调度器就不会 want_affine 了.
want_affine = !wake_wide(p) && !wake_cap(p, cpu, prev_cpu) &&
cpumask_test_cpu(cpu, &p->cpus_allowed);
wake_wide 检查当前cpu的唤醒关系符合 wake_affine 模型。
wake_cap 检查当前 task p 消耗的 CPU capacity 没有超出当前 CPU 的限制。
task p 可以在当前 CPU 上运行。
- wake_affine 则为目标进程选择最合适运行的 wake CPU。
2.1、 want_affine
有 wakeup 关系的进程都是相互关联的进程, 那么大概率 waker 和 wakee 之间有一些数据共享, 这些数据可能是 waker 进程刚刚准备好的, 还在 cache 里面, 那么把它唤醒到 waking CPU, 就能充分利用这些在 cache 中的数据. 但是另外一方面, waker 之前在 prev CPU 上运行, 那么也是 cache-hot 的, 把它迁移到 waking CPU 上, 那么 prev CPU 上那些 cache 就有可能失效, 因此如果 waker 和 wakee 之间没有数据共享或者共享的数据没那么多, 那么wake_affine 直接迁移到 waking CPU 上反而是不合适的。
内核引入 wake_affine 的初衷就是识别什么时候要将 wakee 唤醒到 waking CPU, 什么时候不需要. 这个判断由 want_affine 通过 wake_cap() 和 wake_wide() 来完成。
通过在 struct task_struct 中增加两个成员:上次唤醒的进程 last_wakee, 和累积唤醒翻转计数器. 每当 waker 尝试唤醒 wakee 的时候, 就通过 record_wakee 来更新统计计数。
在 select_task_rq_fair 开始的时候, 如果发现是 SD_BALANCE_WAKE, 则先会 record_wakee 统计 current 的 wakee_flips。
static int
select_task_rq_fair(struct task_struct *p, int prev_cpu, int sd_flag, int wake_flags)
{
if (sd_flag & SD_BALANCE_WAKE) {
record_wakee(p);
wakee_flips 表示了当前进程作为 waker 时翻转(切换)其唤醒目标的次数, 所以高 wakee_flips 值意味着任务不止一个唤醒, 数字越大, 说明当前进程又不止一个 wakee, 而且唤醒频率越比较高. 且当前进程的 wakerr 数目 < wakee_flips。
比如一个进程 P 连续一段时间的唤醒序列为: A, A, A, A, 那么由于没有翻转, 那么他的 wakee_flips 就始终为 1。
static void record_wakee(struct task_struct *p)
{
/*
* Only decay a single time; tasks that have less then 1 wakeup per
* jiffy will not have built up many flips.
*/
if (time_after(jiffies, current->wakee_flip_decay_ts + HZ)) {
current->wakee_flips >>= 1;
current->wakee_flip_decay_ts = jiffies;
}
if (current->last_wakee != p) {
current->last_wakee = p;
current->wakee_flips++;
}
}
wakee_flips 有一定的衰减期, 如果过了 1s (即 1 个 HZ 的时间), 那么 wakee_flips 就衰减为原来的 1/2, 这就类似于 PELT 的指数衰减, Ns 前的 wakee_flips 的占比大概是当前这一个窗口的 1 / 2^N;
全局变量jiffies用来记录自系统启动以来产生的节拍的总数(经过了多少tick)。
在 Linux 内核中,jiffies 是一个全局变量,用于记录系统自启动以来经过的节拍(tick)的数量。它是内核中用于时间管理的核心概念之一。
jiffies 的定义和作用
-
定义:
jiffies是一个全局变量,通常定义为unsigned long类型。它表示系统自启动以来经过的节拍(tick)总数,通常由内核的定时器中断 (timer interrupt) 更新。 -
作用:
jiffies提供了一个全局的、精确到系统时间片的时钟,内核可以使用它来进行时间相关的操作,比如延时、超时检测、定时器管理等。
如何计算 jiffies
jiffies 由定时器中断(也叫做 "tick")驱动。定时器中断是一个固定频率的时钟中断,通常每秒产生一定数量的时钟节拍(ticks)。这一频率通常由内核中的配置项 HZ 决定。HZ 代表系统每秒的定时器中断次数。
HZ:HZ的值通常是 100、250 或 1000,具体取决于系统配置。例如:HZ = 100:每秒 100 次中断(即每 10 毫秒触发一次中断)HZ = 1000:每秒 1000 次中断(即每 1 毫秒触发一次中断)
因此,jiffies 的值通常按以下方式增加:
- 每经过 1 个 tick,
jiffies的值就增加 1。 - 在
HZ = 100的系统中,每秒jiffies会增加 100。 - 在
HZ = 1000的系统中,每秒jiffies会增加 1000。
使用 jiffies 的示例
-
获取当前时间戳:
jiffies表示系统自启动以来经过的 ticks 数量,如果需要获取当前的系统时间戳,可以直接读取jiffies的值。 -
时间延迟和超时:
- 内核可以利用
jiffies来实现超时检测。例如,内核中的定时器机制就依赖于jiffies来检查任务是否超时。 - 延迟操作时,可以通过将当前的
jiffies值与目标的时间戳进行比较来判断是否达到指定的延迟时间。
- 内核可以利用
-
jiffies与HZ的关系: 如果HZ = 100,那么每秒会增加 100 次jiffies,每次jiffies增加 1 对应的时间间隔就是 10 毫秒。如果HZ = 1000,那么每秒会增加 1000 次jiffies,每次jiffies增加 1 对应的时间间隔就是 1 毫秒。
jiffies 的溢出
由于 jiffies 是一个 unsigned long 类型的变量,它的值会在达到最大值后回绕。unsigned long 的最大值通常为 ULONG_MAX,在大多数系统中,这是一个很大的值。
溢出通常不是问题,因为 Linux 内核会在内部正确处理 jiffies 的回绕。例如,Linux 内核会使用 time_after() 和 time_before() 等宏来避免 jiffies 回绕带来的问题。
当前 current 正在为 wakeup p, 并为 p 选择一个合适的 CPU. 那么 wake_wide 就用来检查 current 和 p 之间是否适合 wake_affine 所关心的 waker/wakee 模型.
wake_wide 返回 0, 表示 wake_affine 是有效的. 否则返回 1, 表示这两个进程不适合用 wake_affine.
那么什么时候, wake_wide 返回 1 ?kernel/sched/fair.c
/*
* Detect M:N waker/wakee relationships via a switching-frequency heuristic.
*
* A waker of many should wake a different task than the one last awakened
* at a frequency roughly N times higher than one of its wakees.
*
* In order to determine whether we should let the load spread vs consolidating
* to shared cache, we look for a minimum 'flip' frequency of llc_size in one
* partner, and a factor of lls_size higher frequency in the other.
*
* With both conditions met, we can be relatively sure that the relationship is
* non-monogamous, with partner count exceeding socket size.
*
* Waker/wakee being client/server, worker/dispatcher, interrupt source or
* whatever is irrelevant, spread criteria is apparent partner count exceeds
* socket size.
*/
static int wake_wide(struct task_struct *p)
{
unsigned int master = current->wakee_flips;
unsigned int slave = p->wakee_flips;
int factor = this_cpu_read(sd_llc_size);
if (master < slave)
swap(master, slave);
if (slave < factor || master < slave * factor)
return 0;
return 1;
}
wake_affine 在决策的时候, 要参考 wakee_flips
- 将 wakee_flips 值大的 wakee 唤醒到临近的 CPU, 可能有利于系统其他进程的唤醒, 同样这也意味着, waker 将面临残酷的竞争.
- 此外, 如果 waker 也有一个很高的 wakee_flips, 那意味着多个任务依赖它去唤醒, 然后 1 中造成的 waker 的更高延迟会对这些唤醒造成负面影响, 因此一个高 wakee_flips 的 waker 再去将另外一个高 wakee_flips 的 wakee 唤醒到本地的 CPU 上, 是非常不明智的决策. 因此, 当 waker-> wakee_flips / wakee-> wakee_flips 变得越来越高时, 进行 wake_affine 操作的成本会很高.
理解了这层含义, 那wake_wide 的算法就明晰了. 如下情况认为决策是有效的 wake_affine。
factor = this_cpu_read(sd_llc_size); 这个因子表示了在当前 NODE 上能够共享 cache 的 CPU 数目(或者说当前sched_domain 中 CPU 的数目), 一个 sched_domain 中, 共享 chache 的 CPU 越多(比如 X86 上一个物理 CPU 上包含多个逻辑 CPU), factor 就越大. 那么在 wake_affine 中的影响就是 wake_wide 返回 0 的概率更大, 那么 wake_affine 的结果有效的概率就更大. 因为有跟多的临近 CPU 可以选择, 这些 CPU 之间 cache 共享有优势。
| 条件 | 描述 |
|---|---|
| slave < factor | 即如果 wakee->wakee_flips < factor, 则说明当前进程的唤醒切换不那么频繁, 即使当前进程有 wakee_flips 个 wakee, 当前 sched_domain 也完全能装的下他们. |
| master < slave * factor | 即 master/slave < factor, 两个 waker wakee_flips 的比值小于 factor, 那么这种情况下, 进行 wake_affine 的成本可控. |
由于目前有一些 CPU 都是属于性能异构的 CPU(比如 ARM64 的 big.LITTLE 等), 不同的核 capacity 会差很多. wake_cap 会先看待选择的进程是否
https://elixir.bootlin.com/linux/v5.6.13/source/kernel/sched/fair.c#L6128
/*
* Disable WAKE_AFFINE in the case where task @p doesn't fit in the
* capacity of either the waking CPU @cpu or the previous CPU @prev_cpu.
*
* In that case WAKE_AFFINE doesn't make sense and we'll let
* BALANCE_WAKE sort things out.
*/
static int wake_cap(struct task_struct *p, int cpu, int prev_cpu)
{
long min_cap, max_cap;
if (!static_branch_unlikely(&sched_asym_cpucapacity))
return 0;
min_cap = min(capacity_orig_of(prev_cpu), capacity_orig_of(cpu));
max_cap = cpu_rq(cpu)->rd->max_cpu_capacity;
/* Minimum capacity is close to max, no need to abort wake_affine */
if (max_cap - min_cap < max_cap >> 3)
return 0;
/* Bring task utilization in sync with prev_cpu */
sync_entity_load_avg(&p->se);
return !task_fits_capacity(p, min_cap);
}
注意在 sched/fair: Capacity aware wakeup rework 合入之后, 通过 select_idle_sibling-=>elect_idle_capacity 让 wakeup 感知了 capacity, 因此 原生的 wakeup 路径无需再做 capacity 相关的处理, 因此 wake_cap 就被干掉了. 参见sched/fair: Remove wake_cap()
2.3、 wake_affine
如果 want_affine 发现对当前 wakee 进行 wake_affine 是有意义的, 那么就会为当前进程选择一个能尽快运行的 CPU. 它总是倾向于选择 waking CPU(this_cpu) 以及 prev_cpu.
其中
-
wake_affine_idle 则看 prev_cpu 以及 this_cpu 是不是处于 cache 亲和的以及是不是idle 状态, 这样的 CPU
往往是最佳的. -
wake_affine_weight 则进一步考虑进程的负载信息以及调度的延迟信息.
/*
* The purpose of wake_affine() is to quickly determine on which CPU we can run
* soonest. For the purpose of speed we only consider the waking and previous
* CPU.
*
* wake_affine_idle() - only considers 'now', it check if the waking CPU is
* cache-affine and is (or will be) idle.
*
* wake_affine_weight() - considers the weight to reflect the average
* scheduling latency of the CPUs. This seems to work
* for the overloaded case.
*/
static int wake_affine(struct sched_domain *sd, struct task_struct *p,
int this_cpu, int prev_cpu, int sync)
{
int target = nr_cpumask_bits;
if (sched_feat(WA_IDLE))
target = wake_affine_idle(this_cpu, prev_cpu, sync);
if (sched_feat(WA_WEIGHT) && target == nr_cpumask_bits)
target = wake_affine_weight(sd, p, this_cpu, prev_cpu, sync);
schedstat_inc(p->se.statistics.nr_wakeups_affine_attempts);
if (target == nr_cpumask_bits)
return prev_cpu;
schedstat_inc(sd->ttwu_move_affine);
schedstat_inc(p->se.statistics.nr_wakeups_affine);
return target;
}
wake_affine 函数源码分析之前, 需要先知道三个load的计算方式如下:
source_load(int cpu, int type)
target_load(int cpu, int type)target_load(int cpu, int type)
effective_load(struct task_group *tg, int cpu, long wl, long wg)
根据调度类和 “nice” 值, 对迁移源 CPU 和目的 CPU 的负载 source_load 和 target_load 进行估计.
对于 source_load 我们采用保守的方式进行估计, 对于 target_load 则倾向于偏激. 因此当 type 传入的值非 0 时, source_load 返回最小值, 而 target_load 返回最大值. 当 type == 0 时, 将直接返回 weighted_cpuload
#https://elixir.bootlin.com/linux/v4.14.14/source/kernel/sched/fair.c#5258
/*
* Return a low guess at the load of a migration-source CPU weighted
* according to the scheduling class and "nice" value.
*
* We want to under-estimate the load of migration sources, to
* balance conservatively.
*/
static unsigned long source_load(int cpu, int type)
{
struct rq *rq = cpu_rq(cpu);
unsigned long total = weighted_cpuload(rq);
if (type == 0 || !sched_feat(LB_BIAS))
return total;
return min(rq->cpu_load[type-1], total);
}
#https://elixir.bootlin.com/linux/v4.14.14/source/kernel/sched/fair.c#5280
/*
* Return a high guess at the load of a migration-target CPU weighted
* according to the scheduling class and "nice" value.
*/
static unsigned long target_load(int cpu, int type)
{
struct rq *rq = cpu_rq(cpu);
unsigned long total = weighted_cpuload(rq);
if (type == 0 || !sched_feat(LB_BIAS))
return total;
return max(rq->cpu_load[type-1], total);
}
#https://elixir.bootlin.com/linux/v4.14.14/source/kernel/sched/fair.c#5139
/* Used instead of source_load when we know the type == 0 */
static unsigned long weighted_cpuload(struct rq *rq)
{
return cfs_rq_runnable_load_avg(&rq->cfs);
}
static int
wake_affine_idle(int this_cpu, int prev_cpu, int sync)
{
/*
* If this_cpu is idle, it implies the wakeup is from interrupt
* context. Only allow the move if cache is shared. Otherwise an
* interrupt intensive workload could force all tasks onto one
* node depending on the IO topology or IRQ affinity settings.
*
* If the prev_cpu is idle and cache affine then avoid a migration.
* There is no guarantee that the cache hot data from an interrupt
* is more important than cache hot data on the prev_cpu and from
* a cpufreq perspective, it's better to have higher utilisation
* on one CPU.
*/
if (available_idle_cpu(this_cpu) && cpus_share_cache(this_cpu, prev_cpu))
return available_idle_cpu(prev_cpu) ? prev_cpu : this_cpu;
if (sync && cpu_rq(this_cpu)->nr_running == 1)
return this_cpu;
return nr_cpumask_bits;
}
如果 this_cpu 空闲, 则意味着唤醒来自中断上下文. 仅在 this_cpu 和 prev_cpu 有共享缓存时允许移动. 否则, 中断密集型工作负载可能会将所有任务强制到一个节点, 具体取决于IO拓扑或IRQ亲缘关系设置. 同时如果 this_cpu 也是空闲的, 优先 this_cpu.
另外没有证据保证来自中断的缓存热数据比 prev_cpu 上的缓存热数据更重要, 并且从cpufreq的角度来看, 最好在一个CPU上获得更高的利用率.
wake_affine_weight 会重新计算 wakeup CPU 和 prev CPU 的负载情况, 如果 wakeup CPU 的负载加上唤醒进程的负载比 prev CPU 的负载小, 那么 wakeup CPU 是可以唤醒进程.
static int
wake_affine_weight(struct sched_domain *sd, struct task_struct *p,
int this_cpu, int prev_cpu, int sync)
{
s64 this_eff_load, prev_eff_load;
unsigned long task_load;
this_eff_load = target_load(this_cpu, sd->wake_idx);
if (sync) {
unsigned long current_load = task_h_load(current);
if (current_load > this_eff_load)
return this_cpu;
this_eff_load -= current_load;
}
task_load = task_h_load(p);
this_eff_load += task_load;
if (sched_feat(WA_BIAS))
this_eff_load *= 100;
this_eff_load *= capacity_of(prev_cpu);
prev_eff_load = source_load(prev_cpu, sd->wake_idx);
prev_eff_load -= task_load;
if (sched_feat(WA_BIAS))
prev_eff_load *= 100 + (sd->imbalance_pct - 100) / 2;
prev_eff_load *= capacity_of(this_cpu);
/*
* If sync, adjust the weight of prev_eff_load such that if
* prev_eff == this_eff that select_idle_sibling() will consider
* stacking the wakee on top of the waker if no other CPU is
* idle.
*/
if (sync)
prev_eff_load += 1;
return this_eff_load < prev_eff_load ? this_cpu : nr_cpumask_bits;
}
假设将进程从 prev CPU 迁移到了 wakeup CPU, 那么 this_eff_load 记录了迁移后 wakeup CPU 的负载, 那么 prev_eff_load 则是迁移后 prev CPU 的负载。
eff_load 的计算方式?
eff_load(有效负载)通常指的是系统中某个计算资源(如 CPU、内存、磁盘等)的负载状况,它表示系统当前的资源利用效率或负载程度。具体的计算方式可能根据不同的上下文有所不同,但在一些系统监控、调度或负载均衡算法中,eff_load 可以通过以下几种常见方式来计算。
CPU 负载计算(Efficient Load in CPU Scheduling)
在操作系统中,尤其是 CPU 调度相关的上下文中,eff_load 可以用来衡量 CPU 的负载程度,反映系统在某段时间内的工作密集度。通常,它是通过以下方式来计算的:
CPU Load Average
在 Linux 中,系统的负载平均值(load average)通常表示为过去 1、5、15 分钟的 CPU 负载,它与 eff_load 可能有一定的关联。负载平均值的计算考虑了正在运行和等待 CPU 的进程数目。
eff_load 计算的方式(基于活动进程数量)
一种常见的计算方式是根据 正在运行的进程数 和 等待队列的长度 来计算 CPU 负载:
eff_load=当前正在运行的进程数系统中可以并行处理的进程数\text{eff\_load} = \frac{\text{当前正在运行的进程数}}{\text{系统中可以并行处理的进程数}} eff_load=系统中可以并行处理的进程数当前正在运行的进程数
例如,如果系统可以同时运行 4 个进程(四核 CPU),而当前正在运行 8 个进程,那么 eff_load 为 2,表示负载是 CPU 处理能力的 2 倍。
负载平均值计算公式
在计算机科学中,负载平均值通常使用加权平均公式来计算,它衡量了进程在 CPU 资源的占用情况。例如,Linux 使用以下公式来计算负载平均值:
load_avg(t)=load(t−1)⋅e(−1/t)1+e(−1/t)\text{load\_avg}(t) = \frac{\text{load}(t-1) \cdot e^{(-1/t)}}{1 + e^{(-1/t)}} load_avg(t)=1+e(−1/t)load(t−1)⋅e(−1/t)
这可以通过观察负载的变化来计算 eff_load,其中 t 代表时间段。
内存负载的 eff_load 计算
在一些系统中,eff_load 还可以用来表示内存的负载。它可能通过以下的方式来计算:
eff_load=used_memorytotal_memory\text{eff\_load} = \frac{\text{used\_memory}}{\text{total\_memory}} eff_load=total_memoryused_memory
这里,used_memory 是当前使用的内存量,total_memory 是系统的总内存量。
磁盘负载的 eff_load 计算
对于磁盘或 I/O 负载,eff_load 可能是基于磁盘的使用情况(如读写速度、等待队列长度等)来计算的。常见的计算方式包括:
eff_load=IO wait timetotal time\text{eff\_load} = \frac{\text{IO wait time}}{\text{total time}} eff_load=total timeIO wait time
这表示了磁盘操作过程中等待 I/O 完成的时间占整个操作时间的比重。
2.4、 wake_affine 演进
wake_affine 是一个 Linux 内核中的函数,主要用于 CPU 调度相关的负载均衡操作。它的作用是确定一个进程应该在哪个 CPU 核心上执行,尤其在 SMP(对称多处理)系统中,内核需要确保系统的负载尽可能均匀地分配到多个 CPU 核心上。wake_affine 这个函数的演进涉及到调度器对 CPU 负载的平衡、进程调度的智能化和效率优化的多个方面。
初期版本:简单的负载均衡
在早期的 Linux 内核中,wake_affine 函数的主要目的是在进程从阻塞状态唤醒后,选择一个最合适的 CPU 核心来运行它。最初的负载均衡策略相对简单,主要基于以下几个因素:
- 进程亲和性(CPU Affinity):如果一个进程在某个 CPU 上运行了一段时间,它的状态和缓存可能与该 CPU 相关,因此该进程有较强的“亲和性”,即倾向于继续在相同的 CPU 上运行。
- 负载均衡的简单尝试:在早期,Linux 调度器的负载均衡策略并不非常复杂,简单的策略是将唤醒的进程安排到当前运行负载最轻的 CPU 上。
引入 wake_affine 函数
随着多核 CPU 系统的普及,Linux 调度器需要处理更多复杂的调度问题,wake_affine 在其中起到了关键作用。这个函数的主要目标是智能地选择最合适的 CPU 核心,避免在不同 CPU 核心之间频繁地迁移进程,特别是在 SMP 系统上。
-
避免频繁的 CPU 迁移:早期的负载均衡策略可能导致进程频繁迁移,这样会影响缓存的局部性,造成性能下降。
wake_affine函数开始采用更为智能的亲和性策略,尽可能保持进程在原有 CPU 上运行。 -
亲和性和 CPU 负载:
wake_affine会考虑多个因素,如进程当前的运行状态、CPU 核心的负载、以及 CPU 核心的空闲情况。它会优先选择与该进程亲和性较高的 CPU 来执行,以减少进程切换和缓存失效的成本。
负载均衡和亲和性策略的改进
随着内核版本的演进,wake_affine 函数也得到了改进,特别是在进程调度、负载均衡和 CPU 核心亲和性方面。以下是一些关键的改进方向:
-
增强的负载均衡算法:随着多核 CPU 的发展,内核在负载均衡方面的算法变得更加复杂。
wake_affine开始考虑更多的因素,例如,多个 CPU 核心的负载、CPU 核心之间的调度平衡、进程的缓存局部性等。 -
智能的进程迁移策略:新的版本优化了
wake_affine,使其在负载均衡时不仅考虑 CPU 核心的空闲程度,还会考虑缓存的局部性(即该进程之前在哪个 CPU 核心上运行),尽量避免进程迁移到过远的 CPU。 -
NUMA 体系结构的支持:随着非一致性内存访问(NUMA)架构的引入,
wake_affine还需要考虑不同内存区域对 CPU 的影响。NUMA 系统中,CPU 与其本地内存之间的访问速度远快于远程内存,wake_affine会优先选择与进程当前内存访问模式更匹配的 CPU 核心。
进程调度和负载均衡的动态调整
为了进一步提高多核 CPU 系统的调度效率,wake_affine 和调度器的其他部分开始动态调整负载均衡策略。通过实时跟踪 CPU 的负载、进程的运行状态以及 CPU 亲和性,wake_affine 能够根据当前的系统状态灵活地选择进程的执行核心,确保负载均衡和进程性能的最优化。
-
系统负载监控:随着内核版本的更新,
wake_affine开始依赖更精确的负载监控机制,以便判断进程在不同 CPU 核心上运行的性能表现。例如,内核会实时评估每个 CPU 核心的空闲程度、负载历史、运行的进程数量等,来决定唤醒进程的最优 CPU。 -
高效的 CPU 核心选择:内核对 CPU 核心选择的策略逐渐细化,不再仅仅依赖单一的“负载最轻”原则,还会考虑 CPU 核心间的调度延迟、缓存局部性等因素,以提高性能。
现代化的进程调度优化
在现代的 Linux 内核中,wake_affine 已经和其他调度函数(如 load_balance、wake_up_new_task 等)配合使用,形成了一个综合的负载均衡机制。这个机制不仅涉及 CPU 核心间的负载平衡,还涉及进程的亲和性、NUMA 架构支持、CPU 缓存局部性等多个方面。具体的演进包括:
-
能源效率的考虑:在一些新版本的内核中,
wake_affine的调度决策还考虑了能源效率。调度器可能优先选择功耗更低的 CPU 核心,尤其是在节能模式下。 -
实时调度支持:对于实时任务,
wake_affine会根据任务的优先级、延迟敏感性等特点,智能地选择合适的 CPU 核心,以确保任务的响应性和高效执行。
三、 wake_affine 对 select_task_rq_fair 的影响.
在唤醒CFS 进程的时候通过 select_task_rq_fair 来为进程选择一个最适合的 CPU.
try_to_wake_up
cpu = select_task_rq(p, p->wake_cpu, SD_BALANCE_WAKE, wake_flags);
那么在 wake_affine 机制的参与下, 选核流程是一个什么样的逻辑呢?
- 首先 sd_flag 必须配置 SD_BALANCE_WAKE 才会去做 wake_affine, 如果是 energy aware, EAS 会先通过 find_energy_efficient_cpu 选核。
- 先 record_wakee 更新 wake affine 统计信息, 接着通过 wake_cap 和 wake_wide 看这次对进程的唤醒是不是 want_affine 的。
- 接着从 waker CPU 开始向上遍历调度域。
如果是 want_affine, 则先通过 wake_affine 在当前调度域 tmp 中是从 prev_cpu 和 waker CPU 以及上次的 waker CPU( recent_used_cpu) 中优选一个合适的 new CPU, 待会选核的时候, 就会从走快速路径 select_idle_sibling 中从 prev_cpu 和 new cpu 中优选一个 CPU. 同时设置 recent_used_cpu 为当前 waker CPU。
否则, 如果是 want_affine, 但是 tmp 中没找到满足要求的 CPU, 则最终循环结束条件为 !(tmp->flag & SD_LOAD_BALANCE), 同样如果不是 want_affine 的, 则最终循环结束条件为 !(tmp->flag & SD_LOAD_BALANCE) 或者 !tmp->flags & sd_flag,则 sd 指向了配置了 SD_LOAD_BALANCE 和 sd_flag 的最高那个层次的 sd, 这个时候会通过 find_idlest_cpu 慢速路径选择, 从这个 sd 中选择一个 idle 或者负载最小的 CPU。
只要 wakeup 的时候, 会通过 wake_affine, 然后通过 select_idle_sibling 来选核.
其他情况下, 都是找到满足 sd_flags 的最高层次 sd, 然后通过 find_idlest_cpu 在这个调度域 sd 中去选择一个最空闲的 CPU。
1768

被折叠的 条评论
为什么被折叠?



