linux之调度管理(13)- wake affine 唤醒特性

一、前言

在上一篇文章中,提到**任务放置(task placement),**当阻塞的任务被唤醒的时候,确定该任务应该放置在那个CPU上执行。通过对任务放置场景(task placement)的均衡分布进行分析,加深对内核调度器实现任务均衡分布的理解。

二、任务放置

2.1 什么是任务放置(task placement)

Linux 为每个CPU都配置一个cpu runqueue,用以维护当前CPU需要运行的所有线程,调度器会按一定的规则从runqueue中获取某个线程来执行。如果一个线程正挂在某个CPU的runqueue上,此时它处于就绪状态,尚未得到cpu资源,调度器会适时地通过负载均衡(load balance)来调整任务的分布;当它从runqueue中取出并开始执行时,便处于运行状态,若该状态下的任务负载不是当前CPU所能承受的,那么调度器会将其标记为misfit task,周期性地触发主动迁移(active upmigration),将misfit task布置到更高算力的CPU。

上面提到的场景,都是线程已经被分配到某个具体的CPU并且具备有效的负载。如果一个任务线程还未被放置到任何一个CPU上,即处于阻塞状态,又或者它是刚创建、刚开始执行的,此时调度器又是何如做均衡分布的呢?这便是今天我们要花点篇幅来介绍的任务放置场景。

内核中,task placement场景发生在以下三种情况:

(1)进程通过fork创建子进程;

(2)进程通过sched_exec开始执行;

(3)阻塞的进程被唤醒。

2.2 调度域(sched domain)及其标志位(sd flag)

如果你正在使用智能手机阅读本文,那你或许知道,目前的手机设备往往具备架构不同的8个CPU core。我们仍然以4小核+4大核的处理器结构为例进行说明。4个小核(cpu0-3)组成一个little cluster,另外4个大核(cpu4-7)组成big cluster,每个cluster的CPU架构相同,它们之间使用同一个调频策略,并且频率调节保持一致。大核相对小核而言,具备更高的算力,但也会带来更多的能量损耗。

对于多处理器均衡(multiprocessor balancing)而言,sched domain是极为重要的概念。内核中以结构体 struct sched_domain对其进行定义,将CPU core从下往上按层级划分,对系统所有CPU core进行管理,本系列文章第一篇已进行过较为详细的描述。little cluster和big cluster各自组成底层的MC domain,包含各自cluster的4个CPU core,顶层的DIE domian则覆盖系统中所有的CPU core。

内核调度器依赖sched domain进行均衡,为了方便地对各种均衡状态进行识别,内核定义了一组sched domain flag,用来标识当前sched domain具备的均衡属性。表中,我们可以看到task placement场景常见的三种情况对应的flag。

|--------------------|---------|---------------|
| 属性 | 标识位 | 含义 |
| SD_LOAD_BALANCE | 0x0001 | 允许负载均衡 |
| SD_BALANCE_NEWIDLE | 0x0002 | 进入idle时进行均衡 |
| SD_BALANCE_EXEC | 0x0004 | exec时进行均衡 |
| SD_BALANCE_FORK | 0x0008 | fork时进行均衡 |
| SD_BALANCE_WAKE | 0x0010 | 任务唤醒时进行均衡 |
| SD_WAKE_AFFINE | 0x0020 | 任务唤醒时放置到临近CPU |
| ... |

在构建CPU拓扑结构时,会为各个sched domain配置初始的标识位(build_sched_domains->build_sched_domain->sd_init),如果是异构系统,会设置SD_BALANCE_WAKE:

复制代码
//kernel/kernel/sched/topology.c

static struct sched_domain *
sd_init(struct sched_domain_topology_level *tl,
        const struct cpumask *cpu_map,
        struct sched_domain *child, int dflags, int cpu)
{
        struct sd_data *sdd = &tl->data;
        struct sched_domain *sd = *per_cpu_ptr(sdd->sd, cpu);
        int sd_id, sd_weight, sd_flags = 0;

#ifdef CONFIG_NUMA
        /*
         * Ugly hack to pass state to sd_numa_mask()...
         */
        sched_domains_curr_level = tl->numa_level;
#endif

