commit | patchwork | lkml |
---|---|---|
63b0e9edceec sched/fair: Beef up wake_wide | https://lore.kernel.org/patchwork/patch/576823 | https://lkml.org/lkml/2015/7/8/40 |
理解了这层含义, 那我们 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 的成本可控. |
- Overview
以下内容摘自https://kernel.taobao.org/2017/08/Reconsidering-the-schedulers-wake_wide-heuristic/在多核系统上,内核的调度子系统不仅仅要去决策下一个任务跑啥,还得决策下一个任务跑在哪个CPU上。通常情况下,这种决策被称为“启发式搜索算法”,即一种基于经验法则的最佳实践模式。2015年,有一个关键的“Task-placement heuristic(任务放置启发式搜索算法)”出现了,不过最近的一次讨论让人们开始重新审视它的合理性。
调度器唤醒这事儿总是在发生:任务经常会等待一个事件(比如:定时器过期,POSIX信号,futex()系统调用等等);这类事件发生的时候,任务被唤醒并继续执行。调度器的任务是找到最适合这个刚被唤醒的任务运行的CPU,所谓核心选得好,任务回家早,选对正确的核心对应用的性能极其关键。一些消息传递型的负载,如果运行在同一个CPU上,性能会更好。比如说 pipetest,它有一对收发任务轮流执行,这对任务永远不用也不会并行执行,这样如果他们的数据都保存在一个CPU的cache里,性能就会好得多。
不过在现实生活中,应用程序不会有这么密集的通信,而大多数负载也不会这么轮流发消息,所以一般的高并发的应用都是在响应外部输入后发出消息,而发出消息的时机都是随机的。这里一个非常典型的例子是生产者消费者模型,一个主任务唤醒多个从任务。这种类型的负载,如果其任务同时运行于多个CPU上,性能就会好很多。不过现在机器上CPU很多,怎么挑选一个最好的CPU来跑我们的任务变得非常重要。
另一方面,调度时对CPU的选择也会影响功耗,如果把任务堆在少部分CPU上运行,那么剩下的CPU就可以进入节能模式,如果一个 Socket 上的CPU都空闲了,那么可以更加省电。如果一个节能模式下的空闲CPU被调度上来一个任务,由于CPU从低功耗转变为耗能模式,成本就相应增加了。
所以综上所述,调度器需要根据唤醒模式来猜测负载是什么类型的,并以此来决策上面的任务是要集中运行于少部分CPU以增加 cache 命中率并节能,还是要全打散来获得更高的CPU利用率。这便是当初 wake_wide() 函数被发明的背景。这个函数是让调度器变得更加错综复杂的众多启发式搜索算法之一,它决定一个刚被å¤醒的任务是应该团结在唤醒它的那个任务所运行的CPU附近,还是应该志在远方,跑到另一个 NUMA 节点上去运行。这里面会有个 tradeoff,把任务打包在一起的方式可以提高本地缓存命中率,做过了却也容易让CPU忙不过来,增加runq的调度延迟。 [编辑] 历史
- 代码走读
在select_task_rq_fair的时候,会记录唤醒关系,如下的record_wakee所示:static int select_task_rq_fair(struct task_struct *p, int prev_cpu, int sd_flag, int wake_flags) { struct sched_domain *tmp, *sd = NULL; int cpu = smp_processor_id(); int new_cpu = prev_cpu; int want_affine = 0; int sync = (wake_flags & WF_SYNC) && !(current->flags & PF_EXITING); if (sd_flag & SD_BALANCE_WAKE) { record_wakee(p); 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) && !wake_cap(p, cpu, prev_cpu) && cpumask_test_cpu(cpu, p->cpus_ptr); }
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)) {//从上一次衰减过之后,到现在超过1s钟,则再次衰减,所以,衰减周期为1s钟,每次衰减1半 current->wakee_flips >>= 1; current->wakee_flip_decay_ts = jiffies; } if (current->last_wakee != p) {//被当前task唤醒的task发生变化了,则进行计数。 current->last_wakee = p; current->wakee_flips++; } //所以,wakee_flips表示一段时间被当前task唤醒的不同task数量。值越大,表示较多的task依赖这个task。 }
如上面的code所示,record_wakee记录了当前task一段时间唤醒其他task的情况。值越大,表示越多的task 依赖当前的task。
want_affine的判定条件:
如前overview部分所述,做cpu亲和性选核,为性能考虑,不能让waker与wakee的亲和上来的task超过一定的量使性能下降,例如共簇的cpu有4颗,但是却有8个task相互关联,导致性能下降,即超过了cluster的宽度。同时,在异构的cpu当中,如果大核能力悬殊过大,放在能力更大的cpu上或许可以获得更好的性能,所以,又需要判断能力是否有悬殊。注意这里的亲和性会在当前cpu与task原来所在的cpu之间进行选择。want_affine = !wake_wide(p) && !wake_cap(p, cpu, prev_cpu) && cpumask_test_cpu(cpu, p->cpus_ptr);
wake_wide函数:
当前 current 正在为 wakeup p, 并为 p 选择一个合适的 CPU. 那么 wake_wide 就用来检查 current 和 p 之间是否适合 wake_affine 所关心的 waker/wakee 模型.
wake_wide 返回 0, 表示 wake_affine 是有效的. 否则返回 1, 表示这两个进程不适合用 wake_affine.
/* * 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;//统计的一段时间依赖当前task的数量 unsigned int slave = p->wakee_flips; //一段时间依赖被唤醒task的数量 int factor = this_cpu_read(sd_llc_size);//同簇cpu的数量 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 操作的成本会很高.
由于目前有一些 CPU 都是属于性能异构的 CPU(比如 ARM64 的 big.LITTLE 等), 不同的核 capacity 会差很多. wake_cap 会先看待选择的进程是否。
/*
* 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)//大小核能力悬殊不超过12.5%
return 0;
/* Bring task utilization in sync with prev_cpu */
sync_entity_load_avg(&p->se);
return !task_fits_capacity(p, min_cap);
}
wake_cap表示大小核能力悬殊不超过12.5%。
参考文章:
https://kernel.taobao.org/2017/08/Reconsidering-the-schedulers-wake_wide-heuristic/