Linux CFS調度器之喚醒WAKE_AFFINE 機制--Linux進程的管理與調度(三十一)

日期 內核版本 架構 作者 GitHub KernelShow
2016-0729 Linux-4.6 X86 & arm gatieme LinuxDeviceDrivers Linux進程管理與調度

#1 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.

#2 wake_affine 機制分析

根據 want_affine 變量選擇調度域並確定 new_cpu

我們知道如下的事實 :

  • 進程p的調度域參數設置了SD_BALANCE_WAKE

  • 當前cpu的喚醒次數沒有超標

  • 當前task p消耗的capacity * 1138小於min_cap * 1024

  • 當前cpu在task p的cpu親和數裏面的一個

// https://elixir.bootlin.com/linux/v5.1.15/source/kernel/sched/fair.c#L6674
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_allowed);
 }

 rcu_read_lock();
 for_each_domain(cpu, tmp) {
  if (!(tmp->flags & SD_LOAD_BALANCE))
   break;

  /*
   * 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;
 }
  • 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() 來完成.

###2.2.1 record_wakee 與 wakee_flips

通過在 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). 啓動時, 內核將該變量初始化爲0, 此後, 每次時鐘中斷處理程序都會增加該變量的值.一秒內時鐘中斷的次數等於Hz, 所以jiffies一秒內增加的值也就是Hz.系統運行時間以秒爲單位, 等於jiffies/Hz.
將以秒爲單位的時間轉化爲jiffies:
seconds * Hz
將jiffies轉化爲以秒爲單位的時間:
jiffies / Hz

jiffies記錄了系統啓動以來, .

一個tick代表多長時間, 在內核的CONFIG_HZ中定義.比如CONFIG_HZ=200, 則一個jiffies對應5ms時間.所以內核基於jiffies的定時器精度也是5ms

###2.2.2 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 ?

/*
 * 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

  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 共享有優勢.

條件 描述
slave < factor 即如果 wakee->wakee_flips < factor, 則說明當前進程的喚醒切換不那麼頻繁, 即使當前進程有 wakee_flips 個 wakee, 當前 sched_domain 也完全能裝的下他們.
master < slave * factor 即 master/slave < factor, 兩個 waker wakee_flips 的比值小於 factor, 那麼這種情況下, 進行 wake_affine 的成本可控.
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

###2.2.3 wake_cap

由於目前有一些 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;
}

###2.3.1 負載計算方式

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

###2.3.2 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親緣關係設置. 同時如果 this_cpu 也是空閒的, 優先 this_cpu.

另外沒有證據保證來自中斷的緩存熱數據比 prev_cpu 上的緩存熱數據更重要, 並且從cpufreq的角度來看, 最好在一個CPU上獲得更高的利用率.

###2.3.3 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 = 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 的計算方式爲:

KaTeX parse error: Undefined control sequence: \task at position 18: …={[cpu\_load\pm\̲t̲a̲s̲k̲\_h\_load(p)]\t…

注意使用 wake_affine_weight 需要開啓 WA_WEIGHT 開關

描述 commit 分析
smart wake-affine(lkml,patchwork)

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().

##2.4 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()));

commit lkml patchwork
62470419e993 sched: Implement smarter wake-affine logic
7d9ffa896148 sched: Micro-optimize the smart wake-affine logic
https://lkml.org/lkml/2013/7/4/18 https://lore.kernel.org/patchwork/cover/390846

接着 Vincent Guittot 和 Rik van Riel 做了不少功耗優化的工作. 這時候 wake-affne 中開始考慮 CPU capacity 的信息.

commit lkml patchwork
05bfb65f52cb sched: Remove a wake_affine() condition
bd61c98f9b3f sched: Test the CPU’s capacity in wake_affine()
https://lkml.org/lkml/2014/5/23/458 XXXX

然後 Rik van Riel 在 NUMA 層次支持了 wake_affine

commit lkml patchwork
739294fb03f5 sched/numa: Override part of migrate_degrades_locality() when idle balancing
7d894e6e34a5 sched/fair: Simplify wake_affine() for the single socket case
3fed382b46ba sched/numa: Implement NUMA node level wake_affine()
815abf5af45f sched/fair: Remove effective_load()
https://lkml.org/lkml/2017/6/23/496 https://lore.kernel.org/patchwork/cover/803114/

緊接着是 Peter Zijlstra 的一堆 FIX, 爲了解決支持了 NUMA 之後一系列性能問題.

commit lkml 描述
90001d67be2f sched/fair: Fix wake_affine() for !NUMA_BALANCING https://lkml.org/lkml/2017/8/1/377 XXXX
a731ebe6f17b sched/fair: Fix wake_affine_llc() balancing rules https://lkml.org/lkml/2017/9/6/196 XXXX
d153b153446f sched/core: Fix wake_affine() performance regression
f2cdd9cc6c97 sched/core: Address more wake_affine() regressions
https://lkml.org/lkml/2017/10/14/172 該補丁引入了 WA_IDLE, WA_BIAS+WA_WEIGHT

目前最新 5.2 的內核中,
Dietmar Eggemann 刪除了 LB_BIAS 特性, 因此 wake-affine 的代碼做了部分精簡.(僅僅是代碼重構, 沒有邏輯變更)

commit lkml 描述
fdf5f315d5cf sched/fair: Disable LB_BIAS by default https://lkml.org/lkml/2018/8/9/493 默認 LB_BIAS 爲 false
1c1b8a7b03ef sched/fair: Replace source_load() & target_load() with weighted_cpuload() 沒有 LB_BIAS 之後, source_load/target_load 不再需要, 直接使用 weighted_cpuload 代替
a3df067974c5 sched/fair: Rename weighted_cpuload() to cpu_runnable_load() weighted_cpuload 函數更名爲 cpu_runnable_load, patchwork

#3 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 機制的參與下, 選核流程是一個什麼樣的邏輯呢?
代碼參見, 選用早期 v5.1.15 版本的內核.

  • 首先 sd_flag 必須配置 SD_BALANCE_WAKE 纔會去做 wake_affine, 如果是 energy aware, EAS 會先通過 find_energy_efficient_cpu 選核, 不過這個是 EAS 的範疇, 不是我們今天的重點.
  • 先 record_wakee 更新 wake affine 統計信息, 接着通過 wake_cap 和 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), 同樣如果不是 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.

#4 參考資料

Reduce scheduler migrations due to wake_affine

[scheduler]十. 傳統的負載均衡是如何爲task選擇合適的cpu?

wukongmingjing 的調度器專欄

Linux Kernel- task_h_load

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章