        sd_weight = cpumask_weight(tl->mask(cpu));

        if (tl->sd_flags)
                sd_flags = (*tl->sd_flags)();
        if (WARN_ONCE(sd_flags & ~TOPOLOGY_SD_FLAGS,
                        "wrong sd_flags in topology description\n"))
                sd_flags &= TOPOLOGY_SD_FLAGS;

        /* Apply detected topology flags */
        sd_flags |= dflags;

        *sd = (struct sched_domain){
                .min_interval           = sd_weight,
                .max_interval           = 2*sd_weight,
                .busy_factor            = 16,
                .imbalance_pct          = 117,

                .cache_nice_tries       = 0,

                .flags                  = 1*SD_BALANCE_NEWIDLE
                                        | 1*SD_BALANCE_EXEC
                                        | 1*SD_BALANCE_FORK
                                        | 0*SD_BALANCE_WAKE
                                        | 1*SD_WAKE_AFFINE
                                        | 0*SD_SHARE_CPUCAPACITY
                                        | 0*SD_SHARE_PKG_RESOURCES
                                        | 0*SD_SERIALIZE
                                        | 1*SD_PREFER_SIBLING
                                        | 0*SD_NUMA
                                        | sd_flags
                                        ,

                .last_balance           = jiffies,
                .balance_interval       = sd_weight,
                .max_newidle_lb_cost    = 0,
                .next_decay_max_lb_cost = jiffies,
                .child                  = child,
#ifdef CONFIG_SCHED_DEBUG
                .name                   = tl->name,
#endif
        };

        cpumask_and(sched_domain_span(sd), cpu_map, tl->mask(cpu));
        sd_id = cpumask_first(sched_domain_span(sd));

        /*
         * Convert topological properties into behaviour.
         */

        ....

        return sd;
}

2.3 task placement均衡代码框架

linux内核的调度框架是高度抽象、模块化的,所有的线程都拥有各自所属的调度类(sched class),比如大家所熟知的实时线程属于rt_sched_class,CFS线程属于fair_sched_class,不同的调度类采用不同的调度策略。上面提到的task placement的三种场景,最终的函数入口都是core.c中定义的select_task_rq() 方法,之后会跳转至调度类自己的具体实现。本文以CFS调度类为分析对象,因为该调度类的线程在整个系统中占据较大的比重。有兴趣的朋友可以了解下其它调度类的**select_task_rq()**实现。

int wake_up_process(struct task_struct *p)

{

return try_to_wake_up(p, TASK_NORMAL, 0);

}

EXPORT_SYMBOL(wake_up_process);

wake_up_process 函数调用的是try_to_wake_up。

2.4 select_task_rq_fair ()方法

CFS调度类的线程进行task placement时,会通过core.c的select_task_rq () 方法跳转至select_task_rq _fair(),该方法声明如下:

复制代码
//kernel/kernel/sched/fair.c

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) && 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;
                }

                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 (sd_flag & SD_BALANCE_WAKE) { /* XXX always ? */
                /* Fast path */

                new_cpu = select_idle_sibling(p, prev_cpu, new_cpu);

                if (want_affine)
                        current->recent_used_cpu = cpu;
        }
        rcu_read_unlock();

        return new_cpu;
}

wake_flags参数:特地为SD_BALANCE_WAKE提供的唤醒标识位,一共有三种类型:

复制代码
// kernel/kernel/sched/sched.h

/*
 * wake flags
 */
#define WF_SYNC                 0x01            /* Waker goes to sleep after wakeup */
#define WF_FORK                 0x02            /* Child wakeup after fork */
#define WF_MIGRATED             0x04            /* Internal use, task got migrated */
#define WF_ON_CPU               0x08            /* Wakee is on_cpu */
#define WF_LOCK_SLEEPER         0x10            /* Wakeup spinlock "sleeper" */

select_task_rq_fair() 内仅对WF_SYNC进行处理,若传入该标识位,说明唤醒线程waker在被唤醒线程wakee唤醒后,将进入阻塞状态,调度器会倾向于将wakee放置到waker所在的CPU。这种场景使用相当频繁,比如用户空间两个进程进行非异步binder通信,Server端唤醒一个binder线程处理事务时,调用的接口如下:

select_task_rq_fair() 中涉及到三个重要的选核函数:find_energy_efficient_cpu ()find_idlest_cpu ()select_idle_sibling (),它们分别代表任务放置过程中的三条路径。task placement的各个场景,根据不同条件,最终都会进入如下三条路径的某一条路径,得到任务放置CPU并结束此次的task placement过程。现在让我们来理一理这三条路径的常见进入条件以及基本的CPU选择考量:

(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,不能在这次调度中大展拳脚。

**当EAS不能在这次调度中发挥作用时,分支的走向取决于该任务是否为wake affine类型的任务,**这里让我们先来简单了解下该类型的任务。

用户场景有时会出现一个主任务(waker)唤醒多个子任务(wakee)的情况,如果我们将其作为wake affine类型处理,将wakee打包在临近的CPU上(如唤醒CPU、上次执行的CPU、共享cache的CPU),即可以提高cache命中率,改善性能,又能避免唤醒其它可能正处于idle状态的CPU,节省功耗。看起来这样的处理似乎非常完美,可惜的是,往往有些wakee对调度延迟非常敏感,如果将它们打包在一块,CPU上的任务就变得"拥挤",调度延迟就会急剧上升,这样的场景下,所谓的cache命中率、功耗,一切的诱惑都变得索然无味。

对于wake affine类型的判断,内核主要通过wake_wide ()wake_cap() 的实现,从wakee的数量以及临近CPU算力是否满足任务需求这两个维度进行考量。注意:考虑到在select_idle_sibling中做了相同工作,wake_cap()在6.6中取消了,参考patch:sched/fair: Kill wake_cap()

(2)慢速路径find_idlest_cpu () 。有两种常见的情况会进入慢速路径:a、传入参数sd_flag为SD_BALANCE_WAKE,且EAS没有使能或者返回-1时,如果该任务不是wake affine类型,就会进入慢速路径;b、传入参数sd_flag为SD_BALANCE_FORK、SD_BALANCE_EXEC时,由于此时的任务负载是不可信任的,无法预测其对系统能耗的影响,也会进入慢速路径。慢速路径使用find_idlest_cpu **()**方法找到系统中最空闲的CPU,作为放置任务的CPU并返回。基本的搜索流程是:

首先确定放置的target domain(从waker的base domain向上,找到最底层配置相应sd_flag的domain),然后从target domain 中找到负载最小的调度组 ,进而 在调度组中找到负载最小的CPU

这种选核方式对于刚创建的任务来说,算是一种相对稳妥的做法,开发者也指出,或许可以将新创建的任务放置到特殊类型的CPU上,或者通过它的父进程来推断它的负载走向,但这些启发式的方法也有可能在一些使用场景下造成其他问题。

(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处理。

如果prev cpu或者wake cpu无法满足条件,那么调度器会尝试从它们的LLC domain中去搜索idle的CPU。

task_struct 数据结构中有一个 recent_used_cpu 成员记录了进程最近经常使用的CPU。

三、wake affine 特性

3.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 的原型如下:

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 都很忙, 那放上来可以并不一定好, 唤醒延迟之类的都是一个考量.

那么这些都是一个综合权衡的过程, 我们要考虑的东西比较多

  • wake_cpu,prev_cpu 到底该不该选择?
  • 选择的话选择哪个?
  • 它们都不合适的时候又要怎么去选择一个更合适的?

内核需要一个简单有效的机制去做这个事情, 因此 WAKE_AFFINE 出现在内核中.

3.2 WAKE_AFFINE 机制简介

select_task_rq_fair 选核其实是一个优选的过程, 通常会有限选择一个 cache-miss 等开销最小的一个

  1. 根据 wake_affine 选择调度域并确定 new_cpu

  2. 根据调度域及其调度域参数选择兄弟 idle cpu 根据调度域及其调度域参数选择兄弟 idle cpu

  3. 根据调度域选择最深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.

3.3 wake_affine 机制分析

根据 want_affine 变量选择调度域并确定 new_cpu:

选择的标准:

  • 将要唤醒的进程p的调度域参数设置了SD_BALANCE_WAKE

  • 当前cpu的唤醒次数没有超标(wakee_flips 变量的值小于 当前调度组的CPU的capacity)

  • 当前task p消耗的capacity * 1138小于min_cap * 1024

  • 当前cpu在task p的cpu亲和数里面的一个

    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) && 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;
                  }
    
                  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 (sd_flag & SD_BALANCE_WAKE) { /* XXX always ? */
                  /* Fast path */
    
                  new_cpu = select_idle_sibling(p, prev_cpu, new_cpu);
    
                  if (want_affine)
                          current->recent_used_cpu = cpu;
          }
          rcu_read_unlock();
    
          return new_cpu;

    }

  • wake_wide 和 wake_cap 为调度器提供决策, 当前进程是否符合 wake_affine 的决策模型. 如果他们返回 1, 则说明如果采用 wake_affine 进行决策, 大概率是无效的或者会降低性能, 则调度器就不会 want_affine 了.

want_affine = !wake_wide(p) && cpumask_test_cpu(cpu, p->cpus_ptr);

wake_wide 检查当前cpu的唤醒关系符合 wake_affine 模型.

task p 可以在当前 CPU 上运行.

注意:wake_cap 已经从调度器源码中删除

  • wake_affine 则为目标进程选择最合适运行的 wake CPU.

内核引入 wake_affine 的初衷就是识别什么时候要将 wakee 唤醒到 waking CPU, 什么时候不需要. 这个判断由 want_affine 通过 wake_wide() 来完成.

3.4 record_wakee 与 wakee_flips

通过在 struct task_struct 中增加两个成员: 上次唤醒的进程 last_wakee, 和累积唤醒翻转计数器wakee_flips. 每当 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, 而且唤醒频率越比较高.

比如一个进程 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). 启动时, 内核将该变量初始化为0, 此后, 每次时钟中断处理程序都会增加该变量的值.一秒内时钟中断的次数等于Hz, 所以jiffies一秒内增加的值也就是Hz.系统运行时间以秒为单位, 等于jiffies/Hz.

将以秒为单位的时间转化为jiffies:

seconds * Hz

将jiffies转化为以秒为单位的时间:

jiffies / Hz

jiffies记录了系统启动以来, .

一个tick代表多长时间, 在内核的CONFIG_HZ中定义.比如CONFIG_HZ=250, 则一个jiffies对应4ms时间.所以内核基于jiffies的定时器精度也是4ms

3.5 wake_wide

当前 current 正在为 wakeup p, 并为 p 选择一个合适的 CPU. 那么 wake_wide 就用来检查 current 和 p 之间是否适合 wake_affine 所关心的 waker/wakee 模型.

wake_wide 返回 0, 表示 wake_affine 是有效的. 否则返回 1, 表示这两个进程不适合用 wake_affine.

那么什么时候, wake_wide 返回 1 ?

复制代码
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

  1. 将 wakee_flips 值大的 wakee 唤醒到临近的 CPU, 可能有利于系统其他进程的唤醒, 同样这也意味着, waker 将面临残酷的竞争.
  2. 此外, 如果 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 共享有优势.

3.6 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 则进一步考虑进程的负载信息以及调度的延迟信息.

    //kernel/kernel/sched/fair.c

    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_idle:

复制代码
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亲缘关系设置. 同时如果 prev_cpu 也是空闲的, 优先 prev_cpu.

另外没有证据保证来自中断的缓存热数据比 prev_cpu 上的缓存热数据更重要, 并且从cpufreq的角度来看, 最好在一个CPU上获得更高的利用率.

wake_affine_weight:

wake_affine_weight 会重新计算 wakeup CPUprev 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 = cpu_load(cpu_rq(this_cpu));

        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 = cpu_load(cpu_rq(prev_cpu));
        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;
}

wake_affine_weight 中负载比较的部分经历了很多次的修改.

eeb603986391 sched/fair: Defer calculation of 'prev_eff_load' in wake_affine_weight() until needed

082f764a2f3f sched/fair: Do not migrate on wake_affine_weight() if weights are equal

1c1b8a7b03ef sched/fair: Replace source_load() & target_load() with weighted_cpuload(), 这个是 sched: remove cpu_loads 中的一个补丁, 该补丁集删除了 cpu_load idx 干掉了 LB_BIAS 特性, 它指出 LB_BIAS 的设计本身是有问题的, 在负载均衡迁移时平滑两个 cpu_load 的过程中, 用 source_load/target_load 的方式在源 CPU 和目的 CPU 上用一个随机偏差的方式是错误的, 这个平衡偏差应该取决于cpu组之间的任务转移成本,而不是随机历史记录或即时负载。因为历史负载可能与实际负载相差很大,从而导致不正确的偏差.

11f10e5420f6c sched/fair: Use load instead of runnable load in wakeup path https://lore.kernel.org/patchwork/patch/1141693, 该补丁是 rework load balancce 的一个补丁, 之前唤醒路径用下的是 cpu_runnable_load, 现在修正为 cpu_load. cpu_load 对应的是 rq 的 load_avg, 代表就绪队列平均负载,其包含睡眠进程的负载贡献, cpu_runnable_load 则是 runnable_load_avg只包含就绪队列上所有可运行进程的负载贡献, wakeup 的时候如果使用 cpu_runnable_load 则可能造成选核的时候选择到一个有很多 runnable 线程的 overloaded 的 CPU, 而不是一个有很多 blocked 线程, 但是还有很大空闲的 CPU. 因此使用 cpu_load 在 wakeup 的时候可能更好.

当前内核版本 5.6.13 中 wake_affine_weight 的实现参见, 跟我们前面将的思路没有太大变化, 但是没有了 LB_BIAS, 同时比较负载使用的是 cpu_load().

四、wake_affine 演进

Michael Wang 实现了 Smart wake affine, 引入 wakee_flips 来识别 wake-affine 的场景. 然后 Peter 做了一个简单的优化, factor 使用了 sd->sd_llc_size 而不是直接获取所在NODE 的 CPU 数目. nr_cpus_node(cpu_to_node(smp_processor_id()));

接着 Vincent Guittot 和 Rik van Riel 做了不少功耗优化的工作. 这时候 wake-affne 中开始考虑 CPU capacity 的信息.

然后 Rik van Riel 在 NUMA 层次支持了 wake_affine

紧接着是 Peter Zijlstra 的一堆 FIX, 为了解决支持了 NUMA 之后一系列性能问题.

目前最新 5.2 的内核中,Dietmar Eggemann 删除了 LB_BIAS 特性, 因此 wake-affine 的代码做了部分精简.(仅仅是代码重构, 没有逻辑变更)

4.1 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 选核, 不过这个是 EAS 的范畴, 不是我们今天的重点.
  • 先 record_wakee 更新 wake affine 统计信息, 接着通过 wake_wide 看这次对进程的唤醒是不是 want_affine 的.
  • 接着从 waker CPU 开始向上遍历调度域,
  1. 如果是 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

  2. 否则, 如果是 want_affine, 但是 tmp 中没找到满足要求的 CPU, 则最终循环结束条件为 !(tmp->flag & SD_LOAD_BALANCE),

只要 wakeup 的时候, 会通过 wake_affine, 然后通过 select_idle_sibling 来选核.

其他情况下, 都是找到满足 sd_flags 的最高层次 sd, 然后通过 find_idlest_cpu 在这个调度域 sd 中去选择一个最空闲的 CPU.

相关推荐
腻害兔1 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社2 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海2 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖3 小时前
JDK 26 新特性详解
java·开发语言
马优晨4 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区5 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大6 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai6 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说6 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json
এ慕ོ冬℘゜6 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript