Scheduler -18- (Load Balance 4 EAS)

<kernel v5.4>

EAS(Energy Aware Scheduling)

 

모바일 시스템으로 인해 에너지 절약에 대한 요구, 터치에 대한 빠른 application의 빠른 응답 성능, 그리고 높은 성능 유지 등에 대한 사용자의 요구가 매우 높은데, 기존의 커널 스케줄러 만으로는 각각의 세 항목들을 동시에 만족시키기란 매우 어려운 상황이었다. 이에 대한 시장의 요구를 반영한 솔루션이 안드로이드 진영에서 먼저 시작되어 EAS를 탄생시켰고, 메인 라인 커널에도 업스트림되었다.

  • 참고로 3 가지 항목을 동시(at a time)에 높일 수 없다. 그런 것이 가능할 수는 없다. 다만 환경 조건에 따라 어느 한 쪽에 더 힘을 준다.
  • PM에서 절전을 하는 것 이외에 스케줄러 분야에서는 빠른 응답과 높은 성능 위주로 코드가 튜닝되어 왔는데, EAS 출현 후 에너지 효율에 대해 더 강력한 로직들이 추가되었다.

 

1) 에너지 효율을 달성하기 위해 전력 관리(Power Management) 기술 중 하나인 cpuidle과 관련하여 빅/리틀 클러스터 개념이 탄생되었고 다음과 같이 세 종류의 마이그레이션에 대한 운용 개념이 탄생되었고, 최근의 모바일 시스템은 빅/미디엄/리틀 클러스터를 동시에 사용하는 HMP 방식으로 추세가 바뀌었다. 또한 cpufreq(DVFS)를 통해 에너지 이득을 얻고자 하였다.

  • 클러스터 운용
    • Clustered switching (Cluster migration)
      • 클러스터 단위로 빅/리틀 선택 운용
    • In-kernel switcher (CPU migration)
      • cpu core 단위로 빅/리틀 선택 운용
    • HMP(Heterogeneous multi-processing) 운용
      • 그냥 동시에 같이 운용하고, EAS를 통해 에너지 와 성능에 대해 적절한 cpu를 선택하는 스케줄 방법을 사용한다.
  • DVFS 운용
    • cpu의 유틸 로드에 따라 코어의 주파수를 여러 단계로 변화시켜 절전하는 시스템을 채용하였다.

 

2) 빠른 응답을 달성하기 위해 안드로이드 진영에서는 32ms 단위로 절반씩 deacy되는 특성을 가진 PELT를 WALT로 바꾸어 로드의 상향에 대해 4배, 하향에 대해 8배를 가진 특성을 사용하여 응답성을 높이고, uclamp를 통해 특정 application의 EAS를 통해 적절한 core를 찾아 빠르게 운용된다.

 

3) 높은 성능을 달성하기 위해 EAS는 빅/리틀 코어에 대해 필요 시 빅 코어에 대한 선택을 강화하였다.

  • 태스크의 유틸 로드가 리틀로 마이그레이션 시 capacity 부족으로 인한 misfit 판정 시 리틀로의 마이그레이션을 제한한다.
  • uclamp를 통해 특정 application이 동작 시 유틸 로드 범위를 제한하는 것으로 성능을 위한 빅 클러스터 또는 절전을 위한 리틀 클러스터를 조금 더 선택할 수 있도록 도와준다.

 

EAS component

다음 그림은 EAS를 구성하는 component들을 보여준다. 이들은 모바일 분야에서 치열하게 연구되어 계속 추가되고 있고, 일부는 새로운 기술로 대체되고 있다. (퀄컴, 삼성, 구글, ARM사에서 주도적으로 경쟁하였고 근래에는 서로의 장점을 모아 거의 유사해지고 있다.)

  • EAS Core
    • 스케줄러 내에서 에너지 모델로 동작하는 Task Placement 로직
    • WALT 또는 PELT
  • Schedutil
    • 스케줄러 결합된 새로운 CPUFreq(DVFS) based governor
  • CPUIdle
    • 스케줄러 결합된 새로운 CPUIdle based governor
  • UtilClamp(uclamp)
    • Task Placement와 schedutil에 영향을 준다.
    • 기존 SchedTune을 대체하여 사용한다.

 

Wake 밸런싱과 EA Task Placement

빅/리틀 클러스터를 가진 시스템처럼 Asymetric cpu capacity를 사용하는 시스템에서 깨어난 태스크가 Wake 밸런싱 과정에서 어떠한 cpu에서 동작해야 유리한지 알아본다.

  •                            에너지(J)
  • 파워(W) = ————————–
  •                         시간(second)

 

  •                                           명령어(instruction)
  • 성능(performance) = ——————————
  •                                               시간(second)

 

  • 파워(W)                에너지(J) / 시간(second)                          에너지(J)
  • ———–  =  ————————————————– = —————————— = EAS의 목적
  • 성능(P)          명령어(instruction) / 시간(second)         명령어(instruction)

 

EAS의 경우 성능은 높을 수록 좋고, 파워는 낮을 수록 좋으므로 EAS의 값이 작을 수록 좋다.

 

다음 그림은 wake 밸런스에 의해 깨어날 cpu를 찾는 과정을 보여준다. 깊게 잠든 C2 idle 상태의 cpu 보다 살짝 잠든 WFI 상태의 cpu를 선택하는 모습을 보여준다.

 

다음 그림은 wake 밸런스에 의해 깨어날 cpu를 찾는 과정을 보여주며, DVFS가 영향을 주는 과정을 보여준다.

 


에너지 효과적인 cpu 찾기

find_energy_efficient_cpu()

kernel/sched/fair.c -1/2-

/*
 * find_energy_efficient_cpu(): Find most energy-efficient target CPU for the
 * waking task. find_energy_efficient_cpu() looks for the CPU with maximum
 * spare capacity in each performance domain and uses it as a potential
 * candidate to execute the task. Then, it uses the Energy Model to figure
 * out which of the CPU candidates is the most energy-efficient.
 *
 * The rationale for this heuristic is as follows. In a performance domain,
 * all the most energy efficient CPU candidates (according to the Energy
 * Model) are those for which we'll request a low frequency. When there are
 * several CPUs for which the frequency request will be the same, we don't
 * have enough data to break the tie between them, because the Energy Model
 * only includes active power costs. With this model, if we assume that
 * frequency requests follow utilization (e.g. using schedutil), the CPU with
 * the maximum spare capacity in a performance domain is guaranteed to be among
 * the best candidates of the performance domain.
 *
 * In practice, it could be preferable from an energy standpoint to pack
 * small tasks on a CPU in order to let other CPUs go in deeper idle states,
 * but that could also hurt our chances to go cluster idle, and we have no
 * ways to tell with the current Energy Model if this is actually a good
 * idea or not. So, find_energy_efficient_cpu() basically favors
 * cluster-packing, and spreading inside a cluster. That should at least be
 * a good thing for latency, and this is consistent with the idea that most
 * of the energy savings of EAS come from the asymmetry of the system, and
 * not so much from breaking the tie between identical CPUs. That's also the
 * reason why EAS is enabled in the topology code only for systems where
 * SD_ASYM_CPUCAPACITY is set.
 *
 * NOTE: Forkees are not accepted in the energy-aware wake-up path because
 * they don't have any useful utilization data yet and it's not possible to
 * forecast their impact on energy consumption. Consequently, they will be
 * placed by find_idlest_cpu() on the least loaded CPU, which might turn out
 * to be energy-inefficient in some use-cases. The alternative would be to
 * bias new tasks towards specific types of CPUs first, or to try to infer
 * their util_avg from the parent task, but those heuristics could hurt
 * other use-cases too. So, until someone finds a better way to solve this,
 * let's keep things simple by re-using the existing slow path.
 */
static int find_energy_efficient_cpu(struct task_struct *p, int prev_cpu)
{
        unsigned long prev_delta = ULONG_MAX, best_delta = ULONG_MAX;
        struct root_domain *rd = cpu_rq(smp_processor_id())->rd;
        unsigned long cpu_cap, util, base_energy = 0;
        int cpu, best_energy_cpu = prev_cpu;
        struct sched_domain *sd;
        struct perf_domain *pd;

        rcu_read_lock();
        pd = rcu_dereference(rd->pd);
        if (!pd || READ_ONCE(rd->overutilized))
                goto fail;

        /*
         * Energy-aware wake-up happens on the lowest sched_domain starting
         * from sd_asym_cpucapacity spanning over this_cpu and prev_cpu.
         */
        sd = rcu_dereference(*this_cpu_ptr(&sd_asym_cpucapacity));
        while (sd && !cpumask_test_cpu(prev_cpu, sched_domain_span(sd)))
                sd = sd->parent;
        if (!sd)
                goto fail;

        sync_entity_load_avg(&p->se);
        if (!task_util_est(p))
                goto unlock;

        for (; pd; pd = pd->next) {
                unsigned long cur_delta, spare_cap, max_spare_cap = 0;
                unsigned long base_energy_pd;
                int max_spare_cap_cpu = -1;

                /* Compute the 'base' energy of the pd, without @p */
                base_energy_pd = compute_energy(p, -1, pd);
                base_energy += base_energy_pd;

                for_each_cpu_and(cpu, perf_domain_span(pd), sched_domain_span(sd)) {
                        if (!cpumask_test_cpu(cpu, p->cpus_ptr))
                                continue;

                        /* Skip CPUs that will be overutilized. */
                        util = cpu_util_next(cpu, p, cpu);
                        cpu_cap = capacity_of(cpu);
                        if (!fits_capacity(util, cpu_cap))
                                continue;

                        /* Always use prev_cpu as a candidate. */
                        if (cpu == prev_cpu) {
                                prev_delta = compute_energy(p, prev_cpu, pd);
                                prev_delta -= base_energy_pd;
                                best_delta = min(best_delta, prev_delta);
                        }

                        /*
                         * Find the CPU with the maximum spare capacity in
                         * the performance domain
                         */
                        spare_cap = cpu_cap - util;
                        if (spare_cap > max_spare_cap) {
                                max_spare_cap = spare_cap;
                                max_spare_cap_cpu = cpu;
                        }
                }

                /* Evaluate the energy impact of using this CPU. */
                if (max_spare_cap_cpu >= 0 && max_spare_cap_cpu != prev_cpu) {
                        cur_delta = compute_energy(p, max_spare_cap_cpu, pd);
                        cur_delta -= base_energy_pd;
                        if (cur_delta < best_delta) {
                                best_delta = cur_delta;
                                best_energy_cpu = max_spare_cap_cpu;
                        }
                }
        }

빅/리틀 처럼 asym cpucapacity를 사용하는 도메인에서 @p태스크가 마이그레이션할 에너지 효과적인(절약 가능한) cpu를 찾아 반환한다. @prev_cpu를 반환할 수도 있고, asym cpucapacity가 아니거나 오버유틸 중인 경우 -1을 반환한다. (pd들 중 가장 낮은 에너지를 사용하는 도메인에서 가장 높은 spare를 가진 cpu를 찾는다)

  • 코드 라인 11~13에서 perf domain이 없거나 overutilized 된 경우 fail 레이블을 통해 -1을 반환한다.
  • 코드 라인 19~23에서 빅/리틀 같이 asym cpucapacity 도메인이 없거나 해당 도메인을 포함하여 상위 도메인에 @prev_cpu가 속해있지 않으면 fail 레이블을 통해 -1을 반환한다.
  • 코드 라인 25~36에서 perf 도메인을 순회하며 compute_energy() 함수를 통해 태스크를 마이그레이션 후 소비할 에너지를 추정한 값을 알아와서 base_energy_pd 대입하고, 이를 base_energy에 누적한다.
  • 코드 라인 43~46에서 perf 도메인과 스케줄 도메인에 양쪽에 포함된 cpu들을 순회하며 태스크가 지원하지 않는 cpu들과 overutilized된 cpu들은 skip 한다.
    • 태스크 p가 cpu로 마이그레이션한 후를 예측한 cpu의 유틸을 알아와서 util에 대입하고, 이 값이 cpu의 capacity 이내에서 동작할 수 없으면 skip 한다.
  • 코드 라인 49~53에서 순회 중인 cpu에 @prev_cpu인 경우는 언제나 대상이 되어 @prev_cpu로 compute_energy() 함수를 통해 반환된 값에서 base_energy_pd를 뺀 차이를 prev_delta에 대입한다. 그 후 이 값은 best_delta와 비교하여 작은 값을 best_delta로 갱신한다.
  • 코드 라인 59~63에서 cpu에 남은 capacity 여분이 max_spare_cap보다 큰 경우 이를 갱신한다.
    • perf 도메인내에서 가장 spare가 높은 cpu를 일단 찾아놓는다.
  • 코드 라인 67~74에서 capacity 여분이 가장 큰 cpu가 새로운 cpu로 결정된 경우 태스크를 max_spare_cap_cpu로 마이그레이션할 때의 추정되는 에너지 소비량을 알아와서 base_energy_pd를 뺀 후 cur_delta에 대입한다.  이 값이 best_delta 보다 작을 때엔 best_delta로 이 값을 갱신한다.
    • perf 도메인들 중 가장 낮은 에너지를 소모하는 perf 도메인인을 찾는다. 그 후 위에서 결정한 가장 spare가 높은 cpu를 반환한다.

 

kernel/sched/fair.c -2/2-

unlock:
        rcu_read_unlock();

        /*
         * Pick the best CPU if prev_cpu cannot be used, or if it saves at
         * least 6% of the energy used by prev_cpu.
         */
        if (prev_delta == ULONG_MAX)
                return best_energy_cpu;

        if ((prev_delta - best_delta) > ((prev_delta + base_energy) >> 4))
                return best_energy_cpu;

        return prev_cpu;

fail:
        rcu_read_unlock();

        return -1;
}
  • 코드 라인 8~9에서 @prev_cpu로 결정된 경우가 아니면 best_energy_cpu를 반환한다.
  • 코드 라인 11~12에서 best_energy_cpu가 @prev_cpu보다 6% 이상의 에너지 절약이 되는 경우 best_energy_cpu를 반환한다.
  • 코드 라인 14에서 그냥 @prev_cpu를 반환한다.
  • 코드 라인 16~19에서 fail: 레이블이다. -1을 반환한다.

 

마이그레이션 후 에너지 추정

compute_energy()

kernel/sched/fair.c

/*
 * compute_energy(): Estimates the energy that @pd would consume if @p was
 * migrated to @dst_cpu. compute_energy() predicts what will be the utilization
 * landscape of @pd's CPUs after the task migration, and uses the Energy Model
 * to compute what would be the energy if we decided to actually migrate that
 * task.
 */
compute_energy(struct task_struct *p, int dst_cpu, struct perf_domain *pd)
{
        struct cpumask *pd_mask = perf_domain_span(pd);
        unsigned long cpu_cap = arch_scale_cpu_capacity(cpumask_first(pd_mask));
        unsigned long max_util = 0, sum_util = 0;
        int cpu;

        /*
         * The capacity state of CPUs of the current rd can be driven by CPUs
         * of another rd if they belong to the same pd. So, account for the
         * utilization of these CPUs too by masking pd with cpu_online_mask
         * instead of the rd span.
         *
         * If an entire pd is outside of the current rd, it will not appear in
         * its pd list and will not be accounted by compute_energy().
         */
        for_each_cpu_and(cpu, pd_mask, cpu_online_mask) {
                unsigned long cpu_util, util_cfs = cpu_util_next(cpu, p, dst_cpu);
                struct task_struct *tsk = cpu == dst_cpu ? p : NULL;

                /*
                 * Busy time computation: utilization clamping is not
                 * required since the ratio (sum_util / cpu_capacity)
                 * is already enough to scale the EM reported power
                 * consumption at the (eventually clamped) cpu_capacity.
                 */
                sum_util += schedutil_cpu_util(cpu, util_cfs, cpu_cap,
                                               ENERGY_UTIL, NULL);

                /*
                 * Performance domain frequency: utilization clamping
                 * must be considered since it affects the selection
                 * of the performance domain frequency.
                 * NOTE: in case RT tasks are running, by default the
                 * FREQUENCY_UTIL's utilization can be max OPP.
                 */
                cpu_util = schedutil_cpu_util(cpu, util_cfs, cpu_cap,
                                              FREQUENCY_UTIL, tsk);
                max_util = max(max_util, cpu_util);
        }

        return em_pd_energy(pd->em_pd, max_util, sum_util);
}

태스크 @p가 @dst_cpu로 마이그레이션 후 @pd가 소비할 에너지를 추정한 값을 반환한다.

  • 코드 라인 3~4에서 perf 도메인에 소속한 cpu들 중 첫 cpu의 scale 적용된 cpu capacity를 알아온다.
  • 코드 라인 17~18에서 perf 도메인에 소속한 cpu들 중 online cpu를 순회하며 태스크가 dst_cpu로 마이그레이션 한 후 cpu의 유틸을 예상해온 값을 util_cfs에 대입한다.
  • 코드 라인 27~28에서 순회 중인 cpu에 대해 에너지 산출을 위한 유효 유틸을 알아와서 sum_util에 추가한다.
  • 코드 라인 37~39에서 순회 중인 cpu에 대해 주파수를 선택하기 위한 유효 유틸을 알아와서 cpu_util에 대입하고, max_util도 갱신한다. 단 산출된 유효 유틸은 태스크의 uclamp 된 값으로 제한된다.
  • 코드 라인 42에서 perf 도메인의 cpu들이 소모할 에너지 추정치를 알아와서 반환한다.

 

schedutil_cpu_util()

kernel/sched/cpufreq_schedutil.c

/*
 * This function computes an effective utilization for the given CPU, to be
 * used for frequency selection given the linear relation: f = u * f_max.
 *
 * The scheduler tracks the following metrics:
 *
 *   cpu_util_{cfs,rt,dl,irq}()
 *   cpu_bw_dl()
 *
 * Where the cfs,rt and dl util numbers are tracked with the same metric and
 * synchronized windows and are thus directly comparable.
 *
 * The cfs,rt,dl utilization are the running times measured with rq->clock_task
 * which excludes things like IRQ and steal-time. These latter are then accrued
 * in the irq utilization.
 *
 * The DL bandwidth number otoh is not a measured metric but a value computed
 * based on the task model parameters and gives the minimal utilization
 * required to meet deadlines.
 */
unsigned long schedutil_cpu_util(int cpu, unsigned long util_cfs,
                                 unsigned long max, enum schedutil_type type,
                                 struct task_struct *p)
{
        unsigned long dl_util, util, irq;
        struct rq *rq = cpu_rq(cpu);

        if (!IS_BUILTIN(CONFIG_UCLAMP_TASK) &&
            type == FREQUENCY_UTIL && rt_rq_is_runnable(&rq->rt)) {
                return max;
        }

        /*
         * Early check to see if IRQ/steal time saturates the CPU, can be
         * because of inaccuracies in how we track these -- see
         * update_irq_load_avg().
         */
        irq = cpu_util_irq(rq);
        if (unlikely(irq >= max))
                return max;

        /*
         * Because the time spend on RT/DL tasks is visible as 'lost' time to
         * CFS tasks and we use the same metric to track the effective
         * utilization (PELT windows are synchronized) we can directly add them
         * to obtain the CPU's actual utilization.
         *
         * CFS and RT utilization can be boosted or capped, depending on
         * utilization clamp constraints requested by currently RUNNABLE
         * tasks.
         * When there are no CFS RUNNABLE tasks, clamps are released and
         * frequency will be gracefully reduced with the utilization decay.
         */
        util = util_cfs + cpu_util_rt(rq);
        if (type == FREQUENCY_UTIL)
                util = uclamp_util_with(rq, util, p);

        dl_util = cpu_util_dl(rq);

        /*
         * For frequency selection we do not make cpu_util_dl() a permanent part
         * of this sum because we want to use cpu_bw_dl() later on, but we need
         * to check if the CFS+RT+DL sum is saturated (ie. no idle time) such
         * that we select f_max when there is no idle time.
         *
         * NOTE: numerical errors or stop class might cause us to not quite hit
         * saturation when we should -- something for later.
         */
        if (util + dl_util >= max)
                return max;

        /*
         * OTOH, for energy computation we need the estimated running time, so
         * include util_dl and ignore dl_bw.
         */
        if (type == ENERGY_UTIL)
                util += dl_util;

        /*
         * There is still idle time; further improve the number by using the
         * irq metric. Because IRQ/steal time is hidden from the task clock we
         * need to scale the task numbers:
         *
         *              max - irq
         *   U' = irq + --------- * U
         *                 max
         */
        util = scale_irq_capacity(util, irq, max);
        util += irq;

        /*
         * Bandwidth required by DEADLINE must always be granted while, for
         * FAIR and RT, we use blocked utilization of IDLE CPUs as a mechanism
         * to gracefully reduce the frequency when no tasks show up for longer
         * periods of time.
         *
         * Ideally we would like to set bw_dl as min/guaranteed freq and util +
         * bw_dl as requested freq. However, cpufreq is not yet ready for such
         * an interface. So, we only do the latter for now.
         */
        if (type == FREQUENCY_UTIL)
                util += cpu_bw_dl(rq);

        return min(max, util);
}

@cpu에 대한 유효 유틸을 산출한다. 산출 타입에 FREQUENCY_UTIL(주파수 설정 목적) 및 ENERGY_UTIL(에너지 산출 목적) 타입을 사용한다.

  • 코드 라인 8~11에서 uclamp를 지원하는 않는 커널 옵션이고, FREQUENCY_UTIL 타입으로 rt 태스크가 동작 중인 경우 최대 성능을 위해 최대 에너지가 필요하므로 @max 값을 반환한다.
  • 코드 라인 18~20에서 irq 유틸이 max를 초과하는 경우 최대 성능을 위해 @max 값을 반환한다.
  • 코드 라인 34~36에서 @util_cfs 및 rt 유틸을 더하고, FREQUENCY_UTIL  타입인 경우 @p 태스크에 설정된 uclamp를 적용한다.
  • 코드 라인 38~50에서 dl 유틸을 알아오고, util과 더한 값이 @max를 초과하는 경우도 @max를 반환한다.
  • 코드 라인 56에서 ENERGY_UTIL 타입인 경우 util 값에 dl_util도 추가한다.
  • 코드 라인 67~68에서 irq 스케일 적용한 유틸을 산출한다.
    • task 클럭에는 irq 타임이 제거된 상태이다. 따라서 @max에서 irq을 뺀 나머지 capacity로 유틸을 곱하고 그 후 다시 irq를 추가한다.
  • 코드 라인 81~82에서 FREQUENCY_UTIL  타입인 경우 dl 밴드위드에 설정한 최소 유틸이 필요하므로 이 값도 util에 추가한다.
  • 코드 라인 84에서 산출된 유틸을 반환한다. 단 @max 값을 초과하지 못하게 제한한다.

 

에너지 소비량 추정

em_pd_energy()

include/linux/energy_model.h

/**
 * em_pd_energy() - Estimates the energy consumed by the CPUs of a perf. domain
 * @pd          : performance domain for which energy has to be estimated
 * @max_util    : highest utilization among CPUs of the domain
 * @sum_util    : sum of the utilization of all CPUs in the domain
 *
 * Return: the sum of the energy consumed by the CPUs of the domain assuming
 * a capacity state satisfying the max utilization of the domain.
 */
static inline unsigned long em_pd_energy(struct em_perf_domain *pd,
                                unsigned long max_util, unsigned long sum_util)
{
        unsigned long freq, scale_cpu;
        struct em_cap_state *cs;
        int i, cpu;

        /*
         * In order to predict the capacity state, map the utilization of the
         * most utilized CPU of the performance domain to a requested frequency,
         * like schedutil.
         */
        cpu = cpumask_first(to_cpumask(pd->cpus));
        scale_cpu = arch_scale_cpu_capacity(cpu);
        cs = &pd->table[pd->nr_cap_states - 1];
        freq = map_util_freq(max_util, cs->frequency, scale_cpu);

        /*
         * Find the lowest capacity state of the Energy Model above the
         * requested frequency.
         */
        for (i = 0; i < pd->nr_cap_states; i++) {
                cs = &pd->table[i];
                if (cs->frequency >= freq)
                        break;
        }

        /*
         * The capacity of a CPU in the domain at that capacity state (cs)
         * can be computed as:
         *
         *             cs->freq * scale_cpu
         *   cs->cap = --------------------                          (1)
         *                 cpu_max_freq
         *
         * So, ignoring the costs of idle states (which are not available in
         * the EM), the energy consumed by this CPU at that capacity state is
         * estimated as:
         *
         *             cs->power * cpu_util
         *   cpu_nrg = --------------------                          (2)
         *                   cs->cap
         *
         * since 'cpu_util / cs->cap' represents its percentage of busy time.
         *
         *   NOTE: Although the result of this computation actually is in
         *         units of power, it can be manipulated as an energy value
         *         over a scheduling period, since it is assumed to be
         *         constant during that interval.
         *
         * By injecting (1) in (2), 'cpu_nrg' can be re-expressed as a product
         * of two terms:
         *
         *             cs->power * cpu_max_freq   cpu_util
         *   cpu_nrg = ------------------------ * ---------          (3)
         *                    cs->freq            scale_cpu
         *
         * The first term is static, and is stored in the em_cap_state struct
         * as 'cs->cost'.
         *
         * Since all CPUs of the domain have the same micro-architecture, they
         * share the same 'cs->cost', and the same CPU capacity. Hence, the
         * total energy of the domain (which is the simple sum of the energy of
         * all of its CPUs) can be factorized as:
         *
         *            cs->cost * \Sum cpu_util
         *   pd_nrg = ------------------------                       (4)
         *                  scale_cpu
         */
        return cs->cost * sum_util / scale_cpu;
}

perf 도메인의 cpu들이 소모할 에너지 추정치를 알아온다. @max_util은 도메인의 cpu들 중 가장 큰 유틸, @sum_util 에는 도메인의 전체 유틸을 전달한다.

  • 코드 라인 13~16에서 perf 도메인의 첫 cpu의 capacity와 @max_util 값으로 pd 테이블을 통해 적절한 주파수를 알아온다.
  • 코드 라인 22~26에서 위에서 찾은 주파수를 초과하는 cpu가 실제 지원하는 주파수가 담긴 cs(em_cap_state 구조체)를 알아온다.
  • 코드 라인 70에서 cs->cost * @sum_util / pd 도메인내 첫 cpu의 scale된 cpu capacity를 반환한다.

 

map_util_freq()

include/linux/sched/cpufreq.h

static inline unsigned long map_util_freq(unsigned long util,
                                        unsigned long freq, unsigned long cap)
{
        return (freq + (freq >> 2)) * util / cap;
}

freq 1.25배 * util/cap을 반환한다.

 

마이그레이션 후 유틸 예측

cpu_util_next()

kernel/sched/fair.c

/*
 * Predicts what cpu_util(@cpu) would return if @p was migrated (and enqueued)
 * to @dst_cpu.
 */
static unsigned long cpu_util_next(int cpu, struct task_struct *p, int dst_cpu)
{
        struct cfs_rq *cfs_rq = &cpu_rq(cpu)->cfs;
        unsigned long util_est, util = READ_ONCE(cfs_rq->avg.util_avg);

        /*
         * If @p migrates from @cpu to another, remove its contribution. Or,
         * if @p migrates from another CPU to @cpu, add its contribution. In
         * the other cases, @cpu is not impacted by the migration, so the
         * util_avg should already be correct.
         */
        if (task_cpu(p) == cpu && dst_cpu != cpu)
                sub_positive(&util, task_util(p));
        else if (task_cpu(p) != cpu && dst_cpu == cpu)
                util += task_util(p);

        if (sched_feat(UTIL_EST)) {
                util_est = READ_ONCE(cfs_rq->avg.util_est.enqueued);

                /*
                 * During wake-up, the task isn't enqueued yet and doesn't
                 * appear in the cfs_rq->avg.util_est.enqueued of any rq,
                 * so just add it (if needed) to "simulate" what will be
                 * cpu_util() after the task has been enqueued.
                 */
                if (dst_cpu == cpu)
                        util_est += _task_util_est(p);

                util = max(util, util_est);
        }

        return min(util, capacity_orig_of(cpu));
}

태스크 @p를 @cpu에서 @dst_cpu로 마이그레이션한 후를 예측한 @cpu의 유틸을 반환한다.

  • 코드 라인 3~4에서 태스크가 소속된 cfs 런큐의 유틸 평균을 알아와 util에 대입한다.
  • 코드 라인 12~15에서 태스크가 @cpu에서 @dst_cpu로 마이그레이션 하므로 태스크의 cpu와 두 cpu가 관련된 경우 로드를 빼거나 추가해야 한다.
    • 태스크의 cpu가 @cpu이고, @dst_cpu와 @cpu가 다른 경우 util에서 태스크의 유틸 평균을 뺀다. (양수로 제한)
    • 태스크의 cpu가 @cpu가 아니면서 @dst_cpu와 @cpu가 같은 경우 util에 태스크의 유틸 평균을 추가한다.
  • 코드 라인 17~30에서 UTIL_EST feature를 사용하는 경우 태스크의 cpu와 @dst_cpu가 같은 경우 cfs 런큐의 util_est에서 태스크의 util_est를 더한 후 이 값을 util과 비교하여 최대값으로 util을 갱신한다.
    • 매우 짧은 시간 실행되는 태스크의 경우 1ms 단위로 갱신되는 PELT 시그널을 사용한 유틸 로드가 적을 수 있다. 따라서 태스크의 디큐 시마다 산출되는 추정 유틸(estimated utilization)과 기존 유틸 중 큰 값을 사용해야 cpu 간의 더 정확한 유틸 비교를 할 수 있다.
    • 참고: sched/fair: Update util_est only on util_avg updates (2018, v4.17-rc1)
  • 코드 라인 32에서 산출한 util 값이 기존 @cpu의 capacity를 초과하면 안되도록 제한하여 반환한다.

 

남은 여유 capacity

capacity_spare_without()

kernel/sched/fair.c

static unsigned long capacity_spare_without(int cpu, struct task_struct *p)
{
        return max_t(long, capacity_of(cpu) - cpu_util_without(cpu, p), 0);
}

태스크가 사용중인 유틸을 제외한 cpu에 남은 여유 capacity를 반환한다.

  • cpu의 capacity – 태스크가 cpu에 기여한 유틸 로드를 제외한 cpu의 유틸 로드

 

cpu_util_without()

kernel/sched/fair.c

/*
 * cpu_util_without: compute cpu utilization without any contributions from *p
 * @cpu: the CPU which utilization is requested
 * @p: the task which utilization should be discounted
 *
 * The utilization of a CPU is defined by the utilization of tasks currently
 * enqueued on that CPU as well as tasks which are currently sleeping after an
 * execution on that CPU.
 *
 * This method returns the utilization of the specified CPU by discounting the
 * utilization of the specified task, whenever the task is currently
 * contributing to the CPU utilization.
 */
static unsigned long cpu_util_without(int cpu, struct task_struct *p)
{
        struct cfs_rq *cfs_rq;
        unsigned int util;

        /* Task has no contribution or is new */
        if (cpu != task_cpu(p) || !READ_ONCE(p->se.avg.last_update_time))
                return cpu_util(cpu);

        cfs_rq = &cpu_rq(cpu)->cfs;
        util = READ_ONCE(cfs_rq->avg.util_avg);

        /* Discount task's util from CPU's util */
        lsub_positive(&util, task_util(p));

        /*
         * Covered cases:
         *
         * a) if *p is the only task sleeping on this CPU, then:
         *      cpu_util (== task_util) > util_est (== 0)
         *    and thus we return:
         *      cpu_util_without = (cpu_util - task_util) = 0
         *
         * b) if other tasks are SLEEPING on this CPU, which is now exiting
         *    IDLE, then:
         *      cpu_util >= task_util
         *      cpu_util > util_est (== 0)
         *    and thus we discount *p's blocked utilization to return:
         *      cpu_util_without = (cpu_util - task_util) >= 0
         *
         * c) if other tasks are RUNNABLE on that CPU and
         *      util_est > cpu_util
         *    then we use util_est since it returns a more restrictive
         *    estimation of the spare capacity on that CPU, by just
         *    considering the expected utilization of tasks already
         *    runnable on that CPU.
         *
         * Cases a) and b) are covered by the above code, while case c) is
         * covered by the following code when estimated utilization is
         * enabled.
         */
        if (sched_feat(UTIL_EST)) {
                unsigned int estimated =
                        READ_ONCE(cfs_rq->avg.util_est.enqueued);

                /*
                 * Despite the following checks we still have a small window
                 * for a possible race, when an execl's select_task_rq_fair()
                 * races with LB's detach_task():
                 *
                 *   detach_task()
                 *     p->on_rq = TASK_ON_RQ_MIGRATING;
                 *     ---------------------------------- A
                 *     deactivate_task()                   \
                 *       dequeue_task()                     + RaceTime
                 *         util_est_dequeue()              /
                 *     ---------------------------------- B
                 *
                 * The additional check on "current == p" it's required to
                 * properly fix the execl regression and it helps in further
                 * reducing the chances for the above race.
                 */
                if (unlikely(task_on_rq_queued(p) || current == p))
                        lsub_positive(&estimated, _task_util_est(p));

                util = max(util, estimated);
        }

        /*
         * Utilization (estimated) can exceed the CPU capacity, thus let's
         * clamp to the maximum CPU capacity to ensure consistency with
         * the cpu_util call.
         */
        return min_t(unsigned long, util, capacity_orig_of(cpu));
}

태스크가 cpu에 기여한 로드가 있는 경우 그 로드를 제외한 cpu의 유틸 로드를 반환한다.

  • 코드 라인 7~8에서 태스크가 동작했었던 cpu가 아닌 경우 @cpu의 런큐에는 태스크가 기여한 유틸 로드가 없다. 따라서 이러한 경우 cpu의 유틸 로드를 반환한다. 또한 태스크의 로드 평균을 한 번도 갱신한 적이 없는 새 태스크인 경우도 마찬가지이다.
  • 코드 라인 10~14에서 @cpu의 유틸에서 태스크의 유틸을 뺀다.
  • 코드 라인 42~67에서 UTIL_EST feature를 사용하는 경우 유틸과 cfs 런큐의 추정 유틸 둘 중 큰 값을 사용한다. 추정 유틸 값도 태스크가 이미 런큐에 있거나 현재 태스크와 동일한 경우 태스크의 추정 유틸을 뺀 값을 사용한다.
  • 코드 라인 74에서 유틸 값을 반환하되 @cpu의 오리지날 capacity보다 큰 경우에는 clamp 한다.

 

cpu_util()

kernel/sched/fair.c

/**
 * Amount of capacity of a CPU that is (estimated to be) used by CFS tasks
 * @cpu: the CPU to get the utilization of
 *
 * The unit of the return value must be the one of capacity so we can compare
 * the utilization with the capacity of the CPU that is available for CFS task
 * (ie cpu_capacity).
 *
 * cfs_rq.avg.util_avg is the sum of running time of runnable tasks plus the
 * recent utilization of currently non-runnable tasks on a CPU. It represents
 * the amount of utilization of a CPU in the range [0..capacity_orig] where
 * capacity_orig is the cpu_capacity available at the highest frequency
 * (arch_scale_freq_capacity()).
 * The utilization of a CPU converges towards a sum equal to or less than the
 * current capacity (capacity_curr <= capacity_orig) of the CPU because it is
 * the running time on this CPU scaled by capacity_curr.
 *
 * The estimated utilization of a CPU is defined to be the maximum between its
 * cfs_rq.avg.util_avg and the sum of the estimated utilization of the tasks
 * currently RUNNABLE on that CPU.
 * This allows to properly represent the expected utilization of a CPU which
 * has just got a big task running since a long sleep period. At the same time
 * however it preserves the benefits of the "blocked utilization" in
 * describing the potential for other tasks waking up on the same CPU.
 *
 * Nevertheless, cfs_rq.avg.util_avg can be higher than capacity_curr or even
 * higher than capacity_orig because of unfortunate rounding in
 * cfs.avg.util_avg or just after migrating tasks and new task wakeups until
 * the average stabilizes with the new running time. We need to check that the
 * utilization stays within the range of [0..capacity_orig] and cap it if
 * necessary. Without utilization capping, a group could be seen as overloaded
 * (CPU0 utilization at 121% + CPU1 utilization at 80%) whereas CPU1 has 20% of
 * available capacity. We allow utilization to overshoot capacity_curr (but not
 * capacity_orig) as it useful for predicting the capacity required after task
 * migrations (scheduler-driven DVFS).
 *
 * Return: the (estimated) utilization for the specified CPU
 */
static inline unsigned long cpu_util(int cpu)
{
        struct cfs_rq *cfs_rq;
        unsigned int util;

        cfs_rq = &cpu_rq(cpu)->cfs;
        util = READ_ONCE(cfs_rq->avg.util_avg);

        if (sched_feat(UTIL_EST))
                util = max(util, READ_ONCE(cfs_rq->avg.util_est.enqueued));

        return min_t(unsigned long, util, capacity_orig_of(cpu));
}

cpu의 유틸 로드 값을 반환한다.

  • 코드 라인 6~7에서 cpu의 cfs 런큐에서 유틸 로드 평균을 알아온다.
  • 코드 라인 9~10에서 런큐에서 태스크가 1ms 미만으로 동작하는 경우 유틸 로드가 정확히 반영되지 않아 실제보다 작은 값이 사용될 수 있다. 따라서 UTIL_EST feature를 사용하는 경우 태스크의 디큐마다 갱신되는 예상 유틸 로드와 비교하여 큰 값을 사용하면 조금 더 정확한 유틸 로드를 알아낼 수 있다.
  • 코드 라인 12에서 유틸 값을 반환하되 @cpu의 오리지날 capacity보다 큰 경우에는 clamp 한다.

 

EAS enable 여부

sched_energy_enabled()

kernel/sched/sched.h

static inline bool sched_energy_enabled(void)
{
        return static_branch_unlikely(&sched_energy_present);
}

EAS(Energy Aware Scheduling) 기능이 enabled 상태인지 여부를 반환한다.

 


Performance Domain

기존 스케줄링 도메인의 계층 구조는 캐시 기반으로 구성되어 있다. 그러나 EAS를 사용하려면 플랫폼에 대한 더 많은 정보를 알아야 하며 특히 캐시기반과 무관하게 cpu들 포함시킬 수 있는 performance 도메인이 구성되어야 할 때가 있다. pd(performance domain)는 리스트로 연결되어 루트 도메인에 attach 된다. 여러 개의 루트 도메인을 사용하는 경우 각 루트 도메인에서 사용하는 pd의 멤버 cpu는 서로 중복되면 안된다.

 

다음 그림과 같이 ARM Cortex-A75 아키텍처에서 멀티플 파티션을 구성할 수 있음을 보여준다.

 

다음 그림은 performance domain을 구성한 예를 간단히 보여준다.

  • 두 개의 루트 도메인(rd)에 리스트 연결용 pd4가 중복됨을 볼 수 있다.

 

다음 그림은 performance domain을 구성한 예를 조금 더 자세히 구조체 레벨에서 보여준다.

  • 실제 데이터를 관리하는 에너지 모델 pd(em_perf_domain)는 중복되지 않고 pd 숫자만큼만 만들어진다.

 

다음 그림은 여러 개의 루트 도메인을 구성한 경우 EAS를 사용한 wake-up 밸런싱에서 pd들이 루트 도메인들에 포함되는 모습을 볼 수 있다.

  • performance 도메인은 디바이스 트리를 통해서 결정되고, 루트 도메인은 cgroup 설정으로 지정할 수 있다.
  • 두 개의 루트 도메인 예를 보면
    • cpu0~4는 wake-up 밸런싱에서 pd0~pd4에 소속한 cpu 0~6까지의 cpu를 선택할 수 있음을 보여준다.
    • cpu5~7은 wake-up 밸런싱에서 pd4~pd7에 소속한 cpu 4~7까지의 cpu를 선택할 수 있음을 보여준다.

 

“sched_energy_aware” proc

sched_energy_aware_handler()

kernel/sched/topology.c

int sched_energy_aware_handler(struct ctl_table *table, int write,
                         void __user *buffer, size_t *lenp, loff_t *ppos)
{
        int ret, state;

        if (write && !capable(CAP_SYS_ADMIN))
                return -EPERM;

        ret = proc_dointvec_minmax(table, write, buffer, lenp, ppos);
        if (!ret && write) {
                state = static_branch_unlikely(&sched_energy_present);
                if (state != sysctl_sched_energy_aware) {
                        mutex_lock(&sched_energy_mutex);
                        sched_energy_update = 1;
                        rebuild_sched_domains();
                        sched_energy_update = 0;
                        mutex_unlock(&sched_energy_mutex);
                }
        }

        return ret;
}

에너지 모델을 enable/disable하고 스케줄 도메인을 재구성한다.

  • 코드 라인 6~7에서 CAP_SYS_ADMIN capability가 없으면 권한이 없으므로 -EPERM을 반환한다.
  • 코드 라인 9~19에서 sched_energy_present가 동작 중이면 sysctl_sched_energy_aware로 기록한 값이 변경된 경우 스케줄 도메인을 재구성하며 그 동안 sched_energy_update를 1로 유지한다.
    • “/proc/sys/kernel/sched_energy_aware”

 

rebuild_sched_domains()

kernel/sched/topology.c

void rebuild_sched_domains(void)
{
        get_online_cpus();
        percpu_down_write(&cpuset_rwsem);
        rebuild_sched_domains_locked();
        percpu_up_write(&cpuset_rwsem);
        put_online_cpus();
}

스케줄 도메인들을 재구성한다.

 

rebuild_sched_domains_locked()

kernel/sched/topology.c

/*
 * Rebuild scheduler domains.
 *
 * If the flag 'sched_load_balance' of any cpuset with non-empty
 * 'cpus' changes, or if the 'cpus' allowed changes in any cpuset
 * which has that flag enabled, or if any cpuset with a non-empty
 * 'cpus' is removed, then call this routine to rebuild the
 * scheduler's dynamic sched domains.
 *
 * Call with cpuset_mutex held.  Takes get_online_cpus().
 */
static void rebuild_sched_domains_locked(void)
{
        struct sched_domain_attr *attr;
        cpumask_var_t *doms;
        int ndoms;

        lockdep_assert_cpus_held();
        percpu_rwsem_assert_held(&cpuset_rwsem);

        /*
         * We have raced with CPU hotplug. Don't do anything to avoid
         * passing doms with offlined cpu to partition_sched_domains().
         * Anyways, hotplug work item will rebuild sched domains.
         */
        if (!top_cpuset.nr_subparts_cpus &&
            !cpumask_equal(top_cpuset.effective_cpus, cpu_active_mask))
                return;

        if (top_cpuset.nr_subparts_cpus &&
           !cpumask_subset(top_cpuset.effective_cpus, cpu_active_mask))
                return;

        /* Generate domain masks and attrs */
        ndoms = generate_sched_domains(&doms, &attr);

        /* Have scheduler rebuild the domains */
        partition_and_rebuild_sched_domains(ndoms, doms, attr);
}

스케줄 도메인들을 재구성한다.

 

cpuset cpu 범위 관련

  • cpus_allowed
    • cpuset.cpus에서 지정한 cpu들에 대한 비트마스크
  •  effective_cpus
    • cpuset.cpus에서 지정한 cpu들 중 online cpu에 대한 비트마스크
  • subpart_cpus
    • 부모 effective_cpus 중 현재 effective_cpus에 없는 cpu들에 대한 비트마스크 (offline 포함)

 

다음 그림과 같이 cpuset.cpus를 통해 설정된 cpu들의 범위를 확인할 수 있다.

 

generate_sched_domains()

kernel/cgroup/cpuset.c -1/2-

/*
 * generate_sched_domains()
 *
 * This function builds a partial partition of the systems CPUs
 * A 'partial partition' is a set of non-overlapping subsets whose
 * union is a subset of that set.
 * The output of this function needs to be passed to kernel/sched/core.c
 * partition_sched_domains() routine, which will rebuild the scheduler's
 * load balancing domains (sched domains) as specified by that partial
 * partition.
 *
 * See "What is sched_load_balance" in Documentation/admin-guide/cgroup-v1/cpusets.rst
 * for a background explanation of this.
 *
 * Does not return errors, on the theory that the callers of this
 * routine would rather not worry about failures to rebuild sched
 * domains when operating in the severe memory shortage situations
 * that could cause allocation failures below.
 *
 * Must be called with cpuset_mutex held.
 *
 * The three key local variables below are:
 *    cp - cpuset pointer, used (together with pos_css) to perform a
 *         top-down scan of all cpusets. For our purposes, rebuilding
 *         the schedulers sched domains, we can ignore !is_sched_load_
 *         balance cpusets.
 *  csa  - (for CpuSet Array) Array of pointers to all the cpusets
 *         that need to be load balanced, for convenient iterative
 *         access by the subsequent code that finds the best partition,
 *         i.e the set of domains (subsets) of CPUs such that the
 *         cpus_allowed of every cpuset marked is_sched_load_balance
 *         is a subset of one of these domains, while there are as
 *         many such domains as possible, each as small as possible.
 * doms  - Conversion of 'csa' to an array of cpumasks, for passing to
 *         the kernel/sched/core.c routine partition_sched_domains() in a
 *         convenient format, that can be easily compared to the prior
 *         value to determine what partition elements (sched domains)
 *         were changed (added or removed.)
 *
 * Finding the best partition (set of domains):
 *      The triple nested loops below over i, j, k scan over the
 *      load balanced cpusets (using the array of cpuset pointers in
 *      csa[]) looking for pairs of cpusets that have overlapping
 *      cpus_allowed, but which don't have the same 'pn' partition
 *      number and gives them in the same partition number.  It keeps
 *      looping on the 'restart' label until it can no longer find
 *      any such pairs.
 *
 *      The union of the cpus_allowed masks from the set of
 *      all cpusets having the same 'pn' value then form the one
 *      element of the partition (one sched domain) to be passed to
 *      partition_sched_domains().
 */
static int generate_sched_domains(cpumask_var_t **domains,
                        struct sched_domain_attr **attributes)
{
        struct cpuset *cp;      /* top-down scan of cpusets */
        struct cpuset **csa;    /* array of all cpuset ptrs */
        int csn;                /* how many cpuset ptrs in csa so far */
        int i, j, k;            /* indices for partition finding loops */
        cpumask_var_t *doms;    /* resulting partition; i.e. sched domains */
        struct sched_domain_attr *dattr;  /* attributes for custom domains */
        int ndoms = 0;          /* number of sched domains in result */
        int nslot;              /* next empty doms[] struct cpumask slot */
        struct cgroup_subsys_state *pos_css;
        bool root_load_balance = is_sched_load_balance(&top_cpuset);

        doms = NULL;
        dattr = NULL;
        csa = NULL;

        /* Special case for the 99% of systems with one, full, sched domain */
        if (root_load_balance && !top_cpuset.nr_subparts_cpus) {
                ndoms = 1;
                doms = alloc_sched_domains(ndoms);
                if (!doms)
                        goto done;

                dattr = kmalloc(sizeof(struct sched_domain_attr), GFP_KERNEL);
                if (dattr) {
                        *dattr = SD_ATTR_INIT;
                        update_domain_attr_tree(dattr, &top_cpuset);
                }
                cpumask_and(doms[0], top_cpuset.effective_cpus,
                            housekeeping_cpumask(HK_FLAG_DOMAIN));

                goto done;
        }

        csa = kmalloc_array(nr_cpusets(), sizeof(cp), GFP_KERNEL);
        if (!csa)
                goto done;
        csn = 0;

        rcu_read_lock();
        if (root_load_balance)
                csa[csn++] = &top_cpuset;
        cpuset_for_each_descendant_pre(cp, pos_css, &top_cpuset) {
                if (cp == &top_cpuset)
                        continue;
                /*
                 * Continue traversing beyond @cp iff @cp has some CPUs and
                 * isn't load balancing.  The former is obvious.  The
                 * latter: All child cpusets contain a subset of the
                 * parent's cpus, so just skip them, and then we call
                 * update_domain_attr_tree() to calc relax_domain_level of
                 * the corresponding sched domain.
                 *
                 * If root is load-balancing, we can skip @cp if it
                 * is a subset of the root's effective_cpus.
                 */
                if (!cpumask_empty(cp->cpus_allowed) &&
                    !(is_sched_load_balance(cp) &&
                      cpumask_intersects(cp->cpus_allowed,
                                         housekeeping_cpumask(HK_FLAG_DOMAIN))))
                        continue;

                if (root_load_balance &&
                    cpumask_subset(cp->cpus_allowed, top_cpuset.effective_cpus))
                        continue;

                if (is_sched_load_balance(cp) &&
                    !cpumask_empty(cp->effective_cpus))
                        csa[csn++] = cp;

                /* skip @cp's subtree if not a partition root */
                if (!is_partition_root(cp))
                        pos_css = css_rightmost_descendant(pos_css);
        }
        rcu_read_unlock();

        for (i = 0; i < csn; i++)
                csa[i]->pn = i;
        ndoms = csn;

스케줄 도메인을 생성한다.

  • 코드 라인 13에서 cgroup의 top cpuset에 있는 “cpuset.sched_load_balance” 설정 값 여부에 따라 해당 cpu들이 싱글 스케줄 도메인에 소속되어 로드 밸런스에 참여한다. (디폴트=1)
    • 위의 값이 1인 경우 cgroup의 “cpuset.cpus”에 속한 cpu들이 기본 로드 밸런스에서 동작한다. (디폴트=모든 cpu) 위의 값이 0인 경우 기본 로드 밸런스에서 제외된다. 이 때에는 별도의 파티션 도메인을 만들 때 구성원으로 참여 시킬 수 있다.
  • 코드 라인 20~35에서 별도의 서브 파티션이 없는 경우 해당 cpu들을 대상으로 로드 밸런싱에 참여하게 하도록 싱글 스케줄 도메인을 구성한 후 done 레이블로 이동한다.
  • 코드 라인 37~39에서 이미 생성된 cpuset 수 만큼 cpuset 구조체 포인터를 할당하여 csa에 대입한다.
  • 코드 라인 43~44에서 싱글 스케줄 도메인이 포함되는 경우 첫 번째 csa[0]는 top cpuset을 가리키게 한다.
  • 코드 라인 45~67에서 cpuset들을 대상으로 child 우선 방향으로 순회하며 다음 cpuset들은 제외한다.
    • top cpuset인 경우
    • 순회 중인 cpuset에 cpu들을 지정하였으면서, 로드 밸런싱이 설정되지 않았거나 “isolcpus=” 커널 파라미터로 지정된 cpu들로 다 사용할 수 있는 cpu가 없는 경우
    • 루트에서 로드 밸런싱이 동작하면서 지정한 cpu들은 모두 루트의 effective cpu들에 포함된 경우
  • 코드 라인 69~71에서 순회 중인 cpuset이 기본 로드 밸런싱이 설정되었고 effective_cpus들이 있는 경우 해당 cpuset을 csa[]에 추가한다.
  • 코드 라인 74~75에서 cpuset이 파티션 루트가 아닌 하위 cpuset들은 한꺼번에 skip 한다.
  • 코드 라인 79~81에서 위 루프에서 지정하여 사용가능한 csa[]->pn에 일련번호를 0부터 대입하고, 도메인의 수를 결정한다.

 

kernel/cgroup/cpuset.c -2/2-

restart:
        /* Find the best partition (set of sched domains) */
        for (i = 0; i < csn; i++) {
                struct cpuset *a = csa[i];
                int apn = a->pn;

                for (j = 0; j < csn; j++) {
                        struct cpuset *b = csa[j];
                        int bpn = b->pn;

                        if (apn != bpn && cpusets_overlap(a, b)) {
                                for (k = 0; k < csn; k++) {
                                        struct cpuset *c = csa[k];

                                        if (c->pn == bpn)
                                                c->pn = apn;
                                }
                                ndoms--;        /* one less element */
                                goto restart;
                        }
                }
        }

        /*
         * Now we know how many domains to create.
         * Convert <csn, csa> to <ndoms, doms> and populate cpu masks.
         */
        doms = alloc_sched_domains(ndoms);
        if (!doms)
                goto done;

        /*
         * The rest of the code, including the scheduler, can deal with
         * dattr==NULL case. No need to abort if alloc fails.
         */
        dattr = kmalloc_array(ndoms, sizeof(struct sched_domain_attr),
                              GFP_KERNEL);

        for (nslot = 0, i = 0; i < csn; i++) {
                struct cpuset *a = csa[i];
                struct cpumask *dp;
                int apn = a->pn;

                if (apn < 0) {
                        /* Skip completed partitions */
                        continue;
                }

                dp = doms[nslot];

                if (nslot == ndoms) {
                        static int warnings = 10;
                        if (warnings) {
                                pr_warn("rebuild_sched_domains confused: nslot %d, ndoms %d, csn %d, i %d, apn %d\n",
                                        nslot, ndoms, csn, i, apn);
                                warnings--;
                        }
                        continue;
                }

                cpumask_clear(dp);
                if (dattr)
                        *(dattr + nslot) = SD_ATTR_INIT;
                for (j = i; j < csn; j++) {
                        struct cpuset *b = csa[j];

                        if (apn == b->pn) {
                                cpumask_or(dp, dp, b->effective_cpus);
                                cpumask_and(dp, dp, housekeeping_cpumask(HK_FLAG_DOMAIN));
                                if (dattr)
                                        update_domain_attr_tree(dattr + nslot, b);

                                /* Done with this partition */
                                b->pn = -1;
                        }
                }
                nslot++;
        }
        BUG_ON(nslot != ndoms);

done:
        kfree(csa);

        /*
         * Fallback to the default domain if kmalloc() failed.
         * See comments in partition_sched_domains().
         */
        if (doms == NULL)
                ndoms = 1;

        *domains    = doms;
        *attributes = dattr;
        return ndoms;
}
  • 코드 라인 1~22에서 restart: 레이블이다. cpuset들에서 effective_cpus가 중첩되는 도메인을 제거한 수를 알아낸다.
    • csn 수 만큼 i, j, k 이터레이터를 가진 3 중첩 루프를 사용한다.
    • 처음 i, j로 2 중첩 루프를 돌면서 csa[i]->pn 값과 csn[j]->pn 값이 다르고 csa[i]->effiective_cpus와 csa[j]->effective_cpus가 중첩되는 경우에 한해 k로 3 중첩 루프를 돌면서 j, k에 해당하는 pn이 같을 때 k에 해당하는 pn에 i에 해당하는 pn 값으로 대입한다. 그 후 도메인 수(ndoms)를 감소시키고 restart: 레이블로 이동하여 다시 시작한다.
  • 코드 라인 28~37에서 중첩 도메인을 제거한 최종 결정된 도메인 수 만큼 도메인 마스크와 도메인 속성을 할당한다.
  • 코드 라인 39~78에서 원래 도메인 수(csn)만큼 nslot으로 1차 순회하고, 다시 nslot부터 원래 도메인 수까지(csn) 2차 순회하며 같은 pn에 대해 effective_cpus를 더해 update_domain_attr_tree() 함수를 호출한다.
    • 한 번 생성한 파티션은 다시 만들지 않도록 pn에 -1을 대입한다.
  • 코드 라인 81~92에서 done: 레이블이다. 임시로 만들어 사용했던 csa[] 배열을 할당 해제하고, 출력 인자 *domains에 결정된 도메인 수만큼 중복된 도메인을 합친 cpu 마스크를 반환하고, 출력인자 *dattr에는 결정된 도메인 수만큼의 도메인 속성을 반환한다.
  • 코드 라인 93에서 최종 결정된 도메인 수를 반환한다.

 

다음 그립은 generate_sched_domains() 함수에서 오버랩된 cpu를 가진 도메인을 묶어 도메인 수를 줄인 과정을 보여준다.

 

update_domain_attr_tree()

kernel/cgroup/cpuset.c

static void update_domain_attr_tree(struct sched_domain_attr *dattr,
                                    struct cpuset *root_cs)
{
        struct cpuset *cp;
        struct cgroup_subsys_state *pos_css;

        rcu_read_lock();
        cpuset_for_each_descendant_pre(cp, pos_css, root_cs) {
                /* skip the whole subtree if @cp doesn't have any CPU */
                if (cpumask_empty(cp->cpus_allowed)) {
                        pos_css = css_rightmost_descendant(pos_css);
                        continue;
                }

                if (is_sched_load_balance(cp))
                        update_domain_attr(dattr, cp);
        }
        rcu_read_unlock();
}

도메인 속성 @dattr을 @root_cs 이하 모든 cpuset에 적용한다.

  • 코드 라인 8~14에서 @root_cs 이하 child 우선으로 순회하며 지정된 cpu가 없는 경우 skip 한다.
    • “cpuset.cpus”
  • 코드 라인 16~17에서 로드 밸런싱이 설정된 cpuset인 경우 도메인 속성을 갱신한다.
    • “cpuset.sched_load_balance”

 

partition_and_rebuild_sched_domains()

kernel/sched/topology.c

static void
partition_and_rebuild_sched_domains(int ndoms_new, cpumask_var_t doms_new[],
                                    struct sched_domain_attr *dattr_new)
{
        mutex_lock(&sched_domains_mutex);
        partition_sched_domains_locked(ndoms_new, doms_new, dattr_new);
        rebuild_root_domains();
        mutex_unlock(&sched_domains_mutex);
}

분할된 스케줄 도메인들과 루트 도메인들을 재구성한다.

 

partition_sched_domains()

kernel/sched/topology.c

/*
 * Call with hotplug lock held
 */
void partition_sched_domains(int ndoms_new, cpumask_var_t doms_new[],
                             struct sched_domain_attr *dattr_new)
{
        mutex_lock(&sched_domains_mutex);
        partition_sched_domains_locked(ndoms_new, doms_new, dattr_new);
        mutex_unlock(&sched_domains_mutex);
}

분할된 스케줄 도메인들을 재구성한다.

 

partition_sched_domains_locked()

kernel/sched/topology.c -1/2-

/*
 * Partition sched domains as specified by the 'ndoms_new'
 * cpumasks in the array doms_new[] of cpumasks. This compares
 * doms_new[] to the current sched domain partitioning, doms_cur[].
 * It destroys each deleted domain and builds each new domain.
 *
 * 'doms_new' is an array of cpumask_var_t's of length 'ndoms_new'.
 * The masks don't intersect (don't overlap.) We should setup one
 * sched domain for each mask. CPUs not in any of the cpumasks will
 * not be load balanced. If the same cpumask appears both in the
 * current 'doms_cur' domains and in the new 'doms_new', we can leave
 * it as it is.
 *
 * The passed in 'doms_new' should be allocated using
 * alloc_sched_domains.  This routine takes ownership of it and will
 * free_sched_domains it when done with it. If the caller failed the
 * alloc call, then it can pass in doms_new == NULL && ndoms_new == 1,
 * and partition_sched_domains() will fallback to the single partition
 * 'fallback_doms', it also forces the domains to be rebuilt.
 *
 * If doms_new == NULL it will be replaced with cpu_online_mask.
 * ndoms_new == 0 is a special case for destroying existing domains,
 * and it will not create the default domain.
 *
 * Call with hotplug lock and sched_domains_mutex held
 */
void partition_sched_domains_locked(int ndoms_new, cpumask_var_t doms_new[],
                                    struct sched_domain_attr *dattr_new)
{
        bool __maybe_unused has_eas = false;
        int i, j, n;
        int new_topology;

        lockdep_assert_held(&sched_domains_mutex);

        /* Always unregister in case we don't destroy any domains: */
        unregister_sched_domain_sysctl();

        /* Let the architecture update CPU core mappings: */
        new_topology = arch_update_cpu_topology();

        if (!doms_new) {
                WARN_ON_ONCE(dattr_new);
                n = 0;
                doms_new = alloc_sched_domains(1);
                if (doms_new) {
                        n = 1;
                        cpumask_and(doms_new[0], cpu_active_mask,
                                    housekeeping_cpumask(HK_FLAG_DOMAIN));
                }
        } else {
                n = ndoms_new;
        }

        /* Destroy deleted domains: */
        for (i = 0; i < ndoms_cur; i++) {
                for (j = 0; j < n && !new_topology; j++) {
                        if (cpumask_equal(doms_cur[i], doms_new[j]) &&
                            dattrs_equal(dattr_cur, i, dattr_new, j)) {
                                struct root_domain *rd;

                                /*
                                 * This domain won't be destroyed and as such
                                 * its dl_bw->total_bw needs to be cleared.  It
                                 * will be recomputed in function
                                 * update_tasks_root_domain().
                                 */
                                rd = cpu_rq(cpumask_any(doms_cur[i]))->rd;
                                dl_clear_root_domain(rd);
                                goto match1;
                        }
                }
                /* No match - a current sched domain not in new doms_new[] */
                detach_destroy_domains(doms_cur[i]);
match1:
                ;
        }

        n = ndoms_cur;
        if (!doms_new) {
                n = 0;
                doms_new = &fallback_doms;
                cpumask_and(doms_new[0], cpu_active_mask,
                            housekeeping_cpumask(HK_FLAG_DOMAIN));
        }

        /* Build new domains: */
        for (i = 0; i < ndoms_new; i++) {
                for (j = 0; j < n && !new_topology; j++) {
                        if (cpumask_equal(doms_new[i], doms_cur[j]) &&
                            dattrs_equal(dattr_new, i, dattr_cur, j))
                                goto match2;
                }
                /* No match - add a new doms_new */
                build_sched_domains(doms_new[i], dattr_new ? dattr_new + i : NULL);
match2:
                ;
        }

분할된 스케줄 도메인들을 재구성한다.

  • 코드 라인 11에서 “/proc/sys/kernel/sched_domain” 디렉토리를 등록 해제한다.
  • 코드 라인 14에서 cpu topology 변경 여부를 알아온다. (1=변경)
  • 코드 라인 16~27에서 두 번째 인자 doms_new가 지정되지 않은 경우 isolcpus를 제외한 active cpu들로 1개의 도메인을 할당한다. doms_new가 지정된 경우 도메인 수로 첫 번째 인자 ndoms_new 에서 받은 값을 사용한다.
  • 코드 라인 30~51에서 기존 동작 중인 도메인들을 순회하며 토플로지 변경이 있으면 기존 도메인을 detach한 후 제거한다. 만일 토플로지 변경이 없으면 기존 도메인들 중 기존 cpu와 속성이 같은 경우 detach 및 제거하지 않고 루트 도메인의 dl 밴드위드만 클리어하고, 다른 경우에만 기존 도메인을 detach한 후 제거한다.
  • 코드 라인 53~59에서 새로운 도메인이 요청되지 않은 경우 isolcpus를 제외한 active cpu들로 구성하는 1개의fallback_doms을 지정한다.
  • 코드 라인 62~72에서 새 도메인들을 순회하며 토플로지 변경이 있으면 도메인을 새로 만든다. 만일 토플로지 변경이 없으면 기존 도메인들 중 cpu 및 속성이 같은 경우 새로 만들지 않고,다른 경우에만 도메인을 새로 만든다.

 

kernel/sched/topology.c -2/2-

#if defined(CONFIG_ENERGY_MODEL) && defined(CONFIG_CPU_FREQ_GOV_SCHEDUTIL)
        /* Build perf. domains: */
        for (i = 0; i < ndoms_new; i++) {
                for (j = 0; j < n && !sched_energy_update; j++) {
                        if (cpumask_equal(doms_new[i], doms_cur[j]) &&
                            cpu_rq(cpumask_first(doms_cur[j]))->rd->pd) {
                                has_eas = true;
                                goto match3;
                        }
                }
                /* No match - add perf. domains for a new rd */
                has_eas |= build_perf_domains(doms_new[i]);
match3:
                ;
        }
        sched_energy_set(has_eas);
#endif

        /* Remember the new sched domains: */
        if (doms_cur != &fallback_doms)
                free_sched_domains(doms_cur, ndoms_cur);

        kfree(dattr_cur);
        doms_cur = doms_new;
        dattr_cur = dattr_new;
        ndoms_cur = ndoms_new;

        register_sched_domain_sysctl();
}
  • 코드 라인 3~15에서 EAS를 위해 새 도메인 수 만큼 순회하며 다음의 EAS 동작 변경으로 인한 스케줄 도메인 변경이 진행 중인 경우 perf 도메인을 만든다. 만일 스케줄 도메인 변경이 진행중이 아닌 경우 새 도메인들 중 cpu들이 같고  첫 번째 cpu의 perf 도메인도 이미 있는 경우 perf 도메인을 만들지 않고, 그 외의 경우 perf 도메인을 새로 만든다.
    • “/proc/sys/kernel/sched_energy_aware”
  • 코드 라인 16에서 EAS를 enable/disable 시키고 다음과 같은 로그 중 하나를 출력한다.
    • “sched_energy_set: starting EAS”
    • “sched_energy_set: stopping EAS”
  • 코드 라인 20~23에서 기존 도메인들과 속성들을 할당 해제한다.
  • 코드 라인 24~26에서 새 도메인과 속성 정보를 전역 변수에 지정한다.
  • 코드 라인 28에서 “/proc/sys/kernel/sched_domain” 디렉토리를 등록한다.

 

build_perf_domains()

kernel/sched/topology.c

static bool build_perf_domains(const struct cpumask *cpu_map)
{
        int i, nr_pd = 0, nr_cs = 0, nr_cpus = cpumask_weight(cpu_map);
        struct perf_domain *pd = NULL, *tmp;
        int cpu = cpumask_first(cpu_map);
        struct root_domain *rd = cpu_rq(cpu)->rd;
        struct cpufreq_policy *policy;
        struct cpufreq_governor *gov;

        if (!sysctl_sched_energy_aware)
                goto free;

        /* EAS is enabled for asymmetric CPU capacity topologies. */
        if (!per_cpu(sd_asym_cpucapacity, cpu)) {
                if (sched_debug()) {
                        pr_info("rd %*pbl: CPUs do not have asymmetric capacities\n",
                                        cpumask_pr_args(cpu_map));
                }
                goto free;
        }

        for_each_cpu(i, cpu_map) {
                /* Skip already covered CPUs. */
                if (find_pd(pd, i))
                        continue;

                /* Do not attempt EAS if schedutil is not being used. */
                policy = cpufreq_cpu_get(i);
                if (!policy)
                        goto free;
                gov = policy->governor;
                cpufreq_cpu_put(policy);
                if (gov != &schedutil_gov) {
                        if (rd->pd)
                                pr_warn("rd %*pbl: Disabling EAS, schedutil is mandatory\n",
                                                cpumask_pr_args(cpu_map));
                        goto free;
                }

                /* Create the new pd and add it to the local list. */
                tmp = pd_init(i);
                if (!tmp)
                        goto free;
                tmp->next = pd;
                pd = tmp;

                /*
                 * Count performance domains and capacity states for the
                 * complexity check.
                 */
                nr_pd++;
                nr_cs += em_pd_nr_cap_states(pd->em_pd);
        }

        /* Bail out if the Energy Model complexity is too high. */
        if (nr_pd * (nr_cs + nr_cpus) > EM_MAX_COMPLEXITY) {
                WARN(1, "rd %*pbl: Failed to start EAS, EM complexity is too high\n",
                                                cpumask_pr_args(cpu_map));
                goto free;
        }

        perf_domain_debug(cpu_map, pd);

        /* Attach the new list of performance domains to the root domain. */
        tmp = rd->pd;
        rcu_assign_pointer(rd->pd, pd);
        if (tmp)
                call_rcu(&tmp->rcu, destroy_perf_domain_rcu);

        return !!pd;

free:
        free_pd(pd);
        tmp = rd->pd;
        rcu_assign_pointer(rd->pd, NULL);
        if (tmp)
                call_rcu(&tmp->rcu, destroy_perf_domain_rcu);

        return false;
}

1개의 도메인에 해당하는 @cpu_map 비트마스크 멤버로 perf 도메인들을 구성한다. (1개의 도메인내에 파티션이 하나 이상 존재할 수 있다.)

  • 코드 라인 10~11에서 다음 EAS 기능이 disable된 경우 free 레이블로 이동한다.
    • “/proc/sys/kernel/sched_energy_aware”
  • 코드 라인 14~20에서 빅/리틀 같은 asym cpu capacity 도메인이 없는 경우 EAS 기능을 동작시킬 수 없으므로 정보를 출력한 후 free 레이블로 이동한다.
  • 코드 라인 22~25에서 cpu_map 비트마스크에 설정된 cpu 비트만큼 순회하며 해당 cpu가 멤버인 perf 도메인이 이미 만들어져 있는 경우 skip 한다.
  • 코드 라인 27~29에서 cpufreq policy를 가져온다. 만일 schedutil이 활성화되지 않아 가져올 수 없으면 EAS를 사용하지 못하므로 free 레이블로 이동한다.
  • 코드 라인 30~37에서 policy의 governor가 schedutil인 경우에만 EAS를 사용할 수 있다. 따라서 schedutil이 아닌 경우 free 레이블로 이동한다.
  • 코드 라인 40~44에서 perf 도메인을 만들고 리스트 연결한다.
  • 코드 라인 50~52에서 perf 도메인 수를 증가시키고, em_pd의 cap_states 수를 증가시키고 계속 순회한다.
  • 코드 라인 55~59에서 만일 너무 많은 pd가 구성되는 경우 에너지모델이 너무 복잡해지므로 free 레이블로 이동한다.
    • (cap state 수 + cpu 수) * pd 수 > 2048개
  • 코드 라인 61에서 pd 정보를 디버그 출력한다.
  • 코드 라인 64~67에서 rcu를 사용하여 루트 도메인의 기존 domain은 제거하고, 새롭게 생성한 perf 도메인을 지정한다.
  • 코드 라인 69에서 pd 생성 여부를 반환한다.
  • 코드 라인 71~78에서 free: 레이블이다. pd를 제거하고, 루트 도메인이 null pd를 지정하게한 후 false를 반환한다.

 

EM_MAX_COMPLEXITY

kernel/sched/topology.c

/*
 * EAS can be used on a root domain if it meets all the following conditions:
 *    1. an Energy Model (EM) is available;
 *    2. the SD_ASYM_CPUCAPACITY flag is set in the sched_domain hierarchy.
 *    3. the EM complexity is low enough to keep scheduling overheads low;
 *    4. schedutil is driving the frequency of all CPUs of the rd;
 *
 * The complexity of the Energy Model is defined as:
 *
 *              C = nr_pd * (nr_cpus + nr_cs)
 *
 * with parameters defined as:
 *  - nr_pd:    the number of performance domains
 *  - nr_cpus:  the number of CPUs
 *  - nr_cs:    the sum of the number of capacity states of all performance
 *              domains (for example, on a system with 2 performance domains,
 *              with 10 capacity states each, nr_cs = 2 * 10 = 20).
 *
 * It is generally not a good idea to use such a model in the wake-up path on
 * very complex platforms because of the associated scheduling overheads. The
 * arbitrary constraint below prevents that. It makes EAS usable up to 16 CPUs
 * with per-CPU DVFS and less than 8 capacity states each, for example.
 */
#define EM_MAX_COMPLEXITY 2048

(cap state 수 + cpu 수) * pd 수가 2048개를 초과하는 경우 너무 복잡하여 EAS를 사용할 수 없다.

 

pd_init()

kernel/sched/topology.c

static struct perf_domain *pd_init(int cpu)
{
        struct em_perf_domain *obj = em_cpu_get(cpu);
        struct perf_domain *pd;

        if (!obj) {
                if (sched_debug())
                        pr_info("%s: no EM found for CPU%d\n", __func__, cpu);
                return NULL;
        }

        pd = kzalloc(sizeof(*pd), GFP_KERNEL);
        if (!pd)
                return NULL;
        pd->em_pd = obj;

        return pd;
}

performance 도메인을 할당하고 초기화한 후 반환한다.

 

find_pd()

kernel/sched/topology.c

static struct perf_domain *find_pd(struct perf_domain *pd, int cpu)
{
        while (pd) {
                if (cpumask_test_cpu(cpu, perf_domain_span(pd)))
                        return pd;
                pd = pd->next;
        }

        return NULL;
}

performance 도메인들 내에 속한 @cpu를 찾는다.

  • performance 도메인들은 리스트로 연결되어 있다.

 


Perf 도메인의 EM(Energy Model) 추가

다음은 cpufreq 디바이스 드라이버 초기화 루틴을 통해 dev_pm_opp_of_register_em() 함수를 호출하여 perf 도메인의 em을 추가한 경로 예를 본다.

  • cpufreq_init() – drivers/cpufreq/cpufreq-dt.c
    • dev_pm_opp_of_register_em()
      • em_register_perf_domain()

 

em_register_perf_domain()

kernel/power/energy_model.c

/**
 * em_register_perf_domain() - Register the Energy Model of a performance domain
 * @span        : Mask of CPUs in the performance domain
 * @nr_states   : Number of capacity states to register
 * @cb          : Callback functions providing the data of the Energy Model
 *
 * Create Energy Model tables for a performance domain using the callbacks
 * defined in cb.
 *
 * If multiple clients register the same performance domain, all but the first
 * registration will be ignored.
 *
 * Reteturn 0 on success
 */
int em_register_perf_domain(cpumask_t *span, unsigned int nr_states,
                                                struct em_data_callback *cb)
{
        unsigned long cap, prev_cap = 0;
        struct em_perf_domain *pd;
        int cpu, ret = 0;

        if (!span || !nr_states || !cb)
                return -EINVAL;

        /*
         * Use a mutex to serialize the registration of performance domains and
         * let the driver-defined callback functions sleep.
         */
        mutex_lock(&em_pd_mutex);

        for_each_cpu(cpu, span) {
                /* Make sure we don't register again an existing domain. */
                if (READ_ONCE(per_cpu(em_data, cpu))) {
                        ret = -EEXIST;
                        goto unlock;
                }

                /*
                 * All CPUs of a domain must have the same micro-architecture
                 * since they all share the same table.
                 */
                cap = arch_scale_cpu_capacity(cpu);
                if (prev_cap && prev_cap != cap) {
                        pr_err("CPUs of %*pbl must have the same capacity\n",
                                                        cpumask_pr_args(span));
                        ret = -EINVAL;
                        goto unlock;
                }
                prev_cap = cap;
        }

        /* Create the performance domain and add it to the Energy Model. */
        pd = em_create_pd(span, nr_states, cb);
        if (!pd) {
                ret = -EINVAL;
                goto unlock;
        }

        for_each_cpu(cpu, span) {
                /*
                 * The per-cpu array can be read concurrently from em_cpu_get().
                 * The barrier enforces the ordering needed to make sure readers
                 * can only access well formed em_perf_domain structs.
                 */
                smp_store_release(per_cpu_ptr(&em_data, cpu), pd);
        }

        pr_debug("Created perf domain %*pbl\n", cpumask_pr_args(span));
unlock:
        mutex_unlock(&em_pd_mutex);

        return ret;
}
EXPORT_SYMBOL_GPL(em_register_perf_domain);

perf 도메인의 em을 등록한다.

  • 코드 라인 8~9에서 인자들이 0인 경우 -EINVAL 에러를 반환한다.
  • 코드 라인 17~22에서 @span 비트마스크의 cpu들을 순회하며 em_data에 이미 값이 지정된 경우 -EEXIST 에러를 반환한다.
  • 코드 라인 28~36에서 순회 중인 cpu들의 cpu capacity가 서로 다른 경우 -EINVAL 에러를 반환한다.
  • 코드 라인 39~43에서 em perf 도메인을 생성하고 em들을 추가한다.
  • 코드 라인 45~52에서 @span 비트마스크의 cpu들을 순회하며 em_data에 pd를 지정한다.
  • 코드 라인 54~58에서 pd 생성에 대한 디버그 출력을한 후 결과 값을 반환한다. (성공 시 0)

 

performance 도메인은 다음과 같이 디바이스 트리를 통해서 생성된다.

  • pd0 – little cpu x 2
    • cluster_a53_opp_table 노드 생략
  • pd1 – big cpu x 2
    • cluster_a57_opp_table 노드 생략
        cpus {
                #address-cells = <1>;
                #size-cells = <0>;

                cpu0: cpu@100 {
                        device_type = "cpu";
                        compatible = "arm,cortex-a53";
                        operating-points-v2 = <&cluster_a53_opp_table>;
                        ...
                };

                cpu1: cpu@101 {
                        device_type = "cpu";
                        compatible = "arm,cortex-a53";
                        operating-points-v2 = <&cluster_a53_opp_table>;
                        ...
                };

                cpu2: cpu@0 {
                        device_type = "cpu";
                        compatible = "arm,cortex-a57";
                        operating-points-v2 = <&cluster_a57_opp_table>;
                        ...
                };

                cpu3: cpu@1 {
                        device_type = "cpu";
                        compatible = "arm,cortex-a57";
                        operating-points-v2 = <&cluster_a57_opp_table>;
                        ...
                };
        }

 

em_create_pd()

kernel/power/energy_model.c -1/2-

static struct em_perf_domain *em_create_pd(cpumask_t *span, int nr_states,
                                                struct em_data_callback *cb)
{
        unsigned long opp_eff, prev_opp_eff = ULONG_MAX;
        unsigned long power, freq, prev_freq = 0;
        int i, ret, cpu = cpumask_first(span);
        struct em_cap_state *table;
        struct em_perf_domain *pd;
        u64 fmax;

        if (!cb->active_power)
                return NULL;

        pd = kzalloc(sizeof(*pd) + cpumask_size(), GFP_KERNEL);
        if (!pd)
                return NULL;

        table = kcalloc(nr_states, sizeof(*table), GFP_KERNEL);
        if (!table)
                goto free_pd;

        /* Build the list of capacity states for this performance domain */
        for (i = 0, freq = 0; i < nr_states; i++, freq++) {
                /*
                 * active_power() is a driver callback which ceils 'freq' to
                 * lowest capacity state of 'cpu' above 'freq' and updates
                 * 'power' and 'freq' accordingly.
                 */
                ret = cb->active_power(&power, &freq, cpu);
                if (ret) {
                        pr_err("pd%d: invalid cap. state: %d\n", cpu, ret);
                        goto free_cs_table;
                }

                /*
                 * We expect the driver callback to increase the frequency for
                 * higher capacity states.
                 */
                if (freq <= prev_freq) {
                        pr_err("pd%d: non-increasing freq: %lu\n", cpu, freq);
                        goto free_cs_table;
                }

                /*
                 * The power returned by active_state() is expected to be
                 * positive, in milli-watts and to fit into 16 bits.
                 */
                if (!power || power > EM_CPU_MAX_POWER) {
                        pr_err("pd%d: invalid power: %lu\n", cpu, power);
                        goto free_cs_table;
                }

                table[i].power = power;
                table[i].frequency = prev_freq = freq;

                /*
                 * The hertz/watts efficiency ratio should decrease as the
                 * frequency grows on sane platforms. But this isn't always
                 * true in practice so warn the user if a higher OPP is more
                 * power efficient than a lower one.
                 */
                opp_eff = freq / power;
                if (opp_eff >= prev_opp_eff)
                        pr_warn("pd%d: hertz/watts ratio non-monotonically decreasing: em_cap_state %d >= em_cap_state%dd
\n",
                                        cpu, i, i - 1);
                prev_opp_eff = opp_eff;
        }

em perf 도메인을 생성한다.

  • 코드 라인 6에서 em 정보는 @span 비트마스크의 첫 번째 cpu를 사용한다.
  • 코드 라인 11~12에서 em data 콜백에 (*active_power) 후크 함수가 지정되지 않은 경우 null을 반환한다.
  • 코드 라인 14~20에서 em_perf_domai과 @nr_state 만큼의 테이블을 생성한다.
  • 코드 라인 23~33에서 @nr_states 만큼 순회하며 (*active_power) 후크 함수를 통해 pd 도메인의 첫 번째 cpu에 대한 첫 번째 opp 데이터의 power(mW) 및 frequency(khz) 정보를 알아온다.
  • 코드 라인 39~42에서 frequency 테이블은 항상 증가하여야 한다. 만일 frequency가 증가되지 않은 경우 에러를 출력하고 free_cs_table 레이블로 이동한다.
  • 코드 라인 48~51에서 power 값은 1~0xffff까지 사용한다. 만일 범위 밖인 경우 에러를 출력하고 free_cs_table 레이블로 이동한다.
  • 코드 라인 53~54에서 테이블에 power와 frequency를 추가한다.
  • 코드 라인 62~67에서 operating performance point effective 값을 구한다. (freq / power) 이 값이 감소되지 않는 경우 경고 메시지를 출력한다.

 

kernel/power/energy_model.c -2/2-

        /* Compute the cost of each capacity_state. */
        fmax = (u64) table[nr_states - 1].frequency;
        for (i = 0; i < nr_states; i++) {
                table[i].cost = div64_u64(fmax * table[i].power,
                                          table[i].frequency);
        }

        pd->table = table;
        pd->nr_cap_states = nr_states;
        cpumask_copy(to_cpumask(pd->cpus), span);

        em_debug_create_pd(pd, cpu);

        return pd;

free_cs_table:
        kfree(table);
free_pd:
        kfree(pd);

        return NULL;
}
  • 코드 라인 2~6에서 @nr_states 만큼 순회하며 각 테이블의 cost 값을 산출한다.
    •                                            최대 주파수
    • cost =  power(mW)  * ———————–
    •                                                주파수
  • 코드 라인 8~10에서 em perf 도메인에 테이블과 @nr_states 및 @span 정보를 대입한다.
  • 코드 라인 12에서 em perf 도메인을 위한 sysfs를 생성한다.
  • 코드 라인 14에서 생성된 em perf 도메인을 반환한다.

 

주파수와 파워 알아오기

_get_cpu_power()

drivers/opp/of.c

/*
 * Callback function provided to the Energy Model framework upon registration.
 * This computes the power estimated by @CPU at @kHz if it is the frequency
 * of an existing OPP, or at the frequency of the first OPP above @kHz otherwise
 * (see dev_pm_opp_find_freq_ceil()). This function updates @kHz to the ceiled
 * frequency and @mW to the associated power. The power is estimated as
 * P = C * V^2 * f with C being the CPU's capacitance and V and f respectively
 * the voltage and frequency of the OPP.
 *
 * Returns -ENODEV if the CPU device cannot be found, -EINVAL if the power
 * calculation failed because of missing parameters, 0 otherwise.
 */
static int __maybe_unused _get_cpu_power(unsigned long *mW, unsigned long *kHz,
                                         int cpu)
{
        struct device *cpu_dev;
        struct dev_pm_opp *opp;
        struct device_node *np;
        unsigned long mV, Hz;
        u32 cap;
        u64 tmp;
        int ret;

        cpu_dev = get_cpu_device(cpu);
        if (!cpu_dev)
                return -ENODEV;

        np = of_node_get(cpu_dev->of_node);
        if (!np)
                return -EINVAL;

        ret = of_property_read_u32(np, "dynamic-power-coefficient", &cap);
        of_node_put(np);
        if (ret)
                return -EINVAL;

        Hz = *kHz * 1000;
        opp = dev_pm_opp_find_freq_ceil(cpu_dev, &Hz);
        if (IS_ERR(opp))
                return -EINVAL;

        mV = dev_pm_opp_get_voltage(opp) / 1000;
        dev_pm_opp_put(opp);
        if (!mV)
                return -EINVAL;

        tmp = (u64)cap * mV * mV * (Hz / 1000000);
        do_div(tmp, 1000000000);

        *mW = (unsigned long)tmp;
        *kHz = Hz / 1000;

        return 0;
}

@khz를 포함하는 바로 상위의 주파수에 해당하는 cpu 파워를 알아온다. (입력 인자: @cpu, @mw, 출력 인자: @mw, @khz)

  • 코드 라인 12~14에서 cpu 디바이스가 없으면 -ENODEV 에러를 반환한다.
  • 코드 라인 16~18에서 cpu 노드 정보를 가져온다.
  • 코드 라인 20~23에서 “dynamic-power-coefficient” 속성을 읽어와서 cap에 담아둔다.
    • cpu의 에너지 효율로 리틀 cpu보다 빅 cpu의 값이 더 크다.
  • 코드 라인 25~28에서 요청한 주파수로 인접 상위 opp 데이터를 찾아온다.
  • 코드 라인 30~33에서 opp 데이터에서 volatage를 알아와서 mV로 변환한다.
  • 코드 라인 35~36에서 mW로 변환한다.
    • mW = cap * mV^2 * Mhz / 1000000000
  • 코드 라인 38~39에서 변환한 mW를 출력인자에 대입한다. 또한 주파수도 Khz로 변환하여 출력인자에 대입한다.
  • 코드 라인 41에서 성공 값 0을 반환한다.

 

다음 그림은 _get_cpu_power() 함수를 통해 요청한 주파수를 포함한 상위 인접 opp 데이터에서 mW 및 kHz를 알아오는 과정을 보여준다.

 

dynamic-power-coefficient 속성

디바이스트리의 cpu 노드에서 사용하는 “dynamic-power-coefficient” 속성값은 다음과 같이 산출되고, opp 데이터(전압, 주파수)에서 이 값을 곱해 파워(uW)를 알아내기 위해 사용된다.

 

Energy Model Sysfs

다음은 2개의 perf 도메인을 구성한 모습을 보여준다.

/sys/devices/system/cpu/energy_model
.                             ├── pd0
.                             │   ├── cost
.                             │   ├── cpus
.                             │   ├── frequency
.                             │   └── power
.                             └── pd4
.                                 ├── cost
.                                 ├── cpus
.                                 ├── frequency
.                                 └── power

 

OPP(Operating Performance Point)

다음 그림은 rk3399chip을 사용하는 rock960 보드의 opp 관련 디바이스 트리로부터 cost를 산출한 모습을 보여준다.

  • cpu 4개의 리틀 클러스터(약 400M, 600M, 800M, 1G, 1.2G, 1.4Ghz)
  • cpu 2개의 빅 클러스터(약 400M, 600M, 800M, 1G, 1.2G, 1.4G, 1.6G, 1.8Ghz)

 

참고

 

 

Scheduler -17- (Load Balance 3 NUMA)

<kernel v5.4>

NUMA 밸런싱

NUMA 밸런싱을 시작하면 동작 중인 태스크가 사용하는 메모리 페이지들을 주기적으로 NUMA hinting faults가 발생하도록 언매핑한다. 초당 약 256MB 공간을 처리할 수 있는 주기(period)를 사용하고, 메모리 공간을 상향 이동하며 NUMA 표식(PROT_NONE 매핑 속성)을 사용한채로 언매핑한다. 그 후 특정 태스크가 이 영역에 접근하는 경우 NUMA 표식된 채로 NUMA hinting faults가 발생한다. 이를 통해 각 태스크가 어떤 cpu 노드 또는 메모리 노드에 액세스를 하는지 그 빈도를 누마 faults로 기록하고,  이 값을 사용하여 물리 메모리 페이지를  다른 노드로 마이그레이션하여 사용하거나, 아니면 태스크 자체를 다른 노드로 마이그레이션할 수 있는 선택을 할 수 있도록 도움을 준다.

 

NUMA 밸런싱을 위해 사용되는 다음 항목들을 알아본다.

  • 태스크별 NUMA Scan
  • NUMA hinting fault
    • fault score 수집
  • NUMA 밸런싱
    • page 마이그레이션이 필요한 조건
    • 태스크 마이그레이션이 필요한 조건

 

태스크별 NUMA Scan

  • NUMA 시스템에선 태스크가 생성될 때 마다 NUMA 스캔을 준비 한다.
  • NUMA 스캔
    • 태스크가 사용하는 가상 메모리 영역(VMA)들을 물리메모리와 단절 시키도록 언매핑한다.
    • 언매핑시 PROT_NONE 속성을 사용한다.
      • MMU H/W는 언매핑 상태로 인식
      • Linux 커널 S/W는 fault 발생시 NUMA 스캔 용도로 언매핑하였다는 것을 알아내기 위해 사용
  • VMA 스캐닝은 초당 최대 256MB의 양으로 제한한다.
  • 스캔을 허용하지 않는 VMA들
    • migrate 불가능
    • MOF(Migrate On Fault) 미지원
    • Hugetlb
    • VM_MIXEDMAP 타입
    • readonly 파일 타입(라이브러리 등)
  • 참고: NUMA 스캔이 시작되는 콜백 함수
    • task_numa_work()

 

NUMA Hinting Fault

어떤 노드에서 동작하는 태스크가 Numa 스캔 용도로 언매핑한 메모리에 접근하다 fault가 발생하였을 때 이를 NUMA Hinting fault라고 한다. fault가 발생한 경우 어떠한 노드에서 요청되었는지를 구분하여 faults(접근) 횟수를 몇 가지 필드에 기록하고, 이 값들이 관리되어 노드별 액세스 빈도를 추적하는데 이를 fault score라고 한다. 이 fault score는 해당 태스크와 누마 그룹(ng)으로 나누어 동시에 관리한다. 페이지가 단일 태스크에의해 접근되어 사용되는 경우 해당 태스크의 fault score를 활용하지만, 페이지가 공유 메모리 또는 스레드간에 공유되어 접근되는 공유 페이지인 경우 이들 태스크를 묶어 누마 그룹(ng) 단위로 fault score를 누적하여 활용한다.

  • 참고: NUMA Hinting fault로 시작 함수
    • do_numa_page()

 

fault score 수집 및 관리 방법은 다음과 같다.

  • 기본적으로 faults 발생 시마다 태스크에서 2가지 방법으로 관리한다.
    • p->numa_faults_locaility[]에 local 노드 fault인지 remote 노드 fault인지를 구분하여 단순 증가시켜 누적시킨다.
    • p->numa_faults[]에 fault가 발생한 노드별로 나누어 관리하지만, 그 외에도 추가적으로 다음과 같이 3가지(총 8개) 타입으로 분류하여 관리한다.
      • faults score로 읽어내어 사용하는 mem 노드/cpu 노드 구분
        • NUMA_MEM, NUMA_CPU
        • 아래 단순 누적된 buf 수를 바로 사용하여 누적하지 않고 절반만 누적시켜 사용한다.
        • memless 노드를 가지는 NUMA 시스템을 위해 cpu 노드와 mem 노드를 구분하였다.
      • 단순 누적용 BUF 구분
        • NUMA_MEMBUF, NUMA_CPUBUF
      • 공유 페이지 구분을 위해 private/share로 구분
        • priv=0, priv=1
        • 누마 스캔 주기를 결정하기 위해서만 사용한다.
  • 누마 그룹의 경우 p->numa_faults[]같이 노드별로 나누어 관리하지만 buf 는 제외시킨 2가지(총 4개) 타입으로만 분류하여 ng->faults[]에서 관리한다.
    • faults score로 읽어내어 사용하는 mem 노드/cpu 노드
      • NUMA_MEM, NUMA_CPU
    • 공유 페이지 구분을 위해 private/share로 구분
      • priv=0, priv=1
      • 누마 스캔 주기를 결정하기 위해서만 사용하며, task_scan_max() 함수에서 반환되는 max 주기가 shared 페이지 비율만큼 비례하여 커진다.

 

다음 그림은 태스크의 numa_faults[] 와 누마 그룹의 faults[] 값이 관리되는 모습을 보여준다.

  • faults_cpu 포인터는 NUMA_CPU 배열을 직접가리킨다.
  • 누마 그룹에서 NUMA_MEMBUF 및 NUMA_CPUBUF 관련 faults 항목이 없음을 유의한다.
  • 하늘색 BUF 항목은 fault 발생시마다 누적되며, 누마 스캔 주기 시마다 이 값들을 읽어 내어 연두색 부분으로 옮겨 반영을 한 후 클리어된다.
  • 하늘색 BUF 값이 연두색 기존 값에 반영될 때 다음 값 만큼씩 누적한다.
    • 하늘색 BUF 값 – 연두색 기존 값의 절반

 

예 1) 기존 fault값 0에서 BUF fault 값이 계속 10이 발생한 경우 새 값이 기존 값의 절반을 뺀 차이만큼씩 증가하여 누적되는 것을 알 수 있다.

  • 1st: 0 += 10 – 0/2 = 10
  • 2nd: 10 += 10 – 10/2 = 15
  • 3rd: 15 += 10 – 15/2 = 18
  • 4th: 18 += 10 – 18/2 = 19
  • 5th: 19 += 10 – 19/2 = 20
  • 6th: 20 += 10 – 20/2 = 20
  • 7th: 동일 반복

 

예 2) 기존 fault값 20에서 BUF fault 값이 계속 0이 발생한 경우 절반씩 줄어드는 것을 알 수 있다.

  • 1st: 20 += 0 – 20/2 = 10
  • 2nd: 10 += 0 – 10/2 = 5
  • 3rd: 5 += 0 – 5/2 = 3
  • 4th: 3 += 0 – 3/2 = 2
  • 5th: 2 += 0 – 2/2 = 1
  • 6th: 1 += 0 – 1/2 = 1
  • 7th: 동일 반복

 

페이지 migration 조건

태스크가 실행되는 노드가 아닌 노드에 위치한 페이지에 접근 시 태스크 마이그레이션 조건에 앞서 먼저 다음과 같은 조건인 경우 수행한다.

  •  하나의 task 가 page 에 연속해서 fault가 발생하는 경우
  • 누마 그룹(ng)이 있는 태스크의 경우에만 dst node 가 src node보다 3배 이상 fault score가 높을 때
  • 메모리 리스 노드 시스템에서  메모리를 가지고 있는 노드가 가지고 있지 않는 노드 보다 1.33배 이상 fault score가 높을 때

 

다음 그림은 4개의 노드를 가진 누마 시스템에서 메모리 리스(memless) 노드를 가진 누마 시스템과의 비교를 보여준다.

 

태스크 migration 조건

  • 태스크가 이동이 되는 경우는 최대 fault score를 가진 노드가 우선 노드로 채택된다.
  • 이 우선 노드에서 best cpu로 migrate 또는 swap 둘 중 액세스 성능, 로드 성능 뿐 아니라 태스크 affinity 및 캐시 지역성을 모두 고려하여 정한다.
  • 만일 우선 노드에서 best cpu를 찾지 못하는 경우 다른 노드들을 대상으로 시도한다.

 


NUMA 메모리 Fault score

다음 그림과 같이 누마 그룹의 weight와 태스크의 weight 두 기준에 대한 함수 호출관계를 보여준다.

 

group_weight()

kernel/sched/fair.c

static inline unsigned long group_weight(struct task_struct *p, int nid,
                                         int dist)
{
        struct numa_group *ng = deref_task_numa_group(p);
        unsigned long faults, total_faults;

        if (!ng)
                return 0;

        total_faults = ng->total_faults;

        if (!total_faults)
                return 0;

        faults = group_faults(p, nid);
        faults += score_nearby_nodes(p, nid, dist, false);

        return 1000 * faults / total_faults;
}

태스크의 누마 그룹에 누적된 메모리 faults 값을 해당 누마 그룹의 전체 faults에 대한 가중치 값으로 변환하여 알아온다.  (1000 = 100%)

  • 코드 라인 4~8에서 태스크가 가리키는 누마 그룹이 없으면 0을 반환한다.
  • 코드 라인 10~13에서 누마 그룹의 전체 faults 값이 0인 경우 함수를 빠져나간다.
  • 코드 라인 15에서 누마 그룹에서 관리하고 있는 @nid 노드의 faults 값을 알아온다.
  • 코드 라인 16에서 누마 그룹에서 관리하고 있는 faults에 대해 @nid 노드를 중심으로 전체 노드에 대한 faults 값을 거리별로 반비례 누적시킨 후 알아온 score 값을 faults에 추가한다.
  • 코드 라인 18에서 누마 그룹의 전체 faults에 대해 알아온 faults의 비율을 0~1000 사이 값으로 반환한다.

 

다음 그림은 특정 노드의 faults 값과 다른 노드들의 faults를 score로 변환한 값의 전체 faults에 대한 비율을 산출하는 과정을 보여준다.

 

deref_task_numa_group()

kernel/sched/fair.c

/*
 * For functions that can be called in multiple contexts that permit reading
 * ->numa_group (see struct task_struct for locking rules).
 */
static struct numa_group *deref_task_numa_group(struct task_struct *p)
{
        return rcu_dereference_check(p->numa_group, p == current ||
                (lockdep_is_held(&task_rq(p)->lock) && !READ_ONCE(p->on_cpu)));
}

태스크에 지정된 누마 그룹을 알아온다.

 

group_faults()

kernel/sched/fair.c

static inline unsigned long group_faults(struct task_struct *p, int nid)
{
        struct numa_group *ng = deref_task_numa_group(p);

        if (!ng)
                return 0;

        return ng->faults[task_faults_idx(NUMA_MEM, nid, 0)] +
                ng->faults[task_faults_idx(NUMA_MEM, nid, 1)];
}

태스크가 가리키는 누마 그룹에 누적된 @nid의 메모리 폴트 값을 알아온다.

  • 코드 라인 5~6에서 태스크에 누마 그룹이 설정되지 않은 경우 0을 반환한다.
  • 코드 라인 8~9에서 누마 그룹의 NUMA_MEM의 해당 노드의 두 개 faults[] stat을 더해서 반환한다.

 

task_faults_idx()

kernel/sched/fair.c

/*
 * The averaged statistics, shared & private, memory & CPU,
 * occupy the first half of the array. The second half of the
 * array is for current counters, which are averaged into the
 * first set by task_numa_placement.
 */
static inline int task_faults_idx(enum numa_faults_stats s, int nid, int priv)
{
        return NR_NUMA_HINT_FAULT_TYPES * (s * nr_node_ids + nid) + priv;
}

태스크에서 요청한 누마 @nid의 요청한 타입의 폴트 수를 알아온다.  @priv가 0인 경우 shared stat, 1인 경우 private 카운터 값을 반환한다.

  • 예) 4개의 노드, NUMA_CPU stat, nid=2, priv=0
    • 인덱스 = 2 * (1 * 4 + 2) + 0 = 12
  • 예) 4개의 노드, NUMA_CPU stat, nid=3, priv=0
    • 인덱스 = 2 * (1 * 4 + 3) + 0 = 14

 

task_weight()

kernel/sched/fair.c

/*
 * These return the fraction of accesses done by a particular task, or
 * task group, on a particular numa node.  The group weight is given a
 * larger multiplier, in order to group tasks together that are almost
 * evenly spread out between numa nodes.
 */
static inline unsigned long task_weight(struct task_struct *p, int nid,
                                        int dist)
{
        unsigned long faults, total_faults;

        if (!p->numa_faults)
                return 0;

        total_faults = p->total_numa_faults;

        if (!total_faults)
                return 0;

        faults = task_faults(p, nid);
        faults += score_nearby_nodes(p, nid, dist, true);

        return 1000 * faults / total_faults;
}

태스크에서 관리하고 있는 누마 faults 값을 태스크의 전체 faults에 대한 가중치 값으로 변환하여 알아온다.  (1000 = 100%)

  • 코드 라인 6~7에서 태스크의 누마 메모리 faults가 0인 경우 0을 반환한다.
  • 코드 라인 9~12에서 태스크의 전체 누마 faults 값이 0인 경우 0을 반환한다.
  • 코드 라인 14에서 태스크가 관리하고 있는 누마 faults에 대해 @nid 노드에 해당하는 faults 값을 알아온다.
  • 코드 라인 15에서 태스크에 관리하고 있는 누마 faults에 대해 @nid 노드를 중심으로 다른 노드들에 대한 faults 값을 거리별로 반비례 누적시킨 후 score로 알아온 값을 faults에 추가한다.
  • 코드 라인 17에서 태스크의 전체 누마 faults에 대해 알아온 faults의 비율을 0~1000 사이 값으로 반환한다.

 

task_faults()

kernel/sched/fair.c

static inline unsigned long task_faults(struct task_struct *p, int nid)
{
        if (!p->numa_faults)
                return 0;

        return p->numa_faults[task_faults_idx(NUMA_MEM, nid, 0)] +
                p->numa_faults[task_faults_idx(NUMA_MEM, nid, 1)];
}

태스크에 누적된 @nid의 메모리 폴트 값을 알아온다.

  • faults[] 배열은 2개의 타입 x 노드 수 x 2개와 같은 방법으로 구성되어 있다.
    • faults[4][nid][2]
    • 4개의 타입은 다음과 같다.
      • NUMA_MEM(0), NUMA_CPU(1)
  • faults[] 값들은 시간에 따라 exponential decay 된다.

 

다음 그림은 group_faults() 함수와 task_faults() 함수가 faults 데이터를 가져오는 곳을 보여준다.

 

태스크의 특정 노드에 대한 faults score 산출

score_nearby_nodes()

kernel/sched/fair.c

/* Handle placement on systems where not all nodes are directly connected. */
static unsigned long score_nearby_nodes(struct task_struct *p, int nid,
                                        int maxdist, bool task)
{
        unsigned long score = 0;
        int node;

        /*
         * All nodes are directly connected, and the same distance
         * from each other. No need for fancy placement algorithms.
         */
        if (sched_numa_topology_type == NUMA_DIRECT)
                return 0;

        /*
         * This code is called for each node, introducing N^2 complexity,
         * which should be ok given the number of nodes rarely exceeds 8.
         */
        for_each_online_node(node) {
                unsigned long faults;
                int dist = node_distance(nid, node);

                /*
                 * The furthest away nodes in the system are not interesting
                 * for placement; nid was already counted.
                 */
                if (dist == sched_max_numa_distance || node == nid)
                        continue;

                /*
                 * On systems with a backplane NUMA topology, compare groups
                 * of nodes, and move tasks towards the group with the most
                 * memory accesses. When comparing two nodes at distance
                 * "hoplimit", only nodes closer by than "hoplimit" are part
                 * of each group. Skip other nodes.
                 */
                if (sched_numa_topology_type == NUMA_BACKPLANE &&
                                        dist >= maxdist)
                        continue;

                /* Add up the faults from nearby nodes. */
                if (task)
                        faults = task_faults(p, node);
                else
                        faults = group_faults(p, node);

                /*
                 * On systems with a glueless mesh NUMA topology, there are
                 * no fixed "groups of nodes". Instead, nodes that are not
                 * directly connected bounce traffic through intermediate
                 * nodes; a numa_group can occupy any set of nodes.
                 * The further away a node is, the less the faults count.
                 * This seems to result in good task placement.
                 */
                if (sched_numa_topology_type == NUMA_GLUELESS_MESH) {
                        faults *= (sched_max_numa_distance - dist);
                        faults /= (sched_max_numa_distance - LOCAL_DISTANCE);
                }

                score += faults;
        }

        return score;
}

태스크에 대해 @nid 노드를 중심으로 전체 노드에 대한 faults 값을 거리별로 반비례 누적시킨 후 score로 반환한다. 백본 타입의 누마 토플로지를 사용하는 경우 @max_dist 이상은 skip 한다. @task가 1인 경우 태스크에서 관리하고 있는 numa faults 값을 사용하고, 0인 경우 태스크가 가리키는 누마 그룹에서 관리하고 있는 faults를 사용한다. (0=누마 direct 방식, score가 클 수록 fault가 더 많다. @nid와 더 가까운 노드들은 더 중요하므로 faults 값을 더 많이 반영한다)

  • 코드 라인 12~13에서 full 메시 형태로 모든 노드가 연결되어 있는 경우 score 0을 반환한다.
  • 코드 라인 19~28에서 온라인 노드를 순회하며 요청한 노드 @nid와의 거리가 가장 멀거나 같은 노드인 경우는 skip 한다.
  • 코드 라인 37~39에서 누마 토플로지가 백플레인 타입이면서 @nid와의 거리가 @maxdist 이상인 노드의 경우는 skip 한다.
  • 코드 라인 42~45에서 순회 중인 노드의 태스크의 누마 메모리 fault를 알아온다.
  • 코드 라인 55~58에서 누마 토플로지가 glueless 타입인 경우 다음과 같이 거리가 멀리 있을수록 faults 비율을 반비례하게 줄여 적용한다.즉 멀리있는 노드 끼리의 메모리 faults는중요하지않지만 가까이 있는 노드끼리의 메모리 faults는 더 중요하게 판단한다는 의미이다.
    •                     (최대 distance – 순회 중인 노드의 distance)
    • = faults * —————————————————–
    •                                           (최대 distance – 10)
  • 코드 라인 60~63에서 faults를 score에 누적시키고, 모든 노드에 대해 누적이 완료되면 score를 반환한다.

 

다음 그림은 특정 태스크가 사용하는 누마 그룹의 노드 0번에 대한 메모리 faults score를 산출하는 모습을 보여준다.


누마 밸런싱

 

다음 그림은 태스크에 대한 누마 밸런싱이 시작한 후 누마 스캔을 통해 누마 페이지로 변경하고, 태스크가 누마 페이지에 접근할 때의 함수 호출 과정을 보여준다.

태스크에 대한 NUMA 밸런싱 초기화

init_numa_balancing()

kernel/sched/fair.c

void init_numa_balancing(unsigned long clone_flags, struct task_struct *p)
{
        int mm_users = 0;
        struct mm_struct *mm = p->mm;

        if (mm) {
                mm_users = atomic_read(&mm->mm_users);
                if (mm_users == 1) {
                        mm->numa_next_scan = jiffies + msecs_to_jiffies(sysctl_numa_balancing_scan_delay);
                        mm->numa_scan_seq = 0;
                }
        }
        p->node_stamp                   = 0;
        p->numa_scan_seq                = mm ? mm->numa_scan_seq : 0;
        p->numa_scan_period             = sysctl_numa_balancing_scan_delay;
        /* Protect against double add, see task_tick_numa and task_numa_work */
        p->numa_work.next               = &p->numa_work;
        p->numa_faults                  = NULL;
        RCU_INIT_POINTER(p->numa_group, NULL);
        p->last_task_numa_placement     = 0;
        p->last_sum_exec_runtime        = 0;

        init_task_work(&p->numa_work, task_numa_work);

        /* New address space, reset the preferred nid */
        if (!(clone_flags & CLONE_VM)) {
                p->numa_preferred_nid = NUMA_NO_NODE;
                return;
        }

        /*
         * New thread, keep existing numa_preferred_nid which should be copied
         * already by arch_dup_task_struct but stagger when scans start.
         */
        if (mm) {
                unsigned int delay;

                delay = min_t(unsigned int, task_scan_max(current),
                        current->numa_scan_period * mm_users * NSEC_PER_MSEC);
                delay += 2 * TICK_NSEC;
                p->node_stamp = delay;
        }
}

태스크에 대한 NUMA 밸런싱을 초기화한다.

  • 코드 라인 6~12에서 유저 태스크의 경우 mm_users가 1인 경우 다음 누마 스캔 시각(numa_next_scan)을 sysctl_numa_balancing_scan_delay(디폴트 1초) 시간 후에 수행하도록 설정한다.
  • 코드 라인 13~23에서 태스크의 numa 관련 멤버들을 초기화한다. 누마 스캔 주기(numa_scan_period)는 sysctl_numa_balancing_scan_delay(디폴트 1초)로 지정한다.
  • 코드 라인 26~29에서 가상 주소 공간을 부모 태스크로 부터 clone하여 새로운 가상 주소 공간을 사용하는 태스크의 경우 새로운 누마 밸런싱을 위해 누마 우선 노드 지정을 클리어한다.
  • 코드 라인 35~42에서 유저 태스크의 경우 p->node_stamp에 위에서 설정한 누마 스캔 주기(numa_scan_period) * mm_users 시간 + 2 틱을 나노초로 대입한다.

 

스케줄 틱에서 호출

task_tick_numa()

kernel/sched/fair.c

static void task_tick_numa(struct rq *rq, struct task_struct *curr)
{
        struct callback_head *work = &curr->numa_work;
        u64 period, now;

        /*
         * We don't care about NUMA placement if we don't have memory.
         */
        if (!curr->mm || (curr->flags & PF_EXITING) || work->next != work)
                return;

        /*
         * Using runtime rather than walltime has the dual advantage that
         * we (mostly) drive the selection from busy threads and that the
         * task needs to have done some actual work before we bother with
         * NUMA placement.
         */
        now = curr->se.sum_exec_runtime;
        period = (u64)curr->numa_scan_period * NSEC_PER_MSEC;

        if (now > curr->node_stamp + period) {
                if (!curr->node_stamp)
                        curr->numa_scan_period = task_scan_start(curr);
                curr->node_stamp += period;

                if (!time_before(jiffies, curr->mm->numa_next_scan))
                        task_work_add(curr, work, true);
        }
}

정기 스케줄 틱을 통해 호출되며 cfs 태스크의 누마 메모리 faults를 스캔한다.

  • 코드 라인 9~10에서 유저 태스크가 아니거나 종료 중이거나 또 다른 워크가 존재하는 경우 함수를 빠져나간다.
  • 코드 라인 18~28에서 태스크의 총 실행 시간 기준 numa_scan_period 주기를 초과한 경우이다. node_stamp는 주기만큼 추가하고, 처음인 경우 누마 스캔 주기(numa_scan_period)를 누마 그룹으로 부터 scan 주기를 알아와 설정한다. 다음 스캔 시각(numa_next_scan)을 넘긴 경우 p->task_works 리스트에 누마 워크를 추가한다.
    • 추가된 work는 커널에서 유저로 복귀 시 다음과 같은 경로로 호출되어 실행된다.
      • arch/arm64/entry.S – work_pending: 레이블 -> do_notify_resume() -> do_signal() -> get_signal() -> task_work_run() 함수에서 펜딩 워크들이 호출되어 실행된다.

 

누마 스캔 작업

task_numa_work()

kernel/sched/fair.c -1/2-

/*
 * The expensive part of numa migration is done from task_work context.
 * Triggered from task_tick_numa().
 */
static void task_numa_work(struct callback_head *work)
{
        unsigned long migrate, next_scan, now = jiffies;
        struct task_struct *p = current;
        struct mm_struct *mm = p->mm;
        u64 runtime = p->se.sum_exec_runtime;
        struct vm_area_struct *vma;
        unsigned long start, end;
        unsigned long nr_pte_updates = 0;
        long pages, virtpages;

        SCHED_WARN_ON(p != container_of(work, struct task_struct, numa_work));

        work->next = work;
        /*
         * Who cares about NUMA placement when they're dying.
         *
         * NOTE: make sure not to dereference p->mm before this check,
         * exit_task_work() happens _after_ exit_mm() so we could be called
         * without p->mm even though we still had it when we enqueued this
         * work.
         */
        if (p->flags & PF_EXITING)
                return;

        if (!mm->numa_next_scan) {
                mm->numa_next_scan = now +
                        msecs_to_jiffies(sysctl_numa_balancing_scan_delay);
        }

        /*
         * Enforce maximal scan/migration frequency..
         */
        migrate = mm->numa_next_scan;
        if (time_before(now, migrate))
                return;

        if (p->numa_scan_period == 0) {
                p->numa_scan_period_max = task_scan_max(p);
                p->numa_scan_period = task_scan_start(p);
        }

        next_scan = now + msecs_to_jiffies(p->numa_scan_period);
        if (cmpxchg(&mm->numa_next_scan, migrate, next_scan) != migrate)
                return;

        /*
         * Delay this task enough that another task of this mm will likely win
         * the next time around.
         */
        p->node_stamp += 2 * TICK_NSEC;

        start = mm->numa_scan_offset;
        pages = sysctl_numa_balancing_scan_size;
        pages <<= 20 - PAGE_SHIFT; /* MB in pages */
        virtpages = pages * 8;     /* Scan up to this much virtual space */
        if (!pages)
                return;

누마 스캔 주기마다 동작하는 cost가 높은 작업으로 태스크의 vma 영역을 NUMA hinting fault가 발생하도록 매핑을 변경한다. 한 번 처리할 때마다 sysctl_numa_balancing_scan_size  만큼씩 영역을 처리한다. 매 스캔 시마다 스캔할 영역은 그 전 위치를 기억하여 다음 스캔 시 사용한다.

  • 코드 라인 23~24에서 이미 태스크가 종료가 진행 중인 경우 함수를 빠져나간다.
  • 코드 라인 26~29에서 다음 누마 스캔 시각(numa_next_scan)가 설정되지 않은 경우 현재 시각에 sysctl_numa_balancing_scan_delay(디폴트 1초)를 추가하여 설정한다.
  • 코드 라인 34~36에서 누마 스캔 시각이 지나지 않은 경우 함수를 빠져나간다.
  • 코드 라인 38~41에서 누마 스캔 주기(numa_scan_period)가 설정되지 않은 경우 numa_scan_period_max와 numa_scanm_period를 초기 설정한다.
  • 코드 라인 43~45에서 다음 누마 스캔 시각(numa_next_scan)에 현재 시각 + 누마 스캔 주기(numa_scan_period)로 설정한다. 만일 다른 cpu 등에서 이 값을 먼저 교체한 경우 현재 cpu는 처리를 양보하기 위해 함수를 빠져나간다.
  • 코드 라인 51에서 p->node_stamp에 2 틱 만큼의 시간을 추가한다.
  • 코드 라인 53~58에서 스캔을 시작할 주소와 누마 스캔 사이즈를 알아와서 바이트 단위로 변경하여 pages에 대입한다. 이 함수가 호출되어 한 번에 처리할 영역 사이즈이다. 그리고 이 값의 8배를 virtpages에 대입하는데, 역시 한 번 처리할 때마다 사용할 스캔 영역의 크기이다.

 

kernel/sched/fair.c -2/2-

        if (!down_read_trylock(&mm->mmap_sem))
                return;
        vma = find_vma(mm, start);
        if (!vma) {
                reset_ptenuma_scan(p);
                start = 0;
                vma = mm->mmap;
        }
        for (; vma; vma = vma->vm_next) {
                if (!vma_migratable(vma) || !vma_policy_mof(vma) ||
                        is_vm_hugetlb_page(vma) || (vma->vm_flags & VM_MIXEDMAP)) {
                        continue;
                }

                /*
                 * Shared library pages mapped by multiple processes are not
                 * migrated as it is expected they are cache replicated. Avoid
                 * hinting faults in read-only file-backed mappings or the vdso
                 * as migrating the pages will be of marginal benefit.
                 */
                if (!vma->vm_mm ||
                    (vma->vm_file && (vma->vm_flags & (VM_READ|VM_WRITE)) == (VM_READ)))
                        continue;

                /*
                 * Skip inaccessible VMAs to avoid any confusion between
                 * PROT_NONE and NUMA hinting ptes
                 */
                if (!(vma->vm_flags & (VM_READ | VM_EXEC | VM_WRITE)))
                        continue;

                do {
                        start = max(start, vma->vm_start);
                        end = ALIGN(start + (pages << PAGE_SHIFT), HPAGE_SIZE);
                        end = min(end, vma->vm_end);
                        nr_pte_updates = change_prot_numa(vma, start, end);

                        /*
                         * Try to scan sysctl_numa_balancing_size worth of
                         * hpages that have at least one present PTE that
                         * is not already pte-numa. If the VMA contains
                         * areas that are unused or already full of prot_numa
                         * PTEs, scan up to virtpages, to skip through those
                         * areas faster.
                         */
                        if (nr_pte_updates)
                                pages -= (end - start) >> PAGE_SHIFT;
                        virtpages -= (end - start) >> PAGE_SHIFT;

                        start = end;
                        if (pages <= 0 || virtpages <= 0)
                                goto out;

                        cond_resched();
                } while (end != vma->vm_end);
        }

out:
        /*
         * It is possible to reach the end of the VMA list but the last few
         * VMAs are not guaranteed to the vma_migratable. If they are not, we
         * would find the !migratable VMA on the next scan but not reset the
         * scanner to the start so check it now.
         */
        if (vma)
                mm->numa_scan_offset = start;
        else
                reset_ptenuma_scan(p);
        up_read(&mm->mmap_sem);

        /*
         * Make sure tasks use at least 32x as much time to run other code
         * than they used here, to limit NUMA PTE scanning overhead to 3% max.
         * Usually update_task_scan_period slows down scanning enough; on an
         * overloaded system we need to limit overhead on a per task basis.
         */
        if (unlikely(p->se.sum_exec_runtime != runtime)) {
                u64 diff = p->se.sum_exec_runtime - runtime;
                p->node_stamp += 32 * diff;
        }
}
  • 코드 라인 3~8에서 mm 내에서 누마 스캔할 가상 주소가 포함된 vma를 알아온다. vma가 발견되지 않은 경우 시작 주소를 0으로 변경하고, vma로 mmap 매핑의 처음을 사용한다.
  • 코드 라인 9~30에서 vma를 순회하며 다음 경우에 한하여 skip 한다.(누마밸런싱은 페이지의 migration과 함께 한다)
    • migratable(페이지 이동) 하지 않은 vma 영역
    • migration 시 fault 되도록 정책(MPOL_F_MOF)된 vma 영역이 아닌 경우
    • hugetlb 페이지 영역
    • VM_MIXEDMAP 플래그를 사용한 영역으로 struct page 및 pure PFN 페이지가 포함가능한 영역
    • vma->vm_mm 이 지정되지 않은 경우
    • read only 파일 매핑인 경우
    • rwx(read/write/excute)가 하나도 설정되지 않은 영역
  • 코드 라인 32~55에서 순회 중인 vma 영역의 끝까지 페이지 매핑 protection을 PAGE_NONE으로 변경하여 NUMA hinting fault를 유발할 수 있게 한다. 매핑 변경이 성공한 경우 해당 vma 영역은 sysctl_numa_balancing_scan_size 에서 감소시키고, 스캔 영역 크기는 최대 sysctl_numa_balancing_scan_size의 8배 크기로 제한한다.
  • 코드 라인 58~68에서 out: 레이블이다. vma 영역을 모두 처리하지 않은 경우 다음 스캔 시작 지점을 해당 vma의 끝 주소부터 시작하게 설정해 둔다. vma 영역 모두 끝난 경우 다시 처음 부터 시작될 수 있게 리셋한다.
  • 코드 라인 77~80에서 이 함수가 처리되는 동안 태스크의 총 실행 누적 시간이 변경된 경우 그 차이분 만큼의 32배를 곱해 p->node_stamp에 기록한다.

 

reset_ptenuma_scan()

kernel/sched/fair.c

static void reset_ptenuma_scan(struct task_struct *p)
{
        /*
         * We only did a read acquisition of the mmap sem, so
         * p->mm->numa_scan_seq is written to without exclusive access
         * and the update is not guaranteed to be atomic. That's not
         * much of an issue though, since this is just used for
         * statistical sampling. Use READ_ONCE/WRITE_ONCE, which are not
         * expensive, to avoid any form of compiler optimizations:
         */
        WRITE_ONCE(p->mm->numa_scan_seq, READ_ONCE(p->mm->numa_scan_seq) + 1);
        p->mm->numa_scan_offset = 0;
}

누마 스캔 시작 주소를 0으로 리셋한다.

 

NUMA fault 영역으로 변경

change_prot_numa()

mm/mempolicy.c

/*
 * This is used to mark a range of virtual addresses to be inaccessible.
 * These are later cleared by a NUMA hinting fault. Depending on these
 * faults, pages may be migrated for better NUMA placement.
 *
 * This is assuming that NUMA faults are handled using PROT_NONE. If
 * an architecture makes a different choice, it will need further
 * changes to the core.
 */
unsigned long change_prot_numa(struct vm_area_struct *vma,
                        unsigned long addr, unsigned long end)
{
        int nr_updated;

        nr_updated = change_protection(vma, addr, end, PAGE_NONE, 0, 1);
        if (nr_updated)
                count_vm_numa_events(NUMA_PTE_UPDATES, nr_updated);

        return nr_updated;
}

vma 영역의 가상 주소 @addr ~ @end 범위를 NUMA hinging fault가 발생하게 PROT_NONE 매핑 속성으로 변경한다.

  • PROT_NONE 매핑 속성
    • NUMA 밸런싱을 위해 강제로 액세스 불가능 형태로 바꾸어 이 페이지에 처음 접근할 때에 한하여 numa fault가 발생하게 한다.

 


NUMA 페이지 접근

 

다음 그림은 태스크가 누마 페이지에 접근하여 faults 통계를 갱신하고 밸런싱을 하는 함수 호출과정을 보여준다.

NUMA faults 동작

task_numa_fault()

fault 핸들러 handle_mm_fault() -> __handle_mm_fault() -> handle_pte_fault() -> do_numa_page() 함수로 부터 진입된다.

kernel/sched/fair.c

/*
 * Got a PROT_NONE fault for a page on @node.
 */
void task_numa_fault(int last_cpupid, int mem_node, int pages, int flags)
{
        struct task_struct *p = current;
        bool migrated = flags & TNF_MIGRATED;
        int cpu_node = task_node(current);
        int local = !!(flags & TNF_FAULT_LOCAL);
        struct numa_group *ng;
        int priv;

        if (!static_branch_likely(&sched_numa_balancing))
                return;

        /* for example, ksmd faulting in a user's mm */
        if (!p->mm)
                return;

        /* Allocate buffer to track faults on a per-node basis */
        if (unlikely(!p->numa_faults)) {
                int size = sizeof(*p->numa_faults) *
                           NR_NUMA_HINT_FAULT_BUCKETS * nr_node_ids;

                p->numa_faults = kzalloc(size, GFP_KERNEL|__GFP_NOWARN);
                if (!p->numa_faults)
                        return;

                p->total_numa_faults = 0;
                memset(p->numa_faults_locality, 0, sizeof(p->numa_faults_locality));
        }

        /*
         * First accesses are treated as private, otherwise consider accesses
         * to be private if the accessing pid has not changed
         */
        if (unlikely(last_cpupid == (-1 & LAST_CPUPID_MASK))) {
                priv = 1;
        } else {
                priv = cpupid_match_pid(p, last_cpupid);
                if (!priv && !(flags & TNF_NO_GROUP))
                        task_numa_group(p, last_cpupid, flags, &priv);
        }

        /*
         * If a workload spans multiple NUMA nodes, a shared fault that
         * occurs wholly within the set of nodes that the workload is
         * actively using should be counted as local. This allows the
         * scan rate to slow down when a workload has settled down.
         */
        ng = deref_curr_numa_group(p);
        if (!priv && !local && ng && ng->active_nodes > 1 &&
                                numa_is_active_node(cpu_node, ng) &&
                                numa_is_active_node(mem_node, ng))
                local = 1;

        /*
         * Retry to migrate task to preferred node periodically, in case it
         * previously failed, or the scheduler moved us.
         */
        if (time_after(jiffies, p->numa_migrate_retry)) {
                task_numa_placement(p);
                numa_migrate_preferred(p);
        }

        if (migrated)
                p->numa_pages_migrated += pages;
        if (flags & TNF_MIGRATE_FAIL)
                p->numa_faults_locality[2] += pages;

        p->numa_faults[task_faults_idx(NUMA_MEMBUF, mem_node, priv)] += pages;
        p->numa_faults[task_faults_idx(NUMA_CPUBUF, cpu_node, priv)] += pages;
        p->numa_faults_locality[local] += pages;
}

누마 hinting fault가 발생하고 마이그레이션 후에 이 함수가 호출되었다. 이 함수에서는 numa fault 관련 통계들을 증가시킨다.

  • 코드 라인 10~11에서 누마 밸런싱이 설정되지 않은 경우 함수를 빠져나간다.
  • 코드 라인 14~15에서 유저 태스크가 아닌 경우 함수를 빠져나간다.
  • 코드 라인 18~28에서 처음에 한하여 태스크에 numa_faults[]를 할당하고, total_numa_faults 및 numa_faults_locality[]를 클리어한다.
    • 4 가지 타입 * 노드 수 * 2 (private|shared) 만큼 배열을 할당한다.
  • 코드 라인 34~40에서 priv 값을 다음과 같이 설정한다.
    • 처음 접근 시 priv=1
    • 처음 접근이 아닌 경우 priv 값은 태스크에 설정된 pid가 lastcpupid와 같은 pid 사용 여부이다. 단 priv가 0이고 TNF_NO_GROUP 플래그를 사용하는 경우 누마 그룹을 통해 priv를 알아온다.
  • 코드 라인 48~52에서 priv가 0이고 리모트 노드에서 누마 그룹의 active 노드가 2개 이상이며, mem_node와 cpu_node가 모두 active 상태인 경우 local에 1을 대입한다.
  • 코드 라인 58~61에서 현재 시각이 numa_migrate_retry 시각을 넘긴 경우 태스크를 preferred 노드로 옮기는 것을 재시도한다.
  • 코드 라인 63~64에서 TNF_MIGRATED 플래그를 사용한 경우 numa_pages_migrated에 페이지 수를 추가한다.
  • 코드 라인 65~66에서 TNF_MIGRATE_FAIL 플래그를 사용한 경우 numa_faults_locality[2]에 페이지 수를 추가한다.
    • numa_faults_locality[]
      • 0=remote
      • 1=local
      • 2=failed to migrate
  • 코드 라인 68~70에서 numa_faults[]에 페이지 수를 추가하여 갱신하고, numa_faults_locality[]의 로컬에도 페이지 수를 추가한다.

 

task_numa_group()

kernel/sched/fair.c -1/2-

static void task_numa_group(struct task_struct *p, int cpupid, int flags,
                        int *priv)
{
        struct numa_group *grp, *my_grp;
        struct task_struct *tsk;
        bool join = false;
        int cpu = cpupid_to_cpu(cpupid);
        int i;

        if (unlikely(!deref_curr_numa_group(p))) {
                unsigned int size = sizeof(struct numa_group) +
                                    4*nr_node_ids*sizeof(unsigned long);

                grp = kzalloc(size, GFP_KERNEL | __GFP_NOWARN);
                if (!grp)
                        return;

                refcount_set(&grp->refcount, 1);
                grp->active_nodes = 1;
                grp->max_faults_cpu = 0;
                spin_lock_init(&grp->lock);
                grp->gid = p->pid;
                /* Second half of the array tracks nids where faults happen */
                grp->faults_cpu = grp->faults + NR_NUMA_HINT_FAULT_TYPES *
                                                nr_node_ids;

                for (i = 0; i < NR_NUMA_HINT_FAULT_STATS * nr_node_ids; i++)
                        grp->faults[i] = p->numa_faults[i];

                grp->total_faults = p->total_numa_faults;

                grp->nr_tasks++;
                rcu_assign_pointer(p->numa_group, grp);
        }

        rcu_read_lock();
        tsk = READ_ONCE(cpu_rq(cpu)->curr);

        if (!cpupid_match_pid(tsk, cpupid))
                goto no_join;

        grp = rcu_dereference(tsk->numa_group);
        if (!grp)
                goto no_join;

        my_grp = deref_curr_numa_group(p);
        if (grp == my_grp)
                goto no_join;

        /*
         * Only join the other group if its bigger; if we're the bigger group,
         * the other task will join us.
         */
        if (my_grp->nr_tasks > grp->nr_tasks)
                goto no_join;

        /*
         * Tie-break on the grp address.
         */
        if (my_grp->nr_tasks == grp->nr_tasks && my_grp > grp)
                goto no_join;

        /* Always join threads in the same process. */
        if (tsk->mm == current->mm)
                join = true;

        /* Simple filter to avoid false positives due to PID collisions */
        if (flags & TNF_SHARED)
                join = true;

        /* Update priv based on whether false sharing was detected */
        *priv = !join;

        if (join && !get_numa_group(grp))
                goto no_join;

        rcu_read_unlock();

        if (!join)
                return;

누마 hinting faults을 통해 누마 마이그레이션에 대한 누마 faults 관련 stat을 갱신한다. 만일 요청한 태스크 @p가 처음 접근한 경우엔 태스크에 누마 그룹을 생성한다. 출력 인자 @priv에 1이 단일 접근인 경우이고, 0이 출력되면 공유 접근한 상태이다.

  • 코드 라인 10~34에서 처음인 경우 태스크에 누마 그룹을 할당한 후 내부 멤버들을 초기화한다. 또한 누마 그룹내 faults[] 배열을 할당하고 역시 초기화한다.
  • 코드 라인 36~61에서 다음에 해당하는 경우 no_join 레이블을 통해 함수를 빠져나간다.
    • 현재 cpu에서 동작 중인 태스크와 cpupid의 pid가 서로 다른 경우
    • 현재 태스크의 누마 그룹이 지정되지 않은 경우
    • 현재 태스크의 누마 그룹과 요청한 태스크의 누마 그룹이 서로 같은 경우
    • 현재 태스크의 누마 그룹에서 동작 중인 태스크의 수 보다 요청한 태스크의 그룹에서 동작 중인 태스크의 수 보다 큰 경우
    • 현재 태스크의 누마 그룹에서 동작 중인 태스크의 수와 요청한 태스크의 그룹에서 동작 중인 태스크의 수가 동일한 경우 두 그룹의 주소를 비교하여 요청한 태스크의 그룹 주소가 더 큰 경우
  • 코드 라인 64~69에서 다음의 경우에 한해 join=true로 설정한다.
    • 현재 태스크와 요청한 태스크의 프로세스가 동일한 경우
    • TNF_SHARED 플래그를 사용한 경우
  • 코드 라인 72에서 출력 인자 *priv에는 join의 반대 값을 대입한다.
  • 코드 라인 74~75에서 join=true일 때 현재 태스크의 그룹의 참조 카운터를 1 증가시킨다.
  • 코드 라인 79~80에서 join=false인 경우 함수를 빠져나간다.

 

kernel/sched/fair.c -2/2-

        BUG_ON(irqs_disabled());
        double_lock_irq(&my_grp->lock, &grp->lock);

        for (i = 0; i < NR_NUMA_HINT_FAULT_STATS * nr_node_ids; i++) {
                my_grp->faults[i] -= p->numa_faults[i];
                grp->faults[i] += p->numa_faults[i];
        }
        my_grp->total_faults -= p->total_numa_faults;
        grp->total_faults += p->total_numa_faults;

        my_grp->nr_tasks--;
        grp->nr_tasks++;

        spin_unlock(&my_grp->lock);
        spin_unlock_irq(&grp->lock);

        rcu_assign_pointer(p->numa_group, grp);

        put_numa_group(my_grp);
        return;

no_join:
        rcu_read_unlock();
        return;
}
  • 코드 라인 2에서 현재 태스크의 누마 그룹과 요청한 태스크의 그룹 둘 모두 한꺼번에 스핀락을 건다.
  • 코드 라인 4~12에서 4 종류의 누마 fault stats를 순회하며 태스크의 누마 faults 값을 이동시키고, tatal_faults와 nr_tasks 수도 이동시킨다. (요청한 태스크의 누마 그룹 -> 현재 태스크의 누마 그룹)
  • 코드 라인 14~15에서 두 스핀락을 해제한다.
  • 코드 라인 19~20에서 요청한 태스크의 누마 그룹 참조 카운터를 1 감소시킨 후 정상적으로 함수를 빠져나간다.
  • 코드 라인 22~24에서 no_join: 레이블이다. 함수를 빠져나간다.

 

numa_is_active_node()

kernel/sched/fair.c

static bool numa_is_active_node(int nid, struct numa_group *ng)
{
        return group_faults_cpu(ng, nid) * ACTIVE_NODE_FRACTION > ng->max_faults_cpu;
}

누마 그룹 @ng의 faults 통계를 보고 누마 노드 @nid에 대한 활성화 여부를 반환한다.

  • faults 수 * 3배한 값이 누마 그룹 @ng의 max_faults_cpu보다 큰지 여부를 반환한다.

 

ACTIVE_NODE_FRACTION

kernel/sched/fair.c

/*
 * A node triggering more than 1/3 as many NUMA faults as the maximum is
 * considered part of a numa group's pseudo-interleaving set. Migrations
 * between these nodes are slowed down, to allow things to settle down.
 */
#define ACTIVE_NODE_FRACTION 3

 

최적의 누마 노드로 태스크 이동

task_numa_placement()

kernel/sched/fair.c -1/2-

static void task_numa_placement(struct task_struct *p)
{
        int seq, nid, max_nid = NUMA_NO_NODE;
        unsigned long max_faults = 0;
        unsigned long fault_types[2] = { 0, 0 };
        unsigned long total_faults;
        u64 runtime, period;
        spinlock_t *group_lock = NULL;
        struct numa_group *ng;

        /*
         * The p->mm->numa_scan_seq field gets updated without
         * exclusive access. Use READ_ONCE() here to ensure
         * that the field is read in a single access:
         */
        seq = READ_ONCE(p->mm->numa_scan_seq);
        if (p->numa_scan_seq == seq)
                return;
        p->numa_scan_seq = seq;
        p->numa_scan_period_max = task_scan_max(p);

        total_faults = p->numa_faults_locality[0] +
                       p->numa_faults_locality[1];
        runtime = numa_get_avg_runtime(p, &period);

        /* If the task is part of a group prevent parallel updates to group stats */
        ng = deref_curr_numa_group(p);
        if (ng) {
                group_lock = &ng->lock;
                spin_lock_irq(group_lock);
        }
  • 코드 라인 16~18에서 누마 스캔 시퀀스에 변동이 없으면 함수를 빠져나간다.
  • 코드 라인 19~20에서 바뀐 누마 스캔 시퀀스를 갱신하고, 누마 스캔 주기 최대치를 갱신한다.
  • 코드 라인 22~23에서 태스크에 대한 numa_faults_locality 값을 알아온다. (remote + local)
  • 코드 라인 24에서 태스크의 지난 누마 placement 사이클 동안의 런타임을 알아오고 period에는 기간을 알아온다.
    • 평균 런타임은 runtime / period를 해야 알 수 있다.
  • 코드 라인 27~31에서 태스크에 대한 누마 그룹이 있는 경우 그룹 스핀 락을 획득한다.

 

kernel/sched/fair.c -2/2-

        /* Find the node with the highest number of faults */
        for_each_online_node(nid) {
                /* Keep track of the offsets in numa_faults array */
                int mem_idx, membuf_idx, cpu_idx, cpubuf_idx;
                unsigned long faults = 0, group_faults = 0;
                int priv;

                for (priv = 0; priv < NR_NUMA_HINT_FAULT_TYPES; priv++) {
                        long diff, f_diff, f_weight;

                        mem_idx = task_faults_idx(NUMA_MEM, nid, priv);
                        membuf_idx = task_faults_idx(NUMA_MEMBUF, nid, priv);
                        cpu_idx = task_faults_idx(NUMA_CPU, nid, priv);
                        cpubuf_idx = task_faults_idx(NUMA_CPUBUF, nid, priv);

                        /* Decay existing window, copy faults since last scan */
                        diff = p->numa_faults[membuf_idx] - p->numa_faults[mem_idx] / 2;
                        fault_types[priv] += p->numa_faults[membuf_idx];
                        p->numa_faults[membuf_idx] = 0;

                        /*
                         * Normalize the faults_from, so all tasks in a group
                         * count according to CPU use, instead of by the raw
                         * number of faults. Tasks with little runtime have
                         * little over-all impact on throughput, and thus their
                         * faults are less important.
                         */
                        f_weight = div64_u64(runtime << 16, period + 1);
                        f_weight = (f_weight * p->numa_faults[cpubuf_idx]) /
                                   (total_faults + 1);
                        f_diff = f_weight - p->numa_faults[cpu_idx] / 2;
                        p->numa_faults[cpubuf_idx] = 0;

                        p->numa_faults[mem_idx] += diff;
                        p->numa_faults[cpu_idx] += f_diff;
                        faults += p->numa_faults[mem_idx];
                        p->total_numa_faults += diff;
                        if (ng) {
                                /*
                                 * safe because we can only change our own group
                                 *
                                 * mem_idx represents the offset for a given
                                 * nid and priv in a specific region because it
                                 * is at the beginning of the numa_faults array.
                                 */
                                ng->faults[mem_idx] += diff;
                                ng->faults_cpu[mem_idx] += f_diff;
                                ng->total_faults += diff;
                                group_faults += ng->faults[mem_idx];
                        }
                }

                if (!ng) {
                        if (faults > max_faults) {
                                max_faults = faults;
                                max_nid = nid;
                        }
                } else if (group_faults > max_faults) {
                        max_faults = group_faults;
                        max_nid = nid;
                }
        }

        if (ng) {
                numa_group_count_active_nodes(ng);
                spin_unlock_irq(group_lock);
                max_nid = preferred_group_nid(p, max_nid);
        }

        if (max_faults) {
                /* Set the new preferred node */
                if (max_nid != p->numa_preferred_nid)
                        sched_setnuma(p, max_nid);
        }

        update_task_scan_period(p, fault_types[0], fault_types[1]);
}
  • 코드 라인 2~8에서 온라인 노드를 순회하고, 다시 그 내부에서 누마 hint fault 타입 수(private, shared)만큼 순회한다.
  • 코드 라인 17~19에서 태스크에 대한 누마 faults들 중 membuf에서 mem의 절반을 뺀 값을 diff에 대입하고, 순회 중인 fault 타입의 임시 배열 fault_types[]에 membuf의 값을 추가하고, membuf는 클리어한다.
    • p->numa_faults[mem_idx] 값은 p->numa_faults[membuf_idx] 값의 200%로 제한된다.
  • 코드 라인 28~30에서 f_weight에는 p->numa_faults[cpubuf_idx] 값을 16비트 이진화 정수로 변환(normalization)한 후 다음 두 비율을 적용한다.
    • 누마 placement 사이클 동안의 평균 런타임 비율 (runtime / period)
    • 전체 노드 fault 중 numa_faults[cpubuf_idx] 비율 (p->numa_faults[cpu_idx] / total_faults + 1)
  • 코드 라인 31~32에서 증감 시킬 f_diff 값은 산출한 f_weight – 기존 p->numa_faults[cpu_idx]의 절반을 적용한다. 그 후 p->numa_faults[cpubuf_idx]는 클리어한다.
  • 코드 라인 34~37에서 mem_idx 및 cpu_idx의 p->numa_faults[] 값에 diff 및 f_diff 값을 추가하고, 이렇게 누적된 p->numa_faults[mem_idx] 값을 faults에 다시 누적하고, p->total_numa_fuautls에 diff를 누적한다.
  • 코드 라인 38~50에서 누마 그룹의 faults, faults_cpu, total_faults 들에 대해서도 diff 만큼 추가하고, group_faults에는 ng->faults[mem_idx]를 누적시킨다.
  • 코드 라인 53~61에서 max_faults와 max_nid를 갱신한다.
  • 코드 라인 64~68에서 누마 그룹의 최대 faults 수와 active 노드 수를 갱신하고, 태스크에 가장 적합한 우선 노드를 알아와서 max_nid에 대입한다.
  • 코드 라인 70~74에서 태스크의 우선 노드로 max_nid를 지정한다.
  • 코드 라인 76에서 태스크의 누마 스캔 주기의 증/감을 갱신한다.

 

다음은 numa faults 값의 증감시 변화되는 값을 보여준다.

  • faults가 10씩 증가될때와, 0이된 후 decay되는 값을 보여준다.

 

numa_get_avg_runtime()

kernel/sched/fair.c

/*
 * Get the fraction of time the task has been running since the last
 * NUMA placement cycle. The scheduler keeps similar statistics, but
 * decays those on a 32ms period, which is orders of magnitude off
 * from the dozens-of-seconds NUMA balancing period. Use the scheduler
 * stats only if the task is so new there are no NUMA statistics yet.
 */
static u64 numa_get_avg_runtime(struct task_struct *p, u64 *period)
{
        u64 runtime, delta, now;
        /* Use the start of this time slice to avoid calculations. */
        now = p->se.exec_start;
        runtime = p->se.sum_exec_runtime;

        if (p->last_task_numa_placement) {
                delta = runtime - p->last_sum_exec_runtime;
                *period = now - p->last_task_numa_placement;

                /* Avoid time going backwards, prevent potential divide error: */
                if (unlikely((s64)*period < 0))
                        *period = 0;
        } else {
                delta = p->se.avg.load_sum;
                *period = LOAD_AVG_MAX;
        }

        p->last_sum_exec_runtime = runtime;
        p->last_task_numa_placement = now;

        return delta;
}

태스크 @p의 지난 누마 placement 사이클 동안의 런타임을 알아온다. 출력 인자 *period에는 지난 누마 placement 사이클이 담긴다.

  • 코드 라인 5~6에서 태스크의 이번 스케줄 실행 시각과 총 누적 실행 시간을 알아온다.
    • p->exec_start는 태스크가 스케줄되거나, 매 스케줄 틱마다 현재 시각(런큐 클럭)으로 갱신한다.
    • p->se.sum_exec_runtime에는 스케줄 틱마다 태스크의 총 누적 실행 시간이 담겨있다.
  • 코드 라인 8~18에서 task_numa_placement 함수를 통해 갱신된 적이 있는지 여부에 따라 다음과 같이 처리한다.
    • 갱신된 적이 있으면 태스크의 총 누적 실행 시간에서 지난 갱신 되었던 총 실행 시간을 뺀 delta 값을 알아오고, 출력 인자 *period에는 현재 시각에서 지난 갱신 시각을 뺀 기간을 반환한다.
    • 갱신된 적이 없으면 태스크의 로드 합을 delta에 담고, 출력 인자 *perido에 전체 기간(LOAD_AVG_MAX)를 담는다.
  • 코드 라인 20~21에서 처음 알아온 runtime과 now를 갱신한다.
  • 코드 라인 23에서 delta 실행 시간을 반환한다.

 

numa_group_count_active_nodes()

kernel/sched/fair.c

/*
 * Find out how many nodes on the workload is actively running on. Do this by
 * tracking the nodes from which NUMA hinting faults are triggered. This can
 * be different from the set of nodes where the workload's memory is currently
 * located.
 */
static void numa_group_count_active_nodes(struct numa_group *numa_group)
{
        unsigned long faults, max_faults = 0;
        int nid, active_nodes = 0;

        for_each_online_node(nid) {
                faults = group_faults_cpu(numa_group, nid);
                if (faults > max_faults)
                        max_faults = faults;
        }

        for_each_online_node(nid) {
                faults = group_faults_cpu(numa_group, nid);
                if (faults * ACTIVE_NODE_FRACTION > max_faults)
                        active_nodes++;
        }

        numa_group->max_faults_cpu = max_faults;
        numa_group->active_nodes = active_nodes;
}

@numa_group의 max_faults_cpu와 active_nodes를 갱신해온다.

  • 코드 라인 6~10에서 모든 온라인 노드들을 순회하며 @numa_group의 faults들 중 가장 큰 값을 max_faults에 알아온다.
  • 코드 라인 12~16에서 모든 온라인 노드들을 순회하며 @numa_group의 faults에 액티브 비율(3배)을 적용한 값이 max_faults를 초과하는 노드들의 수를 active_nodes에 알아온다.
    • 가장 접근이 많은 노드의 1/3이 넘는 노드들의 수가 avtive_nodes로 지정된다.
    • 이 값이 2 이상인 경우 2개 이상의 메모리 노드에 분산되어 많이 사용됨을 알 수 있다.
  • 코드 라인 18~19에서 @numa_group의 max_faults_cpu와 active_nodes를 갱신한다.

 

태스크에 적절한 우선 노드 찾기

preferred_group_nid()

kernel/sched/fair.c -1/2-

/*
 * Determine the preferred nid for a task in a numa_group. This needs to
 * be done in a way that produces consistent results with group_weight,
 * otherwise workloads might not converge.
 */
static int preferred_group_nid(struct task_struct *p, int nid)
{
        nodemask_t nodes;
        int dist;

        /* Direct connections between all NUMA nodes. */
        if (sched_numa_topology_type == NUMA_DIRECT)
                return nid;

        /*
         * On a system with glueless mesh NUMA topology, group_weight
         * scores nodes according to the number of NUMA hinting faults on
         * both the node itself, and on nearby nodes.
         */
        if (sched_numa_topology_type == NUMA_GLUELESS_MESH) {
                unsigned long score, max_score = 0;
                int node, max_node = nid;

                dist = sched_max_numa_distance;

                for_each_online_node(node) {
                        score = group_weight(p, node, dist);
                        if (score > max_score) {
                                max_score = score;
                                max_node = node;
                        }
                }
                return max_node;
        }

태스크에 가장 적절한 누마 우선 노드를 알아온다. 누마 노드 타입에 따라 다음과 같이 알아온다.

  • direct 노드 타입의 경우 @nid를 반환한다.
  • glueless mesh 노드 타입의 경우 모든 노드에서 태스크 @p에 대한 그룹 weight가 가장 큰 노드를 선택한다.
  • backplain 노드 타입의 경우 가장 큰 distance부터 시작하여 노드들 중 다른 노드들과의 그룹 faults가 가장 큰 노드를 선택한다. 그룹 faults가 없는 경우 다음 distance를 진행한다.

 

  • 코드 라인 7~8에서 누마 direct 노드 타입의 경우 인자로 전달받은 @nid를 우선 노드로 사용하기 위해 반환한다.
  • 코드 라인 15~29에서 누마 glueless mesh 노드 타입의 경우 태스크 @p와 각 노드 간에 그룹 weight가 가장 큰 노드를 우선 노드로 사용하기 위해 반환한다.

 

kernel/sched/fair.c -2/2-

        /*
         * Finding the preferred nid in a system with NUMA backplane
         * interconnect topology is more involved. The goal is to locate
         * tasks from numa_groups near each other in the system, and
         * untangle workloads from different sides of the system. This requires
         * searching down the hierarchy of node groups, recursively searching
         * inside the highest scoring group of nodes. The nodemask tricks
         * keep the complexity of the search down.
         */
        nodes = node_online_map;
        for (dist = sched_max_numa_distance; dist > LOCAL_DISTANCE; dist--) {
                unsigned long max_faults = 0;
                nodemask_t max_group = NODE_MASK_NONE;
                int a, b;

                /* Are there nodes at this distance from each other? */
                if (!find_numa_distance(dist))
                        continue;

                for_each_node_mask(a, nodes) {
                        unsigned long faults = 0;
                        nodemask_t this_group;
                        nodes_clear(this_group);

                        /* Sum group's NUMA faults; includes a==b case. */
                        for_each_node_mask(b, nodes) {
                                if (node_distance(a, b) < dist) {
                                        faults += group_faults(p, b);
                                        node_set(b, this_group);
                                        node_clear(b, nodes);
                                }
                        }

                        /* Remember the top group. */
                        if (faults > max_faults) {
                                max_faults = faults;
                                max_group = this_group;
                                /*
                                 * subtle: at the smallest distance there is
                                 * just one node left in each "group", the
                                 * winner is the preferred nid.
                                 */
                                nid = a;
                        }
                }
                /* Next round, evaluate the nodes within max_group. */
                if (!max_faults)
                        break;
                nodes = max_group;
        }
        return nid;
}
  • 코드 라인 11~18에서 누마 백플레인 노드 타입인 경우이다. 로컬을 제외한 리모트 최장거리부터 최단거리까지 1씩 줄여가며 순회하며 누마 디스턴스에 사용하지 않는 거리들은 skip 한다.
    • 예) 30, 29, 28, … 11 까지 감소하며 누마 디스턴스인 30, 25, 20, 15을 제외하고는 skip 한다.
  • 코드 라인 20~32에서 누마 온라인 노드들을 대상으로 순회하며 순회하는 노드가 다른 노드들과의 거리가 dist 보다 작을 때 태스크에 대한 그룹 faults를 누적하고, 누적한 그룹을 this_group에 마크하고, nodes에서는 제거한다.
  • 코드 라인 35~44에서 max_faults를 갱신하고, 갱신한 노드를 nid에 대입하고, 마크된 this_group을 max-group에 대입한다.
  • 코드 라인 47~49에서 max-faults가 0인 경우 함수를 빠져나가고, 존재하는 경우 nodes에 max_group을 대입하고, 다음 distance를 진행하도록 한다.
  • 코드 라인 51에서 태스크가 가장 많은 접근(max_faults)을 기록한 nid 노드를 반환한다.

 

누마 스캔 주기 증/감 갱신

update_task_scan_period()

kernel/sched/fair.c

/*
 * Increase the scan period (slow down scanning) if the majority of
 * our memory is already on our local node, or if the majority of
 * the page accesses are shared with other processes.
 * Otherwise, decrease the scan period.
 */
static void update_task_scan_period(struct task_struct *p,
                        unsigned long shared, unsigned long private)
{
        unsigned int period_slot;
        int lr_ratio, ps_ratio;
        int diff;

        unsigned long remote = p->numa_faults_locality[0];
        unsigned long local = p->numa_faults_locality[1];

        /*
         * If there were no record hinting faults then either the task is
         * completely idle or all activity is areas that are not of interest
         * to automatic numa balancing. Related to that, if there were failed
         * migration then it implies we are migrating too quickly or the local
         * node is overloaded. In either case, scan slower
         */
        if (local + shared == 0 || p->numa_faults_locality[2]) {
                p->numa_scan_period = min(p->numa_scan_period_max,
                        p->numa_scan_period << 1);

                p->mm->numa_next_scan = jiffies +
                        msecs_to_jiffies(p->numa_scan_period);

                return;
        }

        /*
         * Prepare to scale scan period relative to the current period.
         *       == NUMA_PERIOD_THRESHOLD scan period stays the same
         *       <  NUMA_PERIOD_THRESHOLD scan period decreases (scan faster)
         *       >= NUMA_PERIOD_THRESHOLD scan period increases (scan slower)
         */
        period_slot = DIV_ROUND_UP(p->numa_scan_period, NUMA_PERIOD_SLOTS);
        lr_ratio = (local * NUMA_PERIOD_SLOTS) / (local + remote);
        ps_ratio = (private * NUMA_PERIOD_SLOTS) / (private + shared);

        if (ps_ratio >= NUMA_PERIOD_THRESHOLD) {
                /*
                 * Most memory accesses are local. There is no need to
                 * do fast NUMA scanning, since memory is already local.
                 */
                int slot = ps_ratio - NUMA_PERIOD_THRESHOLD;
                if (!slot)
                        slot = 1;
                diff = slot * period_slot;
        } else if (lr_ratio >= NUMA_PERIOD_THRESHOLD) {
                /*
                 * Most memory accesses are shared with other tasks.
                 * There is no point in continuing fast NUMA scanning,
                 * since other tasks may just move the memory elsewhere.
                 */
                int slot = lr_ratio - NUMA_PERIOD_THRESHOLD;
                if (!slot)
                        slot = 1;
                diff = slot * period_slot;
        } else {
                /*
                 * Private memory faults exceed (SLOTS-THRESHOLD)/SLOTS,
                 * yet they are not on the local NUMA node. Speed up
                 * NUMA scanning to get the memory moved over.
                 */
                int ratio = max(lr_ratio, ps_ratio);
                diff = -(NUMA_PERIOD_THRESHOLD - ratio) * period_slot;
        }

        p->numa_scan_period = clamp(p->numa_scan_period + diff,
                        task_scan_min(p), task_scan_max(p));
        memset(p->numa_faults_locality, 0, sizeof(p->numa_faults_locality));
}

태스크 @p의 NUMA 스캔 주기를 증/감하여 갱신한다. 누마 로컬 또는 private 접근 비율이 스레졸드(70%) 이상인 증가되어 스캔 주기가 길어지고, 반대로 누마 리모트 또는 shared의 접근 비율이 스레졸드(70%) 이상인 경우 감소되어 스캔 주기는 짧아진다.

  • 코드 라인 8~9에서 리모트 및 로컬 누마 노드로의 접근 수를 알아온다.
  • 코드 라인 18~26에서 아직 접근된 수가 없거나 마이그레이션 실패한 경우가 있으면 누마 스캔 주기를 2배 증가시키고, 다음 스캔 시각을 결정한 후 함수를 빠져나간다.
  • 코드 라인 34에서 누마 스캔 주기를 NUMA_PERIOD_SLOTS(10) 단위로 올림 정렬한 값을 period_slot에 담는다.
  • 코드 라인 35에서 로컬/리모트 중 로컬 접근 비율을 구한다.
  • 코드 라인 36에서 private/shared 중 private 접근 비율을 구한다.
  • 코드 라인 38~65에서 누마 스캔 주기를 증감 시킬 diff 값을 다음과 같이 구한다.
    • private 비율 >= 70%일 때 초과 값을 period_slot에 곱한다. (증가)
    • local 비율 >= 70% 초과 값을 period_slot에 곱한다. (증가)
    • local 비율 또는 private 비율 중 큰 값에서 스레졸드 값을 뺀 후 period_slot에 곱한다. (음수 -> 감소)
  • 코드 라인 67~68에서 누마 스캔 주기에 diff를 반영한다.(증/감)
  • 코드 라인 69에서 numa_faults_locality[]를 0으로 클리어한다.

 


누마 우선 노드로의 마이그레이션

numa_migrate_preferred()

kernel/sched/fair.c

/* Attempt to migrate a task to a CPU on the preferred node. */
static void numa_migrate_preferred(struct task_struct *p)
{
        unsigned long interval = HZ;

        /* This task has no NUMA fault statistics yet */
        if (unlikely(p->numa_preferred_nid == NUMA_NO_NODE || !p->numa_faults))
                return;

        /* Periodically retry migrating the task to the preferred node */
        interval = min(interval, msecs_to_jiffies(p->numa_scan_period) / 16);
        p->numa_migrate_retry = jiffies + interval;

        /* Success if task is already running on preferred CPU */
        if (task_node(p) == p->numa_preferred_nid)
                return;

        /* Otherwise, try migrate to a CPU on the preferred node */
        task_numa_migrate(p);
}

누마 우선 노드로 태스크를 마이그레이션 시도한다.

  • 코드 라인 7~8에서 낮은 확률로 태스크의 우선 노드가 설정되지 않았거나 누마 밸런싱용 fault 통계가 아직 시작되지 않은 경우 함수를 빠져나간다.
  • 코드 라인 11~12에서 최대 1초 이내의 누마 스캔 주기 /16을 현재 시각에서 추가하여 태스크의 numa_migrate_retry에 대입한다.
  • 코드 라인 15~16에서 이미 태스크가 누마 우선 노드를 사용 중이면 함수를 빠져나간다.
  • 코드 라인 19에서 태스크를 마이그레이션 시도한다.

 

task_numa_migrate()

kernel/sched/fair.c -1/2-

static int task_numa_migrate(struct task_struct *p)
{
        struct task_numa_env env = {
                .p = p,

                .src_cpu = task_cpu(p),
                .src_nid = task_node(p),

                .imbalance_pct = 112,

                .best_task = NULL,
                .best_imp = 0,
                .best_cpu = -1,
        };
        unsigned long taskweight, groupweight;
        struct sched_domain *sd;
        long taskimp, groupimp;
        struct numa_group *ng;
        struct rq *best_rq;
        int nid, ret, dist;

        /*
         * Pick the lowest SD_NUMA domain, as that would have the smallest
         * imbalance and would be the first to start moving tasks about.
         *
         * And we want to avoid any moving of tasks about, as that would create
         * random movement of tasks -- counter the numa conditions we're trying
         * to satisfy here.
         */
        rcu_read_lock();
        sd = rcu_dereference(per_cpu(sd_numa, env.src_cpu));
        if (sd)
                env.imbalance_pct = 100 + (sd->imbalance_pct - 100) / 2;
        rcu_read_unlock();

        /*
         * Cpusets can break the scheduler domain tree into smaller
         * balance domains, some of which do not cross NUMA boundaries.
         * Tasks that are "trapped" in such domains cannot be migrated
         * elsewhere, so there is no point in (re)trying.
         */
        if (unlikely(!sd)) {
                sched_setnuma(p, task_node(p));
                return -EINVAL;
        }

        env.dst_nid = p->numa_preferred_nid;
        dist = env.dist = node_distance(env.src_nid, env.dst_nid);
        taskweight = task_weight(p, env.src_nid, dist);
        groupweight = group_weight(p, env.src_nid, dist);
        update_numa_stats(&env.src_stats, env.src_nid);
        taskimp = task_weight(p, env.dst_nid, dist) - taskweight;
        groupimp = group_weight(p, env.dst_nid, dist) - groupweight;
        update_numa_stats(&env.dst_stats, env.dst_nid);

        /* Try to find a spot on the preferred nid. */
        task_numa_find_cpu(&env, taskimp, groupimp);
  • 코드 라인 3~14에서 태스크의 마이그레이션을 위해 관련 함수에서 인자 전달 목적으로 사용될 구조체 내에 태스크를 소스로 구성한다.
  • 코드 라인 31~45에서 태스크의 cpu에 대한 누마 스케줄 도메인이 여부에 따라 다음과 같이 수행한다.
    • 있을 때 imbalance_pct의 100% 초과분을 절반으로 줄인다.
    • 없을 때 태스크의 누마 우선 노드만을 지정한 후 -EINVAL 값을 반환한다.
  • 코드 라인 47에서 dst_nid에 태스크의 누마 우선 노드를 지정한다.
  • 코드 라인 48~51에서 소스 노드측의 러너블 로드와 capacity를 알아온다.
  • 코드 라인 52~54에서 데스트 노드측의 러너블 로드와 capacity를 알아온다.
  • 코드 라인 57에서 데스트 노드내에서 가장 적절한 cpu를 찾는다.

 

kernel/sched/fair.c -2/2-

        /*
         * Look at other nodes in these cases:
         * - there is no space available on the preferred_nid
         * - the task is part of a numa_group that is interleaved across
         *   multiple NUMA nodes; in order to better consolidate the group,
         *   we need to check other locations.
         */
        ng = deref_curr_numa_group(p);
        if (env.best_cpu == -1 || (ng && ng->active_nodes > 1)) {
                for_each_online_node(nid) {
                        if (nid == env.src_nid || nid == p->numa_preferred_nid)
                                continue;

                        dist = node_distance(env.src_nid, env.dst_nid);
                        if (sched_numa_topology_type == NUMA_BACKPLANE &&
                                                dist != env.dist) {
                                taskweight = task_weight(p, env.src_nid, dist);
                                groupweight = group_weight(p, env.src_nid, dist);
                        }

                        /* Only consider nodes where both task and groups benefit */
                        taskimp = task_weight(p, nid, dist) - taskweight;
                        groupimp = group_weight(p, nid, dist) - groupweight;
                        if (taskimp < 0 && groupimp < 0)
                                continue;

                        env.dist = dist;
                        env.dst_nid = nid;
                        update_numa_stats(&env.dst_stats, env.dst_nid);
                        task_numa_find_cpu(&env, taskimp, groupimp);
                }
        }

        /*
         * If the task is part of a workload that spans multiple NUMA nodes,
         * and is migrating into one of the workload's active nodes, remember
         * this node as the task's preferred numa node, so the workload can
         * settle down.
         * A task that migrated to a second choice node will be better off
         * trying for a better one later. Do not set the preferred node here.
         */
        if (ng) {
                if (env.best_cpu == -1)
                        nid = env.src_nid;
                else
                        nid = cpu_to_node(env.best_cpu);

                if (nid != p->numa_preferred_nid)
                        sched_setnuma(p, nid);
        }

        /* No better CPU than the current one was found. */
        if (env.best_cpu == -1)
                return -EAGAIN;

        best_rq = cpu_rq(env.best_cpu);
        if (env.best_task == NULL) {
                ret = migrate_task_to(p, env.best_cpu);
                WRITE_ONCE(best_rq->numa_migrate_on, 0);
                if (ret != 0)
                        trace_sched_stick_numa(p, env.src_cpu, env.best_cpu);
                return ret;
        }

        ret = migrate_swap(p, env.best_task, env.best_cpu, env.src_cpu);
        WRITE_ONCE(best_rq->numa_migrate_on, 0);

        if (ret != 0)
                trace_sched_stick_numa(p, env.src_cpu, task_cpu(env.best_task));
        put_task_struct(env.best_task);
        return ret;
}
  • 코드 라인 8에서 태스크의 누마 그룹을 알아온다.
  • 코드 라인 9~12에서 best_cpu를 찾지 못하였거나 2개 이상의 active 누마 그룹이 있는 경우 모든 온라인 노드를 순회한다. 단 소스 노드와 누마 우선 노드는 skip 한다.
  • 코드 라인 14~19에서 두 소스 노드와 데스트 노드가 백플레인 타입으로 연결되었고 두 노드간의 거리가 env.dist가 아닌 경우 소스 노드에 대한 taskweight과 groupweight을 구한다.
  • 코드 라인 22~25에서 순회 중인 노드에 대한 태스크 weight과 순회 중인 노드에 대한 그룹 weight 둘 다 0보다 작은 경우는 skip 한다.
  • 코드 라인 27~30에서 순회 중인 노드의 러너블 로드와 capacity를 구한 후 idlest cpu를 찾아 env->best_cpu에 알아온다.
  • 코드 라인 42~50에서 태스크의 누마 그룹이 있는 경우 마이그레이트 할 cpu를 찾은 경우 해당 노드, 못 찾은 경우 원래 소스 노드로 태스크의 우선 노드를 결정한다.
  • 코드 라인 53~54에서 마이그레이트할 best cpu를 찾지 못한 경우 -EAGAIN을 반환한다.
  • 코드 라인 56~63에서 env.best_task가 지정되지 않은 경우 best cpu로 태스크를 마이그레이션한 후 그 결과를 갖고 함수를 빠져나간다.
  • 코드 라인 65~71에서 두 개의 태스크를 서로 교체한 후 그 결과를 갖고 함수를 빠져나간다.

 

update_numa_stats()

kernel/sched/fair.c

/*
 * XXX borrowed from update_sg_lb_stats
 */
static void update_numa_stats(struct numa_stats *ns, int nid)
{
        int cpu;

        memset(ns, 0, sizeof(*ns));
        for_each_cpu(cpu, cpumask_of_node(nid)) {
                struct rq *rq = cpu_rq(cpu);

                ns->load += cpu_runnable_load(rq);
                ns->compute_capacity += capacity_of(cpu);
        }

}

누마 stat을 갱신한다. 해당 노드의 러너블 로드와 capacity를 알아온다.

  • 코드 라인 5에서 누마 stat을 모두 0으로 클리어한다.
  • 코드 라인 6~11에서 @nid 노드에 대한 cpu들을 순회하며 러너블 로드와 capacity를 누적한다.

 

태스크를 누마 이동할 cpu 찾기

task_numa_find_cpu()

kernel/sched/fair.c

static void task_numa_find_cpu(struct task_numa_env *env,
                                long taskimp, long groupimp)
{
        long src_load, dst_load, load;
        bool maymove = false;
        int cpu;

        load = task_h_load(env->p);
        dst_load = env->dst_stats.load + load;
        src_load = env->src_stats.load - load;

        /*
         * If the improvement from just moving env->p direction is better
         * than swapping tasks around, check if a move is possible.
         */
        maymove = !load_too_imbalanced(src_load, dst_load, env);

        for_each_cpu(cpu, cpumask_of_node(env->dst_nid)) {
                /* Skip this CPU if the source task cannot migrate */
                if (!cpumask_test_cpu(cpu, env->p->cpus_ptr))
                        continue;

                env->dst_cpu = cpu;
                task_numa_compare(env, taskimp, groupimp, maymove);
        }
}

태스크를 누마 이동할 cpu를 찾아온다. cpu는 env->best_cpu에 알아오고, 교체할 태스크는 env->best_task에 알아온다.

  • 코드 라인 8~10에서 태스크가 마이그레이션된 후의 로드 값을 결정한다.
    • 마이그레이션될 데스트 노드에 태스크 로드를 추가하고,
    • 마이그레이션할 소스 노드에서는 태스크 로드를 감소시킨다.
  • 코드 라인 16에서 마이그레이션 후의 소스 노드와 데스트 노드 간의 로드가 차이가 적은지 여부를 maymove에 대입한다.
  • 코드 라인 18~25에서 데스트 노드의 cpu들을 순회하고 태스크가 지원하는 cpu에 대해서만 로드를 비교하여 idlest cpu를 찾아 env->best_cpu에 기록한다.

 

load_too_imbalanced()

kernel/sched/fair.c

static bool load_too_imbalanced(long src_load, long dst_load,
                                struct task_numa_env *env)
{
        long imb, old_imb;
        long orig_src_load, orig_dst_load;
        long src_capacity, dst_capacity;

        /*
         * The load is corrected for the CPU capacity available on each node.
         *
         * src_load        dst_load
         * ------------ vs ---------
         * src_capacity    dst_capacity
         */
        src_capacity = env->src_stats.compute_capacity;
        dst_capacity = env->dst_stats.compute_capacity;

        imb = abs(dst_load * src_capacity - src_load * dst_capacity);

        orig_src_load = env->src_stats.load;
        orig_dst_load = env->dst_stats.load;

        old_imb = abs(orig_dst_load * src_capacity - orig_src_load * dst_capacity);

        /* Would this change make things worse? */
        return (imb > old_imb);
}

태스크를 마이그레이션 했을 때의 소스 로드와 데스트 로드의 차이 값과 태스크를 마이그레이션 하기 전의 오리지날 소스 로드와 오리지날 데스트 로드의 차이를 비교한 결과를 반환한다. (1=마이그레이션 후가 더 불균형 상태, 0=마이그레이션 후 밸런스 상태)

  • 코드 라인 15~18에서 소스 로드와 데스트 로드에 노드 capacity 비율을 적용한 두 값의 차이를 imb에 대입한다.
  • 코드 라인 20~23에서 오리지날 소스 로드와 오리지날 데스트 로드에 노드 capacity 비율을 적용한 두 값의 차이를 old_imb에 대입한다.
  • 코드 라인 26에서 마이그레이션 후의 imb가 마이그레이션 전의 old_imb보다 큰 경우 오히려 더 불균형 상태로 판단되어 1을 반환하고, 그렇지 않은 경우 이미 밸런스로 판단하여 0을 반환한다.

 

task_numa_compare()

kernel/sched/fair.c -1/2-

/*
 * This checks if the overall compute and NUMA accesses of the system would
 * be improved if the source tasks was migrated to the target dst_cpu taking
 * into account that it might be best if task running on the dst_cpu should
 * be exchanged with the source task
 */
static void task_numa_compare(struct task_numa_env *env,
                              long taskimp, long groupimp, bool maymove)
{
        struct numa_group *cur_ng, *p_ng = deref_curr_numa_group(env->p);
        struct rq *dst_rq = cpu_rq(env->dst_cpu);
        long imp = p_ng ? groupimp : taskimp;
        struct task_struct *cur;
        long src_load, dst_load;
        int dist = env->dist;
        long moveimp = imp;
        long load;

        if (READ_ONCE(dst_rq->numa_migrate_on))
                return;

        rcu_read_lock();
        cur = rcu_dereference(dst_rq->curr);
        if (cur && ((cur->flags & PF_EXITING) || is_idle_task(cur)))
                cur = NULL;

        /*
         * Because we have preemption enabled we can get migrated around and
         * end try selecting ourselves (current == env->p) as a swap candidate.
         */
        if (cur == env->p)
                goto unlock;

        if (!cur) {
                if (maymove && moveimp >= env->best_imp)
                        goto assign;
                else
                        goto unlock;
        }

        /*
         * "imp" is the fault differential for the source task between the
         * source and destination node. Calculate the total differential for
         * the source task and potential destination task. The more negative
         * the value is, the more remote accesses that would be expected to
         * be incurred if the tasks were swapped.
         */
        /* Skip this swap candidate if cannot move to the source cpu */
        if (!cpumask_test_cpu(env->src_cpu, cur->cpus_ptr))
                goto unlock;

        /*
         * If dst and source tasks are in the same NUMA group, or not
         * in any group then look only at task weights.
         */
        cur_ng = rcu_dereference(cur->numa_group);
        if (cur_ng == p_ng) {
                imp = taskimp + task_weight(cur, env->src_nid, dist) -
                      task_weight(cur, env->dst_nid, dist);
                /*
                 * Add some hysteresis to prevent swapping the
                 * tasks within a group over tiny differences.
                 */
                if (cur_ng)
                        imp -= imp / 16;
        } else {
                /*
                 * Compare the group weights. If a task is all by itself
                 * (not part of a group), use the task weight instead.
                 */
                if (cur_ng && p_ng)
                        imp += group_weight(cur, env->src_nid, dist) -
                               group_weight(cur, env->dst_nid, dist);
                else
                        imp += task_weight(cur, env->src_nid, dist) -
                               task_weight(cur, env->dst_nid, dist);
        }

        if (maymove && moveimp > imp && moveimp > env->best_imp) {
                imp = moveimp;
                cur = NULL;
                goto assign;
        }

        /*
         * If the NUMA importance is less than SMALLIMP,
         * task migration might only result in ping pong
         * of tasks and also hurt performance due to cache
         * misses.
         */
        if (imp < SMALLIMP || imp <= env->best_imp + SMALLIMP / 2)
                goto unlock;
  • 코드 라인 3~10에서 env->p 태스크의 누마 그룹이 있는 경우 @groupimp, 없는 경우 @taskimp를 imp 및 moveimp에 대입한다.
  • 코드 라인 13~14에서 누마 마이그레이션이 진행 중인 경우 함수를 빠져나간다.
  • 코드 라인 17~19에서 dst 런큐에서 동작 중인 태스크가 이미 종료 중이거나 idle 상태인 경우 cur에 null을 대입한다.
  • 코드 라인 25~26에서 cur가 env->p와 동일한 경우 unlock 레이블로 이동한다.
  • 코드 라인 28~33에서 cur가 없는 경우 @maymove 이면서 moveimp 가 env->best_imp 이상인 경우 assign 레이블로 이동하고, 그렇지 않은 경우 unlock 레이블로 이동한다.
  • 코드 라인 43~44에서 src cpu가 cur 태스크가 허용하는 cpu에 없는 경우 unlock 레이블로 이동한다.
  • 코드 라인 50~71에서 cur 태스크와 env->p가 같은 누마 그룹에 있을 때와 그렇지 않을 때의 imp 값은 다음과 같이 산출한다.
    • 같은 경우 @task_imp + 소스 노드에서의 cur 태스크 weight – 데스트 노드에서의 cur 태스크 비중을 구한 후 1/16 만큼 감소시킨다.
    • 다른 경우 소스 노드에서의 cur 그룹 weight – 데스트 노드에서의 cur 그룹 weight을 imp에 추가한다. 만일 하나라도 그룹이 없으면 소스 노드에서의 cur 태스크 weight – 데스트 노드에서의 cur 태스크 weight을 imp에 추가한다.
  • 코드 라인 73~77에서 @maymove이고 moveimp가 imp나 env->best_imp보다 더 큰 경우 imp에 moveimp를 대입하고, cur에 null을 대입한 후 assign 레이블로 이동한다.
  • 코드 라인 85~86에서 마이그레이션으로 서로 ping-pong 하지 못하게 누마 importance 값이 너무 작은 경우  unlock 레이블로 이동한다.

 

kernel/sched/fair.c -2/2-

        /*
         * In the overloaded case, try and keep the load balanced.
         */
        load = task_h_load(env->p) - task_h_load(cur);
        if (!load)
                goto assign;

        dst_load = env->dst_stats.load + load;
        src_load = env->src_stats.load - load;

        if (load_too_imbalanced(src_load, dst_load, env))
                goto unlock;

assign:
        /*
         * One idle CPU per node is evaluated for a task numa move.
         * Call select_idle_sibling to maybe find a better one.
         */
        if (!cur) {
                /*
                 * select_idle_siblings() uses an per-CPU cpumask that
                 * can be used from IRQ context.
                 */
                local_irq_disable();
                env->dst_cpu = select_idle_sibling(env->p, env->src_cpu,
                                                   env->dst_cpu);
                local_irq_enable();
        }

        task_numa_assign(env, cur, imp);
unlock:
        rcu_read_unlock();
}
  • 코드 라인 4~6에서 env->p 태스크의 로드에서 cur 태스크의 로드를 뺀 로드를 load에 대입하고, 이 값이 0인 경우 assign 레이블로 이동한다.
  • 코드 라인 8~12에서 태스크를 마이그레이션 후에 로드가 더 불균형해지는 경우 마이그레이션을 포기하도록 unlock 레이블로 이동한다.
  • 코드 라인 14~28에서 assign: 레이블이다. cur가 없는 경우 env->dst_cpu에 idle cpu를 찾아오되 env->dst_cpu, env->src_cpu 및 캐시 공유 도메인 내 cpu들 순으로 처리한다.
  • 코드 라인 30에서 누마 best cpu, task 및 imp를 지정한다.
  • 코드 라인 31~32에서 unlock: 레이블이다. rcu read 락을 해제한 후 함수를 빠져나간다.

 

task_numa_assign()

kernel/sched/fair.c

static void task_numa_assign(struct task_numa_env *env,
                             struct task_struct *p, long imp)
{
        struct rq *rq = cpu_rq(env->dst_cpu);

        /* Bail out if run-queue part of active NUMA balance. */
        if (xchg(&rq->numa_migrate_on, 1))
                return;

        /*
         * Clear previous best_cpu/rq numa-migrate flag, since task now
         * found a better CPU to move/swap.
         */
        if (env->best_cpu != -1) {
                rq = cpu_rq(env->best_cpu);
                WRITE_ONCE(rq->numa_migrate_on, 0);
        }

        if (env->best_task)
                put_task_struct(env->best_task);
        if (p)
                get_task_struct(p);

        env->best_task = p;
        env->best_imp = imp;
        env->best_cpu = env->dst_cpu;
}

누마 마이그레이션용 best cpu, task 및 imp를 지정한다.

  • 코드 라인 4~8에서 dst_cpu가 이미 누마 마이그레이션이 진행 중인 경우 함수를 빠져나간다.
  • 코드 라인 14~17에서 best_cpu를 처음 결정하는 경우 누마 마이그레이션 진행중 표시를 제거한다.
  • 코드 라인 19~20에서 기존에 설정되었던 best_task의 참조는 1 감소시킨다.
  • 코드 라인 21~24에서 태스크 @p를 best_task로 지정한다.
  • 코드 라인 25~26에서 @imp를 best_imp로 지정하고, dst_cpu를 best_cpu로 지정한다.

 


구조체

task_struct 구조체 (NUMA 밸런싱 부분)

include/linux/sched.h

struct task_struct {
...
#ifdef CONFIG_NUMA_BALANCING
        int                             numa_scan_seq;
        unsigned int                    numa_scan_period;
        unsigned int                    numa_scan_period_max;
        int                             numa_preferred_nid;
        unsigned long                   numa_migrate_retry;
        /* Migration stamp: */
        u64                             node_stamp;
        u64                             last_task_numa_placement;
        u64                             last_sum_exec_runtime;
        struct callback_head            numa_work;

        /*
         * This pointer is only modified for current in syscall and
         * pagefault context (and for tasks being destroyed), so it can be read
         * from any of the following contexts:
         *  - RCU read-side critical section
         *  - current->numa_group from everywhere
         *  - task's runqueue locked, task not running
         */
        struct numa_group __rcu         *numa_group;

        /*
         * numa_faults is an array split into four regions:
         * faults_memory, faults_cpu, faults_memory_buffer, faults_cpu_buffer
         * in this precise order.
         *
         * faults_memory: Exponential decaying average of faults on a per-node
         * basis. Scheduling placement decisions are made based on these
         * counts. The values remain static for the duration of a PTE scan.
         * faults_cpu: Track the nodes the process was running on when a NUMA
         * hinting fault was incurred.
         * faults_memory_buffer and faults_cpu_buffer: Record faults per node
         * during the current scan window. When the scan completes, the counts
         * in faults_memory and faults_cpu decay and these values are copied.
         */
        unsigned long                   *numa_faults;
        unsigned long                   total_numa_faults;

        /*
         * numa_faults_locality tracks if faults recorded during the last
         * scan window were remote/local or failed to migrate. The task scan
         * period is adapted based on the locality of the faults with different
         * weights depending on whether they were shared or private faults
         */
        unsigned long                   numa_faults_locality[3];

        unsigned long                   numa_pages_migrated;
#endif /* CONFIG_NUMA_BALANCING */
...
}
  • numa_scan_seq
    • 누마 스캔 주기마다 증가되는 시퀀스
  • numa_scan_period
    • 누마 스캔 주기
  • numa_scan_period_max
    • 누마 스캔 최대 주기
  • numa_preferred_nid
    • 누마 우선 노드
  • numa_migrate_retry
    • 누마 migration 재시도 수
  • node_stamp
    • 다음 누마 스캔 시각이 담긴다.
  • last_task_numa_placement
    • 누마 placement 지난 갱신 시각
  • last_sum_exec_runtime
    • 누마 placement용 누적 실행 시간
  • numa_work
    • 누마 밸런싱 work이 담기는 callback head
  • *numa_group
    • 누마 그룹 구조체를 가리킨다.
  • *numa_faults
    • 다음 4 종류의 영역이 있고, 각각 노드별 및 private/shared 별 누마 faults 값이 담긴다. (예: 6 개의노드인 경우 배열은 4 * 6 * 2 = 48개가 할당된다)
      • faults_memory
        • 메모리 노드에 대한 누마 faults의 exponential decay 된 값이 누적된다.
      • faults_cpu
        • cpu 노드에 대한 누마 faults의 exponential decay 된 값이 누적된다.
      • faults_memory_buffer
        • 메모리 노드에 대한 누마 faults가 누적되고, 스캔 윈도우 주기마다 클리어된다.
      • faults_cpu_buffer
        • cpu 노드에 대한 누마 faults가 누적되고, 스캔 윈도우 주기마다 클리어된다.
  • total_numa_faults
    • 누마 faults의 exponential decay 된 값이 누적된다.
  • numa_faults_locality[3]
    • 다음과 같이 노드 지역성별로 numa faults 값이 담기며, 누마 스캔 주기마다 클리어된다.
      • remote
      • local
      • failed to migrate
  • numa_pages_migrated
    • 누마 마이그레이션 페이지 수

 

numa_group 구조체

kernel/sched/fair.c

struct numa_group {
        refcount_t refcount;

        spinlock_t lock; /* nr_tasks, tasks */
        int nr_tasks;
        pid_t gid;
        int active_nodes;

        struct rcu_head rcu;
        unsigned long total_faults;
        unsigned long max_faults_cpu;
        /*
         * Faults_cpu is used to decide whether memory should move
         * towards the CPU. As a consequence, these stats are weighted
         * more by CPU use than by memory faults.
         */
        unsigned long *faults_cpu;
        unsigned long faults[0];
};
  • refcount
    • 태스크들의 누마 그룹에 대한 참조 카운터
  • nr_tasks
    • 이 누마 그룹에 조인한 태스크 수
  • gid
    • 그룹 id
  • active_nodes
    • 페이지가 일정 부분 액세스를 하는 노드 수로 노드들 중 faults 수가 max_faults의 1/3 이상인 노드 수이다.
  • rcu
  • total_faults
    • 이 그룹의 전체 numa faults
    • 태스크의 numa 밸런싱 참여 시 추가되고, 태스크가 종료될 때 태스크의 누마 faults가 감소한다.
  • max_faults_cpu
  • *faults_cpu
  • faults[]
    • 누마 faults로 태스크에 저장되는 것과 동일하게 4 종류의 영역에 각각 노드별 및 private/shared 별로 numa faults 값이 담긴다.

 

numa_stats 구조체

kernel/sched/fair.c

/* Cached statistics for all CPUs within a node */
struct numa_stats {
        unsigned long load;

        /* Total compute capacity of CPUs on a node */
        unsigned long compute_capacity;
};
  • load
    • 노드 내 cpu들의 러너블 로드 합계
  • compute_capacity
    • 노드 내 cpu들의 scaled cpu capactiy 합계

 

task_numa_env 구조체

kernel/sched/fair.c

struct task_numa_env {
        struct task_struct *p;

        int src_cpu, src_nid;
        int dst_cpu, dst_nid;

        struct numa_stats src_stats, dst_stats;

        int imbalance_pct;
        int dist;

        struct task_struct *best_task;
        long best_imp;
        int best_cpu;
};
  • *p
    • 마이그레이션 할 태스크
  • src_cpu
    • 소스 cpu
  • src_nid
    • 소느 노드 id
  • dst_cpu
    • 데스트(목적지) cpu
  • dst_nid
    • 데스트(목적지) 노드 id
  • src_stats
    • 소스 노드의 러너블 로드와 scaled cpu capacity가 담긴다.
  • dst_stats
    • 데스트(목적지) 노드의 러너블 로드와 scaled cpu capacity가 담긴다.
  • imbalance_pct
    • 불균형 스레졸드 값(100 이상 = 100% 이상)
  • dist
    • 노드 distance
  • *best_task
    • swap할 best 태스크
  • best_imp
    • 중요(importance)도
  • best_cpu
    • 마이그레이션할 최적의 cpu

 

참고

 

 

Scheduler -14- (Scheduling Domain 2)

<kernel v5.4>

cgroup에서 사용하는 “그룹 스케줄링”과 로드 밸런싱에서 사용하는 “스케줄 그룹”은 서로 다른 기술을 의미하므로 각별히 주의해야한다.

 

스케줄링 도메인과 스케줄 그룹

리눅스 커널이 적절한 로드밸런싱을 수행하기 위해 모든 cpu들이 동등한 조건으로 로드밸런싱을 할 수가 없다. 각 cpu들의 성능, 캐시 공유, 노드에 따른 메모리 대역폭이 달라 다른 성능을 나태낸다. 가장 로드밸런싱이 용이한 cpu들끼리 그루핑을하고, 그 다음 레벨의 cpu들과 그루핑하는 식으로 레벨별로 묶어 로드 밸런싱에 대한 우선 순위를 결정하기 위해 cpu 토플로지를 파악하고, 이를 통해 스케줄링 도메인 토플로지 레벨을 구성한다. 그런 후 최종 단계에서 스케줄링 도메인과 스케줄 그룹을 구성하면 로드 밸런싱에서 이들을 이용하게 된다.

스케줄링 도메인 토플로지

1) arm 스케줄링 도메인 토플로지

32bit arm cpu 토플로지를 만들때 다음과 같은 순서로 단계별로 구성한다.

  • GMC
    • core power 제어 가능한 그룹
    • CONFIG_SCHED_MC 커널 옵션 사용
  • MC
    • 그 다음으로 l2 캐시를 공유하는 클러스터 내의 cpu에 더 우선권을 준다.
    • CONFIG_SCHED_MC 커널 옵션 사용
  • DIE
    • 그 다음으로 (l3 캐시를 공유하는) 같은 die를 사용하는 cpu에 더 우선권을 준다.
  • NUMA
    • 같은 NUMA 노드를 사용하는 cpu에 더 우선권을 준다.
    • NUMA 노드에서 최단 거리 path를 사용하는 cpu에 더 우선권을 준다.
    • CONFIG_NUMA 커널 옵션 사용

 

2) arm64 및 디폴트 스케줄링 도메인 토플로지

ARM64 및 RISC-V 시스템의 토플로지를 만들때 다음과 같은 순서로 단계별로 구성한다.

  • SMT
    • l1 캐시를 공유하는 cpu에 대해 더 우선권을 준다. (virtual core)
    • 캐시를 플러시할 필요가 없으므로 가장 비용이 저렴하다.
    • arm64는 아직 활용하지 않는다.
    • CONFIG_SCHED_SMT 커널 옵션 사용
  • MC
    • 그 다음으로 l2 캐시를 공유하는 클러스터 내의 cpu에 더 우선권을 준다.
    • big/little 등 클러스터가 존재하는 경우 이 옵션을 사용하여 클러스터 단위로 관리한다.
    • CONFIG_SCHED_MC 커널 옵션 사용
  • DIE
    • 그 다음으로 (l3 캐시를 공유하는) 같은 die를 사용하는 cpu에 더 우선권을 준다.
    • 보통 멀티 클러스터가 아닌 대부분의 몇 개 코어 이하의 시스템들은 이 단계만 사용한다.
  • NUMA
    • NUMA 레벨은 NUMA distance 별로 여러 개의 레벨을 사용할 수 있다.
    • NUMA 노드에서 최단 거리 path를 사용하는 cpu에 더 우선권을 준다.
    • CONFIG_SCHED_NUMA 커널 옵션 사용

 

다음 그림 3가지는 시스템 구성에 따른 cpu topology를 나타낸다.

  • NODE 단계의 도메인 토플로지는 DIE 단계의 토플로지와 동일한 cpu들로 구성되었고, 그룹도 1개만 포함되었다. 이러한 구성은 로드밸런싱에 불필요하므로 보통 삭제된다.

 

 

 

스케줄 도메인 플래그

  • SD_LOAD_BALANCE
    • 이 도메인에서 로드 밸런싱을 허용한다.
  • SD_BALANCE_NEWIDLE
    • 이 도메인에서 새롭게 idle로 진입하는 로드 밸런싱을 허용한다. (idle 밸런싱)
  • SD_BALANCE_EXEC
    • 이 도메인은 실행되는 태스크에 대해 로드 밸런싱을 허용한다. (exec 밸런싱)
  • SD_BALANCE_FORK
    • 이 도메인은 새롭게 fork한 태스크에 대해 로드 밸런싱을 허용한다. (fork 밸런싱)
  • SD_BALANCE_WAKE
    • 이 도메인은 idle 상태에서 깨어난 cpu에 대해 로드 밸런싱을 허용한다. (wake 밸런싱)
  • SD_WAKE_AFFINE
    • 이 도메인은 idle 상태에서 깨어난 cpu가 도메인내의 idle sibling cpu 선택을 허용한다.
  • SD_ASYM_CPUCAPACITY
    • 도메인 내의 멤버들이 다른 cpu 성능을 가진다.
  • SD_SHARE_CPUCAPACITY
    • 이 도메인은 cpu 성능을 공유한다.
    • 하드웨어 스레드들은 하나의 코어에 대한 성능을 공유한다.
    • x86(하이퍼 스레드)이나 powerpc의 SMT 도메인 토플로지 레벨에서 사용한다.
  • SD_SHARE_POWERDOMAIN
    • 이 도메인에서 파워를 공유한다.
    • 절전을 위해 클러스터 단위로 core들의 파워를 제어한다. (빅/리틀 클러스터 등)
    • 현재 arm/arm64의 GMC 도메인 토플로지 레벨에서 사용한다.
  • SD_SHARE_PKG_RESOURCES
    • 이 도메인에서 패키시 내의 각종 캐시 등의 리소스를 공유한다.
    • arm, arm64의 경우 보통 한 패키지(DIE) 안에 구성된 단위 클러스터내의 코어들이 캐시를 공유한다. (L2 캐시 등)
    • x86이나 powerpc 같은 경우 하드웨어 스레드(SMT)와 코어(MC)가 캐시를 공유한다. (L2 또는 L3 캐시까지)
  • SD_SERIALIZE
    • 이 도메인은 누마 시스템에서 싱글 로드밸런싱에서만 사용된다.
  • SD_ASYM_PACKING
    • 낮은 번호의 하드웨어 스레드가 높은 더 성능을 가진다. (비균형)
    • powerpc의 SMT 도메인 토플로지 레벨에서 사용한다.
    • ITMT(Intel Turbo Boost Max Technology 3.0) 기능을 지원하는 x86 시스템에서 SMT 및 MC 도메인에서 사용된다.
  • SD_PREFER_SIBLING
    • sibling 도메인내에서 태스크를 수행할 수 있도록 권장한다.
    • SD_NUMA, SD_SHARE_PKG_RESOURCES(MC, SMT), SD_SHARE_CPUCAPACITY(SMT)가 설정되지 않은 도메인에서만 사용 가능하므로 주로 DIE 도메인에서 사용된다.
  • SD_OVERLAP
    • 도메인들 간에 오버랩되는 경우 사용한다.
  • SD_NUMA
    • 이 도메인이 누마 도메인이다.
    • NUMA distance 단계 수 만큼의 도메인 레벨을 구성하여 사용한다.

 

적절한 로드밸런싱을 위해 태스크의 migration 비용을 고려하여 언제 수행해야 할 지 다음의 항목들을 체크한다.

  • 스케줄러 특성에 따른 로드 밸런싱
  • cpu 토플로지 레벨에 따른 로드 밸런싱
    • cpu affinity별 로드 밸런싱을 수행해야 하기 때문에 단계별 cpu 토플로지를 만들어 사용한다.
    • cpu capacity(core 능력치 * freq)를 파악하여 코어별 로드 밸런싱에 사용한다.
    • 도메인 그룹 간 균형을 맞추기 위한 active 밸런싱

 

스케줄러 특성 별 로드 밸런싱

다음과 같이 사용하는 스케줄러에 따라  로드밸런싱을 하는 방법이 달라진다.  cpu의 검색 순서는 모든 스케줄러가 스케줄링 도메인 레벨을 차례 대로 사용한다.

  • Deadline 스케줄러
    • dl 태스크가 2 개 이상 동작해야 하는 경우 그 중 deadline이 가장 급한 dl 태스크를 제외하고 나머지들을 다른 cpu로 옮기려한다.
    • 다른 cpu들에서 dl 태스크가 수행되고 있지 않거나 deadline이 가장 큰 dl 태스크가 동작하는 cpu를 찾아 그 cpu로 dl 태스크를 migration 한다.
  • RT 스케줄러
    • rt 태스크가 2개 이상 동작해야 하는 경우 그 중 우선 순위가 가장 높은 rt 태스크를 제외한 나머지들을 다른 cpu로 옮기려한다.
    • 다른 cpu들에서 rt 태스크가 수행되고 있지 않거나 우선 순위가 가장 낮은 rt 태스크가 동작하는 cpu를 찾아 그 cpu로 rt 태스크를 migration 한다.
  • CFS 스케줄러
    • cpu 로드가 가장 낮은 cpu로 cfs 태스크를 migration 한다.
    • cpu 로드가 얼마 이상일 때 수행할지 여부

 


스케줄링 도메인 토플로지 레벨 with NUMA

kernel_init() -> kernel_init_freeable() -> sched_init_smp() 함수에서 호출된다.

 

sched_init_numa()

kernel/sched/topology.c -1/3-

void sched_init_numa(void)
{
        int next_distance, curr_distance = node_distance(0, 0);
        struct sched_domain_topology_level *tl;
        int level = 0;
        int i, j, k;

        sched_domains_numa_distance = kzalloc(sizeof(int) * (nr_node_ids + 1), GFP_KERNEL);
        if (!sched_domains_numa_distance)
                return;

        /* Includes NUMA identity node at level 0. */
        sched_domains_numa_distance[level++] = curr_distance;
        sched_domains_numa_levels = level;

        /*
         * O(nr_nodes^2) deduplicating selection sort -- in order to find the
         * unique distances in the node_distance() table.
         *
         * Assumes node_distance(0,j) includes all distances in
         * node_distance(i,j) in order to avoid cubic time.
         */
        next_distance = curr_distance;
        for (i = 0; i < nr_node_ids; i++) {
                for (j = 0; j < nr_node_ids; j++) {
                        for (k = 0; k < nr_node_ids; k++) {
                                int distance = node_distance(i, k);

                                if (distance > curr_distance &&
                                    (distance < next_distance ||
                                     next_distance == curr_distance))
                                        next_distance = distance;

                                /*
                                 * While not a strong assumption it would be nice to know
                                 * about cases where if node A is connected to B, B is not
                                 * equally connected to A.
                                 */
                                if (sched_debug() && node_distance(k, i) != distance)
                                        sched_numa_warn("Node-distance not symmetric");

                                if (sched_debug() && i && !find_numa_distance(distance))
                                        sched_numa_warn("Node-0 not representative");
                        }
                        if (next_distance != curr_distance) {
                                sched_domains_numa_distance[level++] = next_distance;
                                sched_domains_numa_levels = level;
                                curr_distance = next_distance;
                        } else break;
                }

                /*
                 * In case of sched_debug() we verify the above assumption.
                 */
                if (!sched_debug())
                        break;
        }
  • 코드 라인 8~10에서 numa distance 배열을 노드 수 + 1(for null finish) 만큼 할당한다.
  • 코드 라인 13~14에서 0번 레벨에 대한 값을 지정한다. 0번 레벨에는 from 0 to 0에 대한 distance 값으로 지정한다.
    • ARM64의 경우 디바이스 트리를 통한 numa_distance[] 배열의 초기화 및 파싱 함수는 다음과 같다.
  • 코드 라인 23~57에서 distance 순서대로 정렬하여 sched_domains_numa_distance[]을 구성한다.
    • 예) 총 5 레벨: [0]=10, [1]=15, [2]=20, [3]=25, [4]=30

 

kernel/sched/topology.c -2/3-

.       /*
         * 'level' contains the number of unique distances
         *
         * The sched_domains_numa_distance[] array includes the actual distance
         * numbers.
         */

        /*
         * Here, we should temporarily reset sched_domains_numa_levels to 0.
         * If it fails to allocate memory for array sched_domains_numa_masks[][],
         * the array will contain less then 'level' members. This could be
         * dangerous when we use it to iterate array sched_domains_numa_masks[][]
         * in other functions.
         *
         * We reset it to 'level' at the end of this function.
         */
        sched_domains_numa_levels = 0;

        sched_domains_numa_masks = kzalloc(sizeof(void *) * level, GFP_KERNEL);
        if (!sched_domains_numa_masks)
                return;

        /*
         * Now for each level, construct a mask per node which contains all
         * CPUs of nodes that are that many hops away from us.
         */
        for (i = 0; i < level; i++) {
                sched_domains_numa_masks[i] =
                        kzalloc(nr_node_ids * sizeof(void *), GFP_KERNEL);
                if (!sched_domains_numa_masks[i])
                        return;

                for (j = 0; j < nr_node_ids; j++) {
                        struct cpumask *mask = kzalloc(cpumask_size(), GFP_KERNEL);
                        if (!mask)
                                return;

                        sched_domains_numa_masks[i][j] = mask;

                        for_each_node(k) {
                                if (node_distance(j, k) > sched_domains_numa_distance[i])
                                        continue;

                                cpumask_or(mask, mask, cpumask_of_node(k));
                        }
                }
        }
  • 코드 라인 17에서sched_domains_numa_leves를 일단 0으로 리셋한다. 이 값은 함수 끝에서 다시 지정된다.
  • 코드 라인 19~21에서 sched_domains_numa_masks[]에 먼저 레벨 수 만큼 포인터 배열을 할당한다.
  • 코드 라인 27~47에서 sched_domains_numa_masks[][]에 레벨 * 노드 수 만큼 cpu 비트 마스크를 할당하고 소속된 cpu들을 설정한다.
    • sched_domains_numa_distance[][] 배열의 각 distance에 해당 distance 뿐만 아니라 그 이하 distance에 속한 cpu 까지도 포함된다.

 

kernel/sched/topology.c -3/3-

        /* Compute default topology size */
        for (i = 0; sched_domain_topology[i].mask; i++);

        tl = kzalloc((i + level + 1) *
                        sizeof(struct sched_domain_topology_level), GFP_KERNEL);
        if (!tl)
                return;

        /*
         * Copy the default topology bits..
         */
        for (i = 0; sched_domain_topology[i].mask; i++)
                tl[i] = sched_domain_topology[i];

        /*
         * Add the NUMA identity distance, aka single NODE.
         */
        tl[i++] = (struct sched_domain_topology_level){
                .mask = sd_numa_mask,
                .numa_level = 0,
                SD_INIT_NAME(NODE)
        };

        /*
         * .. and append 'j' levels of NUMA goodness.
         */
        for (j = 1; j < level; i++, j++) {
                tl[i] = (struct sched_domain_topology_level){
                        .mask = sd_numa_mask,
                        .sd_flags = cpu_numa_flags,
                        .flags = SDTL_OVERLAP,
                        .numa_level = j,
                        SD_INIT_NAME(NUMA)
                };
        }

        sched_domain_topology = tl;

        sched_domains_numa_levels = level;
        sched_max_numa_distance = sched_domains_numa_distance[level - 1];

        init_numa_topology_type();
}
  • 코드 라인 2~7에서 디폴트 토플로지 수 + numa distance 레벨 수 + 1(for null terminate)만큼 tl(토플로지 레벨)을 할당한다.
    • 예) MC + DIE + NUMA(10) + NUMA(15) + NUMA(20) + NUMA(30) + NULL
  • 코드 라인 12~13에서 디폴트 토플로지의 값을 새로 할당한 tl로 옮긴다.
  • 코드 라인 18~22에서 NUMA 0 레벨을 추가한다.
  • 코드 라인 27~35에서 NUMA 1 레벨부터 마지막 레벨까지 초기화한다.
  • 코드 라인 37에서 스케줄러용 sched_domain_topology 변수에 새로 구성한 tl을 대입한다.
  • 코드 라인 39에서 sched_domains_numa_levels에 NUMA 레벨을 대입한다.
    • 예) 10, 15, 20, 30인 경우 레벨 수=4
  • 코드 라인 40에서 sched_max_numa_distance에 가장 거리가 먼 distance를 대입한다.
  • 코드 라인 42에서 numa topology 타입 3 가지 중 하나를 선택한다.

 

다음 그림은 NUMA 시스템의 도메인 토플로지 레벨을 구성하는 모습을 보여준다.

 

init_numa_topology_type()

kernel/sched/topology.c

/*
 * A system can have three types of NUMA topology:
 * NUMA_DIRECT: all nodes are directly connected, or not a NUMA system
 * NUMA_GLUELESS_MESH: some nodes reachable through intermediary nodes
 * NUMA_BACKPLANE: nodes can reach other nodes through a backplane
 *
 * The difference between a glueless mesh topology and a backplane
 * topology lies in whether communication between not directly
 * connected nodes goes through intermediary nodes (where programs
 * could run), or through backplane controllers. This affects
 * placement of programs.
 *
 * The type of topology can be discerned with the following tests:
 * - If the maximum distance between any nodes is 1 hop, the system
 *   is directly connected.
 * - If for two nodes A and B, located N > 1 hops away from each other,
 *   there is an intermediary node C, which is < N hops away from both
 *   nodes A and B, the system is a glueless mesh.
 */
static void init_numa_topology_type(void)
{
        int a, b, c, n;

        n = sched_max_numa_distance;

        if (sched_domains_numa_levels <= 2) {
                sched_numa_topology_type = NUMA_DIRECT;
                return;
        }

        for_each_online_node(a) {
                for_each_online_node(b) {
                        /* Find two nodes furthest removed from each other. */
                        if (node_distance(a, b) < n)
                                continue;

                        /* Is there an intermediary node between a and b? */
                        for_each_online_node(c) {
                                if (node_distance(a, c) < n &&
                                    node_distance(b, c) < n) {
                                        sched_numa_topology_type =
                                                        NUMA_GLUELESS_MESH;
                                        return;
                                }
                        }

                        sched_numa_topology_type = NUMA_BACKPLANE;
                        return;
                }
        }
}

다음 3가지 NUMA topology 타입 중 하나를 선택한다.

  • NUMA_DIRECT
    • 모든 노드 간에 1 hop으로 접근할 수 있는 경우이다.
  • NUMA_GLULESS_MESH
    • 모든 노드 간에 1 hop으로 접근할 수 없지만, 2 hop 이내에 접근 가능한 경우이다.
  • NUMA_BACKPLANE
    • 모든 노드 간에 2 hop 이내에서 접근 할 수 없는 경우이다.

 

다음 그림은 numa topology 타입을 보여준다.

 


스케줄링 도메인들 초기화

kernel_init() -> kernel_init_freeable() -> sched_init_smp(cpu_active_mask) 함수에서 최종 호출된다.

 

sched_init_domains() 함수 이후로 다음과 같은 함수들이 호출되어 처리한다.

 

sched_init_domains()

kernel/sched/topology.c

/*
 * Set up scheduler domains and groups.  For now this just excludes isolated
 * CPUs, but could be used to exclude other special cases in the future.
 */
int sched_init_domains(const struct cpumask *cpu_map)
{
        int err;

        zalloc_cpumask_var(&sched_domains_tmpmask, GFP_KERNEL);
        zalloc_cpumask_var(&sched_domains_tmpmask2, GFP_KERNEL);
        zalloc_cpumask_var(&fallback_doms, GFP_KERNEL);

        arch_update_cpu_topology();
        ndoms_cur = 1;
        doms_cur = alloc_sched_domains(ndoms_cur);
        if (!doms_cur)
                doms_cur = &fallback_doms;
        cpumask_and(doms_cur[0], cpu_map, housekeeping_cpumask(HK_FLAG_DOMAIN));
        err = build_sched_domains(doms_cur[0], NULL);
        register_sched_domain_sysctl();

        return err;
}

요청한 cpu 맵을 사용하여 스케줄 도메인들을 초기화한다.

  • 코드 라인 9에서 아키텍처가 지원하는 경우 cpu topology를 갱신한다. (arm, arm64는 지원하지 않음)
  • 코드 라인 10~13에서 스케줄 도메인의 수를 1로 지정하고 하나의 스케줄 도메인용 비트마스크를 할당해온다. 할당이 실패한 경우 싱글 cpumask로 이루어진 fallback 도메인을 사용한다.
  • 코드 라인 14에서 인수로 전달받은 cpu_map에서 cpu_isolated_map을 제외시킨 cpumask를 doms_cur[0]에 대입한다.
  • 코드 라인 15에서 산출된 cpumask 만큼 스케줄 도메인을 구성한다.
  • 코드 라인 16에서 스케줄 도메인들을 sysctl에 구성한다.

 

alloc_sched_domains()

kernel/sched/core.c

cpumask_var_t *alloc_sched_domains(unsigned int ndoms)
{
        int i;
        cpumask_var_t *doms;

        doms = kmalloc(sizeof(*doms) * ndoms, GFP_KERNEL);
        if (!doms)
                return NULL;
        for (i = 0; i < ndoms; i++) {
                if (!alloc_cpumask_var(&doms[i], GFP_KERNEL)) {
                        free_sched_domains(doms, i);
                        return NULL;
                }
        }
        return doms;
}

요청한 스케줄 도메인 수 만큼의 cpu 비트마스크 어레이를 할당하고 반환한다.

  • 코드 라인 6~8에서 요청한 스케줄 도메인 수 만큼 스케줄 도메인용 비트마스크를 할당한다.
  • 코드 라인 9~14에서 대단위 cpumask가  필요한 경우 할당받아온다.
    • 32bit 시스템에서는 cpu가 32개를 초과하는 경우,
    • 64bit 시스템에서는 cpu가 64개를 초과하는 경우 별도의 cpumask를 할당받는다.

 

“isolcpus=” 커널 파라메터

/* cpus with isolated domains */
static cpumask_var_t cpu_isolated_map;

/* Setup the mask of cpus configured for isolated domains */
static int __init isolated_cpu_setup(char *str)
{
        alloc_bootmem_cpumask_var(&cpu_isolated_map);
        cpulist_parse(str, cpu_isolated_map);
        return 1;
}

__setup("isolcpus=", isolated_cpu_setup);

아이솔레이티드 도메인들을 위해  지정된 cpu리스트들을 마스크한다.

  • 지정된 태스크들로 태스크들이 스케줄되지 않도록 분리시킨다. 이렇게 분리된 cpu에서도 인터럽트는 사용될 수 있다.
  • “cat /sys/devices/system/cpu/isolated”으로 확인할 수 있다.
  • 예) “isolcpus=0,1”
  • 참고: how to detect if isolcpus is activated? | Linux & Unix

 

build_sched_domains()

kernel/sched/topology.c -1/2-

/*
 * Build sched domains for a given set of CPUs and attach the sched domains
 * to the individual CPUs
 */
static int
build_sched_domains(const struct cpumask *cpu_map, struct sched_domain_attr *attr)
{
        enum s_alloc alloc_state = sa_none;
        struct sched_domain *sd;
        struct s_data d;
        struct rq *rq = NULL;
        int i, ret = -ENOMEM;
        struct sched_domain_topology_level *tl_asym;
        bool has_asym = false;

        if (WARN_ON(cpumask_empty(cpu_map)))
                goto error;

        alloc_state = __visit_domain_allocation_hell(&d, cpu_map);
        if (alloc_state != sa_rootdomain)
                goto error;

        tl_asym = asym_cpu_capacity_level(cpu_map);

        /* Set up domains for CPUs specified by the cpu_map: */
        for_each_cpu(i, cpu_map) {
                struct sched_domain_topology_level *tl;

                sd = NULL;
                for_each_sd_topology(tl) {
                        int dflags = 0;

                        if (tl == tl_asym) {
                                dflags |= SD_ASYM_CPUCAPACITY;
                                has_asym = true;
                        }

                        sd = build_sched_domain(tl, cpu_map, attr, sd, dflags, i);

                        if (tl == sched_domain_topology)
                                *per_cpu_ptr(d.sd, i) = sd;
                        if (tl->flags & SDTL_OVERLAP)
                                sd->flags |= SD_OVERLAP;
                        if (cpumask_equal(cpu_map, sched_domain_span(sd)))
                                break;
                }
        }

스케줄 도메인을 구성한다. 이 함수를 호출하는 루틴은 다음 두 개가 있다.

  • sched_init_domains() -> 커널 부트업 시 속성값을 null로 진입
  • partition_sched_domains() -> cpu on/off 시 진입된다.

 

  • 코드 라인 15~17에서 스케줄 도메인 토플로지의 자료 구조를 할당받고 초기화하고 루트 도메인을 할당받은 후 초기화한다.
  • 코드 라인 19에서 동일하지 않은 cpu capacity 성능을 가진 경우 해당 도메인을 알아온다.
  • 코드 라인 22에서 cpu_map 비트마스크에 설정된 cpu에 대해 순회한다.
  • 코드 라인 26~34에서 스케줄 도메인 계층 구조를 순회하며 스케줄 도메인을 구성한다. 또한 asym 토플로지 레벨에 대해서는 dflags에 SD_ASYM_CPUCAPACITY 플래그를 추가하여 둔다.
  • 코드 라인 36~37에서 순회중인 tl이 전역 스케줄 도메인 토플로지인 경우 s_data.sd의 현재 순회중인 cpu에 구성한 스케줄 도메인을 연결한다.
  • 코드 라인 38~39에서 토플로지 레벨 tl에 SDTL_OVERLAP 설정된 경우 스케줄 도메인에도 SD_OVERLAP 플래그를 추가한다.
  • 코드 라인 40~41에서 cpu_map과 스케줄 도메인의 span 구성이 동일한 경우 루프를 벗어난다.

 

kernel/sched/topology.c -2/2-

        /* Build the groups for the domains */
        for_each_cpu(i, cpu_map) {
                for (sd = *per_cpu_ptr(d.sd, i); sd; sd = sd->parent) {
                        sd->span_weight = cpumask_weight(sched_domain_span(sd));
                        if (sd->flags & SD_OVERLAP) {
                                if (build_overlap_sched_groups(sd, i))
                                        goto error;
                        } else {
                                if (build_sched_groups(sd, i))
                                        goto error;
                        }
                }
        }

        /* Calculate CPU capacity for physical packages and nodes */
        for (i = nr_cpumask_bits-1; i >= 0; i--) {
                if (!cpumask_test_cpu(i, cpu_map))
                        continue;

                for (sd = *per_cpu_ptr(d.sd, i); sd; sd = sd->parent) {
                        claim_allocations(i, sd);
                        init_sched_groups_capacity(i, sd);
                }
        }

        /* Attach the domains */
        rcu_read_lock();
        for_each_cpu(i, cpu_map) {
                rq = cpu_rq(i);
                sd = *per_cpu_ptr(d.sd, i);

                /* Use READ_ONCE()/WRITE_ONCE() to avoid load/store tearing: */
                if (rq->cpu_capacity_orig > READ_ONCE(d.rd->max_cpu_capacity))
                        WRITE_ONCE(d.rd->max_cpu_capacity, rq->cpu_capacity_orig);

                cpu_attach_domain(sd, d.rd, i);
        }
        rcu_read_unlock();

        if (has_asym)
                static_branch_inc_cpuslocked(&sched_asym_cpucapacity);

        if (rq && sched_debug_enabled) {
                pr_info("root domain span: %*pbl (max cpu_capacity = %lu)\n",
                        cpumask_pr_args(cpu_map), rq->rd->max_cpu_capacity);
        }

        ret = 0;
error:
        __free_domain_allocs(&d, alloc_state, cpu_map);

        return ret;
}
  • 코드 라인 2~3에서 cpu_map에 설정된 cpu를 순회하고 그 하위 루프에서 해당 cpu의 하위 스케줄 도메인부터 상위 스케줄 도메인까지 순회한다.
  • 코드 라인 4에서 스케줄 도메인의 span_weight 멤버에 도메인에 속한 cpu 수를 대입한다.
  • 코드 라인 5~7에서 NUMA 스케줄 도메인 단계에서는 overlap 스케줄 그룹을 구성한다.
  • 코드 라인 8~11에서 그 외의 경우 일반 스케줄 그룹을 구성한다.
  • 코드 라인 16~18에서 cpu 수 만큼 거꾸로 순회하며 cpu_map에 설정되지 않은 cpu는 skip 한다.
  • 코드 라인 20~23에서 하위 스케줄 도메인부터 상위 스케줄 도메인까지 순회하며 스케줄링 도메인 토플로지에 구성된 sd, sg, sgc 들에 null을 넣어 함수의 가장 마지막에서 삭제하지 않도록 만든다. 그리고 스케줄링 그룹의 capacity를 초기화한다.
  • 코드 라인 28~37에서 cpu_map에 설정된 cpu를 순회하며 도메인을 연결한다.
  • 코드 라인 40~41에서 asym cpu capacity가 적용되도록 활성화한다.
  • 코드 라인 50~52에서 스케줄링 도메인 토플로지에 구성된 sd, sg, sgc 멤버들에서 사용되지 않는 구조체 할당들을 모두 해제한다.

 


스케줄 도메인들 할당

__visit_domain_allocation_hell()

kernel/sched/topology.c

static enum s_alloc 
__visit_domain_allocation_hell(struct s_data *d, const struct cpumask *cpu_map)
{
        memset(d, 0, sizeof(*d));

        if (__sdt_alloc(cpu_map))
                return sa_sd_storage;
        d->sd = alloc_percpu(struct sched_domain *);
        if (!d->sd)
                return sa_sd_storage;
        d->rd = alloc_rootdomain();
        if (!d->rd)
                return sa_sd;

        return sa_rootdomain;
}

요청한 cpu_map 비트마스크를 사용하여 스케줄 도메인 토플로지의 자료 구조를 할당받고 초기화한다. 그리고 루트 도메인을 할당받은 후 초기화한다. 성공한 경우 sa_rootdomain(0)을 반환한다.

__sdt_alloc()

kernel/sched/topology.c

static int __sdt_alloc(const struct cpumask *cpu_map)
{
        struct sched_domain_topology_level *tl;
        int j;

        for_each_sd_topology(tl) {
                struct sd_data *sdd = &tl->data;

                sdd->sd = alloc_percpu(struct sched_domain *);
                if (!sdd->sd)
                        return -ENOMEM;

                sdd->sds = alloc_percpu(struct sched_domain_shared *);
                if (!sdd->sds)
                        return -ENOMEM;

                sdd->sg = alloc_percpu(struct sched_group *);
                if (!sdd->sg)
                        return -ENOMEM;

                sdd->sgc = alloc_percpu(struct sched_group_capacity *);
                if (!sdd->sgc)
                        return -ENOMEM;

                for_each_cpu(j, cpu_map) {
                        struct sched_domain *sd;
                        struct sched_domain_shared *sds;
                        struct sched_group *sg;
                        struct sched_group_capacity *sgc;

                        sd = kzalloc_node(sizeof(struct sched_domain) + cpumask_size(),
                                        GFP_KERNEL, cpu_to_node(j));
                        if (!sd)
                                return -ENOMEM;

                        *per_cpu_ptr(sdd->sd, j) = sd;

                        sds = kzalloc_node(sizeof(struct sched_domain_shared),
                                        GFP_KERNEL, cpu_to_node(j));
                        if (!sds)
                                return -ENOMEM;

                        *per_cpu_ptr(sdd->sds, j) = sds;

                        sg = kzalloc_node(sizeof(struct sched_group) + cpumask_size(),
                                        GFP_KERNEL, cpu_to_node(j));
                        if (!sg)
                                return -ENOMEM;

                        sg->next = sg;

                        *per_cpu_ptr(sdd->sg, j) = sg;

                        sgc = kzalloc_node(sizeof(struct sched_group_capacity) + cpumask_size(),
                                        GFP_KERNEL, cpu_to_node(j));
                        if (!sgc)
                                return -ENOMEM;

#ifdef CONFIG_SCHED_DEBUG
                        sgc->id = j;
#endif

                        *per_cpu_ptr(sdd->sgc, j) = sgc;
                }
        }

        return 0;
}

요청한 cpu_map 비트마스크로 스케줄 도메인 토플로지를 초기화한다.

  • 코드 라인 6에서 스케줄 도메인 단계만큼 순회한다.
    • arm의 경우 보통 1 단계 DIE 만을 구성하여 사용한다. 클러스터를 구성하여 사용하는 경우 2 단계로 구성한 DIE – MC(Multi core)로 구성한다. 또한 클러스터들 끼리 distance가 다른 경우 NUMA 구성을 추가하여 사용한다.
  • 코드 라인 9~23에서 스케줄 도메인의 sd_data에 sched_domain, sched_group, sched_group_capacity에 대한 포인터를 per-cpu로 할당하여 구성한다.
  • 코드 라인 25~65에서 cpu_map 비트마스크에 포함된 cpu들에 대해 sched_domain, sched_group, sched_group_capacity 구조체를 할당받은 후 sd_data에 연결한다.
  • 코드 라인 67에서 성공 값 0을 반환한다.

 

다음 그림은 cpu_map 비트마스크에 설정된 cpu들에 대해 관련 자료 구조 할당을 받아 스케줄 도메인 토플로지에 연결하는 모습을 보여준다.

 

for_each_sd_topology()

kernel/sched/core.c

#define for_each_sd_topology(tl)                        \
        for (tl = sched_domain_topology; tl->mask; tl++)

스케줄링 도메인 토플로지 레벨에 따라 순회한다.

  • tl->mask가 null인 경우 순회를 정지한다.

 

asym cpu capacity를 사용하는 토플로지 레벨

asym_cpu_capacity_level()

kernel/sched/topology.c

/*
 * Find the sched_domain_topology_level where all CPU capacities are visible
 * for all CPUs.
 */
static struct sched_domain_topology_level
*asym_cpu_capacity_level(const struct cpumask *cpu_map)
{
        int i, j, asym_level = 0;
        bool asym = false;
        struct sched_domain_topology_level *tl, *asym_tl = NULL;
        unsigned long cap;

        /* Is there any asymmetry? */
        cap = arch_scale_cpu_capacity(cpumask_first(cpu_map));

        for_each_cpu(i, cpu_map) {
                if (arch_scale_cpu_capacity(i) != cap) {
                        asym = true;
                        break;
                }
        }

        if (!asym)
                return NULL;

        /*
         * Examine topology from all CPU's point of views to detect the lowest
         * sched_domain_topology_level where a highest capacity CPU is visible
         * to everyone.
         */
        for_each_cpu(i, cpu_map) {
                unsigned long max_capacity = arch_scale_cpu_capacity(i);
                int tl_id = 0;

                for_each_sd_topology(tl) {
                        if (tl_id < asym_level)
                                goto next_level;

                        for_each_cpu_and(j, tl->mask(i), cpu_map) {
                                unsigned long capacity;

                                capacity = arch_scale_cpu_capacity(j);

                                if (capacity <= max_capacity)
                                        continue;

                                max_capacity = capacity;
                                asym_level = tl_id;
                                asym_tl = tl;
                        }
next_level:
                        tl_id++;
                }
        }

        return asym_tl;
}

asymetric cpu capacity를 사용하는 토플로지 레벨을 알아온다.

  • 코드 라인 10~20에서 @cpu_map의 첫 번째 cpu에 대한 cpu capacity를 알아온 후 이를 다른 cpu들의 cpu capacity와 비교하여 모두가 같지 않은 경우 asym=true를 설정한다. 모두가 동일한 경우엔 null을 반환한다.
  • 코드 라인 27~33에서 @cpu_map cpu들을 대상으로 순회하며 하위 루프에서 도메인 레벨을 순회한다. 그 중 asym 도메인 레벨 미만은 skip 한다.
  • 코드 라인 35~46에서 세 번째 루프인 cpu_map을 다시 순회하며 max_capacity를 초과하는 경우 갱신하게 하고, asym 도메인과 레벨도 갱신하게 한다.
  • 코드 라인 53에서 발견된 asym 도메인 레벨을 반환한다.
    • 예) 빅/리틀 클러스터로 구성된 시스템이 2 단계 MC 및 DIE 도메인을 구성한 경우 aysm 도메인 레벨은 DIE로 지정된다.

 

루트 도메인 할당 및 초기화

alloc_rootdomain()

kernel/sched/topology.c

static struct root_domain *alloc_rootdomain(void)
{
        struct root_domain *rd;

        rd = kmalloc(sizeof(*rd), GFP_KERNEL);
        if (!rd)
                return NULL;

        if (init_rootdomain(rd) != 0) {
                kfree(rd);
                return NULL;
        }

        return rd;
}

루트 도메인을 할당받고 초기화한다.

 

다음 그림은 루트도메인을 할당받고 초기화하는 모습을 보여준다.

 

claim_allocations()

kernel/sched/topology.c

/*
 * NULL the sd_data elements we've used to build the sched_domain and
 * sched_group structure so that the subsequent __free_domain_allocs()
 * will not free the data we're using.
 */
static void claim_allocations(int cpu, struct sched_domain *sd)
{
        struct sd_data *sdd = sd->private;

        WARN_ON_ONCE(*per_cpu_ptr(sdd->sd, cpu) != sd);
        *per_cpu_ptr(sdd->sd, cpu) = NULL;

        if (atomic_read(&(*per_cpu_ptr(sdd->sds, cpu))->ref))
                *per_cpu_ptr(sdd->sds, cpu) = NULL;

        if (atomic_read(&(*per_cpu_ptr(sdd->sg, cpu))->ref))
                *per_cpu_ptr(sdd->sg, cpu) = NULL;

        if (atomic_read(&(*per_cpu_ptr(sdd->sgc, cpu))->ref))
                *per_cpu_ptr(sdd->sgc, cpu) = NULL;
}

이미 빌드한 할당들에 대해서는 추후 삭제되지 않도록 임시 포인터 배열을 묶어둔 sdd들을 사용하여 null을 대입해둔다.

  • 코드 라인 6에서 @cpu에 대한 스케줄링 도메인을 사용하므로 null을 대입한다.
  • 코드 라인 8~9에서 @cpu에 대한 sched_domain_share가 사용 중인 경우 null을 대입한다.
  • 코드 라인 11~12에서 @cpu에 대한 스케줄링 그룹이 사용 중인 경우 null을 대입한다.
  • 코드 라인 14~15에서 @cpu에 대한 스케줄링 그룹 capacity가 사용 중인 경우 null을 대입한다.

 


스케줄 도메인 구성

 

kernel/sched/topology.c

/*
 * Package topology (also see the load-balance blurb in fair.c)
 *
 * The scheduler builds a tree structure to represent a number of important
 * topology features. By default (default_topology[]) these include:
 *
 *  - Simultaneous multithreading (SMT)
 *  - Multi-Core Cache (MC)
 *  - Package (DIE)
 *
 * Where the last one more or less denotes everything up to a NUMA node.
 *
 * The tree consists of 3 primary data structures:
 *
 *      sched_domain -> sched_group -> sched_group_capacity
 *          ^ ^             ^ ^
 *          `-'             `-'
 *
 * The sched_domains are per-CPU and have a two way link (parent & child) and
 * denote the ever growing mask of CPUs belonging to that level of topology.
 *
 * Each sched_domain has a circular (double) linked list of sched_group's, each
 * denoting the domains of the level below (or individual CPUs in case of the
 * first domain level). The sched_group linked by a sched_domain includes the
 * CPU of that sched_domain [*].
 *
 * Take for instance a 2 threaded, 2 core, 2 cache cluster part:
 *
 * CPU   0   1   2   3   4   5   6   7
 *
 * DIE  [                             ]
 * MC   [             ] [             ]
 * SMT  [     ] [     ] [     ] [     ]
 *
 *  - or -
 *
 * DIE  0-7 0-7 0-7 0-7 0-7 0-7 0-7 0-7
 * MC   0-3 0-3 0-3 0-3 4-7 4-7 4-7 4-7
 * SMT  0-1 0-1 2-3 2-3 4-5 4-5 6-7 6-7
 *
 * CPU   0   1   2   3   4   5   6   7
 *
 * One way to think about it is: sched_domain moves you up and down among these
 * topology levels, while sched_group moves you sideways through it, at child
 * domain granularity.
 *
 * sched_group_capacity ensures each unique sched_group has shared storage.
 *
 * There are two related construction problems, both require a CPU that
 * uniquely identify each group (for a given domain):
 *
 *  - The first is the balance_cpu (see should_we_balance() and the
 *    load-balance blub in fair.c); for each group we only want 1 CPU to
 *    continue balancing at a higher domain.
 *
 *  - The second is the sched_group_capacity; we want all identical groups
 *    to share a single sched_group_capacity.
 *
 * Since these topologies are exclusive by construction. That is, its
 * impossible for an SMT thread to belong to multiple cores, and cores to
 * be part of multiple caches. There is a very clear and unique location
 * for each CPU in the hierarchy.
 *
 * Therefore computing a unique CPU for each group is trivial (the iteration
 * mask is redundant and set all 1s; all CPUs in a group will end up at _that_
 * group), we can simply pick the first CPU in each group.
 *
 *
 * [*] in other words, the first group of each domain is its child domain.
 */

 

build_sched_domain()

kernel/sched/topology.c

static struct sched_domain *build_sched_domain(struct sched_domain_topology_level *tl,
                const struct cpumask *cpu_map, struct sched_domain_attr *attr,
                struct sched_domain *child, int dflags, int cpu)
{
        struct sched_domain *sd = sd_init(tl, cpu_map, child, dflags, cpu);

        if (child) {
                sd->level = child->level + 1;
                sched_domain_level_max = max(sched_domain_level_max, sd->level);
                child->parent = sd;

                if (!cpumask_subset(sched_domain_span(child),
                                    sched_domain_span(sd))) {
                        pr_err("BUG: arch topology borken\n");
#ifdef CONFIG_SCHED_DEBUG
                        pr_err("     the %s domain not a subset of the %s domain\n",
                                        child->name, sd->name);
#endif
                        /* Fixup, ensure @sd has at least @child CPUs. */
                        cpumask_or(sched_domain_span(sd),
                                   sched_domain_span(sd),
                                   sched_domain_span(child));
                }

        }
        set_domain_attribute(sd, attr);

        return sd;
}

요청한 토플로지 레벨에 대한 스케줄 도메인을 구성한다.

  • 코드 라인 5에서 요청한 스케줄 도메인 토플로지 레벨 tl에 대한 스케줄 도메인을 초기화한다.
  • 코드 라인 7~10에서 @child 스케줄 도메인 토플로지 레벨이 있는 경우 현재 스케줄 도메인의 레벨을 child 보다 1 큰 값으로하고 부모 관계를 설정한다.
    • 전역 변수 sched_domain_level_max에는 최대 스케줄 도메인 레벨 값을 갱신한다.
  • 코드 라인 12~23에서 자식 스케줄 도메인에 속한 cpu가 요청한 스케줄 도메인에 포함되지 않은 경우 경고 메시지를 출력하고 해당 스케줄 도메인에 포함시킨다.
  • 코드 라인 26에서 스케줄링 도메인의 레벨이 요청한 relax 도메인 레벨보다 큰 경우 wake 및 newidle 플래그를 클리어하고 그렇지 않은 경우 설정한다. 요청한 relax 도메인 레벨이 없는 경우 디폴트 relax 도메인 레벨 값을 사용하여 판단한다.
  • 코드 라인 28에서 스케줄링 도메인을 반환한다.

 

다음 그림은 각 cpu에서 스케줄 도메인간의 계층구조를 보여준다.

 

다음 그림은 각 레벨의 스케줄 도메인에 소속된 cpu의 span 값을 표현하였다.

 

다음 그림도 위의 그림과 동일하지만 스케줄 도메인을 cpu별로 span 값이 같은것들 끼리 뭉쳐 표현하였다.

sd_init()

kernel/sched/topology.c -1/2-

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            = 32,
                .imbalance_pct          = 125,

                .cache_nice_tries       = 0,

                .flags                  = 1*SD_LOAD_BALANCE
                                        | 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
        };

스케줄링 도메인을 초기화한다.

  • 코드 라인 19~23에서 요청한 토플로지의 sd_flags() 함수 후크를 수행하여 sd_flags 값을 알아온다. sd_flags 값으로 다음 플래그 값들 이외의 플래그가 설정되어 있는 경우 경고 메시지를 출력하고 제거한다.
    • 허용: SD_SHARE_CPUCAPACITY | SD_SHARE_PKG_RESOURCES | SD_NUMA | SD_ASYM_PACKING | SD_SHARE_POWERDOMAIN
  • 코드 라인 26에서 토플로지 플래그들을 스케줄링 도메인 플래그에 추가한다.
  • 코드 라인 28~57에서 스케줄링 도메인의 초기값을 설정한다.
    • 사용자가 지정한 스케줄링 도메인의 초기 플래그 값 sd_flags 이외에 다음 플래그 값은 커널이 기본 플래그로 추가한다.
    • 추가: SD_LOAD_BALANCE | SD_BALANCE_NEWIDLE | SD_BALANCE_EXEC | SD_BALANCE_FORK | SD_WAKE_AFFINE | SD_PREFER_SIBLING

 

kernel/sched/topology.c -2/2-

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

        /*
         * Convert topological properties into behaviour.
         */

        if (sd->flags & SD_ASYM_CPUCAPACITY) {
                struct sched_domain *t = sd;

                /*
                 * Don't attempt to spread across CPUs of different capacities.
                 */
                if (sd->child)
                        sd->child->flags &= ~SD_PREFER_SIBLING;

                for_each_lower_domain(t)
                        t->flags |= SD_BALANCE_WAKE;
        }

        if (sd->flags & SD_SHARE_CPUCAPACITY) {
                sd->imbalance_pct = 110;

        } else if (sd->flags & SD_SHARE_PKG_RESOURCES) {
                sd->imbalance_pct = 117;
                sd->cache_nice_tries = 1;

#ifdef CONFIG_NUMA
        } else if (sd->flags & SD_NUMA) {
                sd->cache_nice_tries = 2;

                sd->flags &= ~SD_PREFER_SIBLING;
                sd->flags |= SD_SERIALIZE;
                if (sched_domains_numa_distance[tl->numa_level] > node_reclaim_distance) {
                        sd->flags &= ~(SD_BALANCE_EXEC |
                                       SD_BALANCE_FORK |
                                       SD_WAKE_AFFINE);
                }

#endif
        } else {
                sd->cache_nice_tries = 1;
        }

        /*
         * For all levels sharing cache; connect a sched_domain_shared
         * instance.
         */
        if (sd->flags & SD_SHARE_PKG_RESOURCES) {
                sd->shared = *per_cpu_ptr(sdd->sds, sd_id);
                atomic_inc(&sd->shared->ref);
                atomic_set(&sd->shared->nr_busy_cpus, sd_weight);
        }

        sd->private = sdd;

        return sd;
}
  • 코드 라인 1~2에서 @cpu_map & tl->mask() 값을 sd->span 비트마스크에 대입하고, sd_id에는 sd->span 중 첫 번째 cpu를 알아온다.
  • 코드 라인 8~19에서 arm의 빅/리틀 클러스터처럼 asym cpu capacity를 사용하는 스케줄링 도메인의 경우 다음 두 가지를 실행한다.
    • 자식 스케줄링 도메인의 플래그에서 SD_PREFER_SIBLING 플래그를 제거한다.
    • 하위 스케줄링 도메인의 플래그에 SD_BALANCE_WAKE 플래그를 추가한다.
  • 코드 라인 21~22에서 hw thread를 지원하는 경우 스케줄링 도메인의 경우 imbalance_pct 값을 110으로 조정한다.
  • 코드 라인 24~26에서 패키지 리소스(l2 캐시 등)을 공유하는 스케줄링 도메인의 경우 imbalance_pct 값을 117로 조정하고, cache_nice_tries 값을 1로 사용한다.
  • 코드 라인 29~38에서 numa 도메인들의 경우 다음과 같이 적용한다.
    • cache_nice_tries 값을 2로 사용
    • SD_PREFER_SIBLING 플래그 제거
    • SD_SERIALIZE 추가
    • distance가 멀어 reclaim을 금지 시킨 도메인의 경우 SD_BALANCE_EXEC, SD_BALANCE_FORK, SD_WAKE_AFFINE 플래그를 제거한다.
  • 코드 라인 41~43에서 그 외의 스케줄링 도메인의 경우 cache_nice_tries 값을 1로 사용한다.
  • 코드 라인 49~53에서 패키지 리소스를 공유하는 도메인의 경우 shared에 shared 도메인의 cpu에 해당하는 sdd->sds 를 지정한다. shared 도메인의 참조 카운트를 1 증가시키고, nr_busy_cpus에 도메인 멤버 cpu 수를 지정한다.
  • 코드 라인 55에서 sd->private에 스케줄링 도메인 데이터 sdd를 지정한다.

 

다음 그림은 스케줄링 도메인을 초기화하는 모습을 보여준다.

 

다음 그림은 스케줄 도메인 토플로지 레벨별로 플래그의 변화를 보여준다.

 

다음 그림은 arm64에서 토플로지에 설정된 sd_flags 값을 읽어 sd_init() 함수 호출 시 스케줄링 도메인의 플래그들 초기값을 보여준다.

 

다음 그림은 32bit arm 역시 토플로지도 유사함을 보여준다.

  • rpi2의 경우 1개의 클러스터만 사용하고, 동등한 cpu capacity를 사용하므로 DIE 레벨만 사용하여도 충분하다.
  • ARM32에도 cortex-a15, cortex-a7 아키텍처를 사용하는 빅/리틀 클러스터를 구성하는 경우도 있다. 이 경우 각 클러스터의 cpu capacity가 다르며 CONFIG_SCHED_MC를 설정하여 DIE – MC – GMC의 3 단계를 구성하여 사용한다.

 

Relax domain 레벨

전역 변수 default_relax_domain_level은 “relax_domain_level=” 커널 파라메터로 변경된다. domain 속성에서 특별히 relax_domain_level 값을 지정하지 않는 경우 default_relax_domain_level을 사용한다.

  • relax_domain_level을 초과한 상위의 도메인에서 SD_BALANCE_WAKE, SD_BALANCE_NEWIDLE 플래그를 삭제하여 wakeup 밸런싱과 new idle 밸런싱을 하지 못하도록 한다. 반대로 그 이하의 도에인에는 해당 플래그를 추가한다.
  • 보통 원거리(long distance)의 NUMA 시스템에서 태스크가 위의 2 가지 밸런싱을 통해 멀리 migration 되지 않도록 억제한다.

 

set_domain_attribute()

kernel/sched/topology.c

static void set_domain_attribute(struct sched_domain *sd,
                                 struct sched_domain_attr *attr)
{
        int request;

        if (!attr || attr->relax_domain_level < 0) {
                if (default_relax_domain_level < 0)
                        return;
                else
                        request = default_relax_domain_level;
        } else
                request = attr->relax_domain_level;
        if (request < sd->level) {
                /* turn off idle balance on this domain */
                sd->flags &= ~(SD_BALANCE_WAKE|SD_BALANCE_NEWIDLE);
        } else {
                /* turn on idle balance on this domain */
                sd->flags |= (SD_BALANCE_WAKE|SD_BALANCE_NEWIDLE);
        }
}

스케줄링 도메인의 레벨이 요청한 relax 도메인 레벨보다 큰 경우 wake 및 newidle 플래그를 클리어하고 그렇지 않은 경우 설정한다. 요청한 relax 도메인 레벨이 없는 경우 디폴트 relax 도메인 레벨 값을 사용하여 판단한다.

  • 코드 라인 6~10에서 부트업 과정에서 이 함수에 진입 시에는 attr 값이 null로 진입한다. null로 진입하거나 속성에 부여된 레벨 값이 0보다 작은 경우 default_relax_domain_level 값이 설정되지 않았으면 함수를 빠져나간다. 만일 default_relax_domain_level 값이 이미 설정된 경우 그 값을 기준으로 삼기 위해 request에 대입한다.
  • 코드 라인 11~12에서 속성값이 주어진 경우 request에 대입한다.
  • 코드 라인 13~15에서 스케줄링 도메인의 레벨이 요청한 레벨보다 큰 경우 이 도메인에서 SD_BALANCE_WAKE와 SD_BALANCE_NEWIDLE 플래그를 제거한다.
  • 코드 라인 16~19에서 그 외의 경우 이 도메인에 SD_BALANCE_WAKE와 SD_BALANCE_NEWIDLE 플래그를 설정한다.

 

setup_relax_domain_level()

kernel/sched/topology.c

static int default_relax_domain_level = -1;
int sched_domain_level_max;

static int __init setup_relax_domain_level(char *str)
{
        if (kstrtoint(str, 0, &default_relax_domain_level))
                pr_warn("Unable to set relax_domain_level\n");

        return 1;
}
__setup("relax_domain_level=", setup_relax_domain_level);

“relax_domain_level=” 값을 파싱하여 전역 변수 default_relax_domain_level에 대입한다. (초기 값은 -1)

 


DIE 레벨 이하의 스케줄 그룹 구성

build_sched_groups()

kernel/sched/topology.c

/*
 * build_sched_groups will build a circular linked list of the groups
 * covered by the given span, will set each group's ->cpumask correctly,
 * and will initialize their ->sgc.
 *
 * Assumes the sched_domain tree is fully constructed
 */
static int
build_sched_groups(struct sched_domain *sd, int cpu)
{
        struct sched_group *first = NULL, *last = NULL;
        struct sd_data *sdd = sd->private;
        const struct cpumask *span = sched_domain_span(sd);
        struct cpumask *covered;
        int i;

        lockdep_assert_held(&sched_domains_mutex);
        covered = sched_domains_tmpmask;

        cpumask_clear(covered);

        for_each_cpu_wrap(i, span, cpu) {
                struct sched_group *sg;

                if (cpumask_test_cpu(i, covered))
                        continue;

                sg = get_group(i, sdd);

                cpumask_or(covered, covered, sched_group_span(sg));

                if (!first)
                        first = sg;
                if (last)
                        last->next = sg;
                last = sg;
        }
        last->next = first;
        sd->groups = first;

        return 0;
}

요청한 스케줄 도메인 레벨의 첫 번째 cpu 번호가 주어진 경우 해당 스케줄 그룹들을 구성한다.

  • 코드 라인 6에서 요청한 스케줄 도메인의 span cpu 비트마스크를 알아온다.
  • 코드 라인 11~13에서 covered 비트마스크를 모두 클리어한다.
  • 코드 라인 15~19에서 도메인에 속한 cpu를 cpu 번호부터 순회한다. 단 covered에 이미 설정된 cpu는 skip한다.
  • 코드 라인 17에서 순회 중인 i번 cpu에 연결된 스케줄링 그룹을 알아온다.
    • 요청한 cpu의 스케줄 도메인을 스케줄그룹과 연결한다.
  • 코드 라인 19에서 sg에 해당하는 cpu들을 covered 비트마스크에 추가한다.
  • 코드 라인 21~22에서 첫 sg인 경우 시작 sg로 선정한다.
  • 코드 라인 23~25에서 마지막 sg가 선정된 경우에 한하여 마지막 sg의 next에 현재 sg를 연결하여 list로 연결해간다.
  • 코드 라인 27에서 단방향 환형 리스트로 연결한다.
  • 코드 라인 28에서 스케줄 도메인이 첫 sg를 가리키게 한다.

 

다음 그림은 build_sched_groups() 함수가 각 도메인에 대해 처리되는 모습을 보여준다.

  • 예) 2 clusters * 2 cores = 4 cpus

 

다음 그림은 도메인과 스케줄 그룹간의 관계를 보여준다.

  • 예) 2 clusters * 2 cores * 2 hw-threads = 8 cpus

 

다음 그림은 NUMA 시스템에서의 스케줄링 도메인과 그룹이 생성된 모습을 보여준다.

  • NUMA 도메인에서는 per-cpu로 생성한 그룹을 사용하지 않고 overlap되는 cpu들을 표현하기 위해 별도로 필요한 만큼 그룹을 만들어 연결한다.

 

get_group()

kernel/sched/topology.c

static struct sched_group *get_group(int cpu, struct sd_data *sdd)
{
        struct sched_domain *sd = *per_cpu_ptr(sdd->sd, cpu);
        struct sched_domain *child = sd->child;
        struct sched_group *sg;
        bool already_visited;

        if (child)
                cpu = cpumask_first(sched_domain_span(child));

        sg = *per_cpu_ptr(sdd->sg, cpu);
        sg->sgc = *per_cpu_ptr(sdd->sgc, cpu);

        /* Increase refcounts for claim_allocations: */
        already_visited = atomic_inc_return(&sg->ref) > 1;
        /* sgc visits should follow a similar trend as sg */
        WARN_ON(already_visited != (atomic_inc_return(&sg->sgc->ref) > 1));

        /* If we have already visited that group, it's already initialized. */
        if (already_visited)
                return sg;

        if (child) {
                cpumask_copy(sched_group_span(sg), sched_domain_span(child));
                cpumask_copy(group_balance_mask(sg), sched_group_span(sg));
        } else {
                cpumask_set_cpu(cpu, sched_group_span(sg));
                cpumask_set_cpu(cpu, group_balance_mask(sg));
        }

        sg->sgc->capacity = SCHED_CAPACITY_SCALE * cpumask_weight(sched_group_span(sg));
        sg->sgc->min_capacity = SCHED_CAPACITY_SCALE;
        sg->sgc->max_capacity = SCHED_CAPACITY_SCALE;

        return sg;
}

요청한 @cpu에 해당하는 스케줄 그룹을 알아온다. 첫 참조시에는 child 도메인의 span을 사용하여 스케줄 그룹을 구성하고 단방향 환형 리스트로 연결한다.

  • 코드 라인 8~9에서 child 도메인이 있는 경우 child 도메인에 소속된 첫 번째 cpu를 알아온다.
  • 코드 라인 11~12에서 cpu에 해당하는 per-cpu용 스케줄링 그룹을 알아온다. 그 후 스케줄링 그룹의 sgc를 연결한다.
  • 코드 라인 15에서 스케줄링 그룹에 대한 참조 카운터를 증가시킨다.
  • 코드 라인 17에서 sgc에 대한 참조카운터를 증가시킨다.
  • 코드 라인 20~21에서 이미 참조된 경우 곧바로 스케줄링 그룹을 반환한다.
  • 코드 라인 23~29에서 첫 참조된 경우에는 스케줄링 그룹에 소속될 cpu들과 sgc에 소속될 cpu들을 구성한다. 최하위 인경우 해당 cpu에 대한 비트만을 사용하고, 그 외엔 child의 도메인의 span을 가져와서 동일하게 스케줄링 그룹 및 sgc를 구성한다.
  • 코드 라인 31~33에서 sgc에 대한 capacity 값은 1024 * cpu 수 만큼 값으로 초기화한다.
  • 코드 라인 32~33에서 sgc에 대한 min, max capacity 값은 1024 값으로 초기화한다.
  • 코드 라인 35에서 구성한 스케줄링 그룹을 반환한다.

 

다음 그림은 2단계의 스케줄 도메인 토플로지가 구성된 상태에서 하위 단계부터 상위 단계까지 get_group()을 모두 호출한 경우를 보여준다.

  • 예) 2 clusters * 2 cores = 4 cpus

 


NUMA 레벨에서 Overlap 스케줄 그룹 구성

 

/*
 * NUMA topology (first read the regular topology blurb below)
 *
 * Given a node-distance table, for example:
 *
 *   node   0   1   2   3
 *     0:  10  20  30  20
 *     1:  20  10  20  30
 *     2:  30  20  10  20
 *     3:  20  30  20  10
 *
 * which represents a 4 node ring topology like:
 *
 *   0 ----- 1
 *   |       |
 *   |       |
 *   |       |
 *   3 ----- 2
 *
 * We want to construct domains and groups to represent this. The way we go
 * about doing this is to build the domains on 'hops'. For each NUMA level we
 * construct the mask of all nodes reachable in @level hops.
 *
 * For the above NUMA topology that gives 3 levels:
 *
 * NUMA-2       0-3             0-3             0-3             0-3
 *  groups:     {0-1,3},{1-3}   {0-2},{0,2-3}   {1-3},{0-1,3}   {0,2-3},{0-2}
 *
 * NUMA-1       0-1,3           0-2             1-3             0,2-3
 *  groups:     {0},{1},{3}     {0},{1},{2}     {1},{2},{3}     {0},{2},{3}
 *
 * NUMA-0       0               1               2               3
 *
 *
 * As can be seen; things don't nicely line up as with the regular topology.
 * When we iterate a domain in child domain chunks some nodes can be
 * represented multiple times -- hence the "overlap" naming for this part of
 * the topology.
 *
 * In order to minimize this overlap, we only build enough groups to cover the
 * domain. For instance Node-0 NUMA-2 would only get groups: 0-1,3 and 1-3.
 *
 * Because:
 *
 *  - the first group of each domain is its child domain; this
 *    gets us the first 0-1,3
 *  - the only uncovered node is 2, who's child domain is 1-3.
 *
 * However, because of the overlap, computing a unique CPU for each group is
 * more complicated. Consider for instance the groups of NODE-1 NUMA-2, both
 * groups include the CPUs of Node-0, while those CPUs would not in fact ever
 * end up at those groups (they would end up in group: 0-1,3).
 *
 * To correct this we have to introduce the group balance mask. This mask
 * will contain those CPUs in the group that can reach this group given the
 * (child) domain tree.
 *
 * With this we can once again compute balance_cpu and sched_group_capacity
 * relations.
 *
 * XXX include words on how balance_cpu is unique and therefore can be
 * used for sched_group_capacity links.
 *
 *
 * Another 'interesting' topology is:
 *
 *   node   0   1   2   3
 *     0:  10  20  20  30
 *     1:  20  10  20  20
 *     2:  20  20  10  20
 *     3:  30  20  20  10
 *
 * Which looks a little like:
 *
 *   0 ----- 1
 *   |     / |
 *   |   /   |
 *   | /     |
 *   2 ----- 3
 *
 * This topology is asymmetric, nodes 1,2 are fully connected, but nodes 0,3
 * are not.
 *
 * This leads to a few particularly weird cases where the sched_domain's are
 * not of the same number for each CPU. Consider:
 *
 * NUMA-2       0-3                                             0-3
 *  groups:     {0-2},{1-3}                                     {1-3},{0-2}
 *
 * NUMA-1       0-2             0-3             0-3             1-3
 *
 * NUMA-0       0               1               2               3
 *
 */

 

build_overlap_sched_groups()

kernel/sched/topology.c

static int
build_overlap_sched_groups(struct sched_domain *sd, int cpu)
{
        struct sched_group *first = NULL, *last = NULL, *sg;
        const struct cpumask *span = sched_domain_span(sd);
        struct cpumask *covered = sched_domains_tmpmask;
        struct sd_data *sdd = sd->private;
        struct sched_domain *sibling;
        int i;

        cpumask_clear(covered);

        for_each_cpu_wrap(i, span, cpu) {
                struct cpumask *sg_span;

                if (cpumask_test_cpu(i, covered))
                        continue;

                sibling = *per_cpu_ptr(sdd->sd, i);

                /*
                 * Asymmetric node setups can result in situations where the
                 * domain tree is of unequal depth, make sure to skip domains
                 * that already cover the entire range.
                 *
                 * In that case build_sched_domains() will have terminated the
                 * iteration early and our sibling sd spans will be empty.
                 * Domains should always include the CPU they're built on, so
                 * check that.
                 */
                if (!cpumask_test_cpu(i, sched_domain_span(sibling)))
                        continue;

                sg = build_group_from_child_sched_domain(sibling, cpu);
                if (!sg)
                        goto fail;

                sg_span = sched_group_span(sg);
                cpumask_or(covered, covered, sg_span);

                init_overlap_sched_group(sd, sg);

                if (!first)
                        first = sg;
                if (last)
                        last->next = sg;
                last = sg;
                last->next = first;
        }
        sd->groups = first;

        return 0;

fail:
        free_sched_groups(first, 0);

        return -ENOMEM;
}

overlap 스케줄링 그룹을 구성한다. NUMA 시스템에서는 overlap되는 cpu들을 표현하기 위해 기존에 미리 만들어 두었던 그룹을 사용하지 않고 별도로 필요한 만큼 그룹을 만들어 사용한다.

  • 코드 라인 11에서 coverd 비트마스크를 클리어해둔다.
  • 코드 라인 13~17에서 @cpu 번호의 cpu부터 순회한다. 단 이미 covered에 설정된 cpu는 skip 한다.
  • 코드 라인 19에서 순회 cpu 번호에 해당하는 스케줄 도메인을 sibling에 알아온다.
  • 코드 라인 31~32에서 순회 cpu가 sibling에 속한 cpu가 아닌 경우 skip 한다.
  • 코드 라인 34~36에서 child 스케줄 도메인을 참조거나 없으면 현재 도메인을 참조하여 스케줄링 그룹을 구성한다.
  • 코드 라인 38~39에서 covered 비트마스크에 스케줄링 그룹의 cpu들을 모두 마크한다.
  • 코드 라인 41에서 overlap 스케줄링 그룹을 초기화한다.
  • 코드 라인 43~50에서 스케줄링 그룹 끼리 단방향 환형 리스트를 구성한다.
  • 코드 라인 52에서 성공 값 0을 반환한다.

 

참고: 커널 v5.15의 경우 위의 NUMA overlap 모델에서 스케줄 그룹을 생성시 build_group_from_child_sched_domain() 함수를 통해 하위 도메인 span 정보를 사용하는데, 하위 도메인의 span이 현재 도메인내에 포함될 수 없는 경우 더 하위 도메인을 선택하게 하도록 수정되었다.

 

build_group_from_child_sched_domain()

kernel/sched/topology.c

/*
 * XXX: This creates per-node group entries; since the load-balancer will
 * immediately access remote memory to construct this group's load-balance
 * statistics having the groups node local is of dubious benefit.
 */
static struct sched_group *
build_group_from_child_sched_domain(struct sched_domain *sd, int cpu)
{
        struct sched_group *sg;
        struct cpumask *sg_span;

        sg = kzalloc_node(sizeof(struct sched_group) + cpumask_size(),
                        GFP_KERNEL, cpu_to_node(cpu));

        if (!sg)
                return NULL;

        sg_span = sched_group_span(sg);
        if (sd->child)
                cpumask_copy(sg_span, sched_domain_span(sd->child));
        else
                cpumask_copy(sg_span, sched_domain_span(sd));

        atomic_inc(&sg->ref);
        return sg;
}

child 스케줄 도메인을 참조거나 없으면 현재 도메인을 참조하여 스케줄링 그룹을 구성한다.

  • 코드 라인 7~11에서 스케줄 그룹을 생성한다.
  • 코드 라인 13~17에서 child 스케줄 도메인의 cpu 수만큼 스케줄 그룹의 cpu를 동일하게 설정한다. child가 없으면 스케줄링 도메인과 동일한 cpu를 사용하게 설정한다.
  • 코드 라인 19~20에서 스케줄링 그룹의 참조 카운터를 1 증가시키고 반환한다.

 

init_overlap_sched_group()

kernel/sched/topology.c

static void init_overlap_sched_group(struct sched_domain *sd,
                                     struct sched_group *sg)
{
        struct cpumask *mask = sched_domains_tmpmask2;
        struct sd_data *sdd = sd->private;
        struct cpumask *sg_span;
        int cpu;

        build_balance_mask(sd, sg, mask);
        cpu = cpumask_first_and(sched_group_span(sg), mask);

        sg->sgc = *per_cpu_ptr(sdd->sgc, cpu);
        if (atomic_inc_return(&sg->sgc->ref) == 1)
                cpumask_copy(group_balance_mask(sg), mask);
        else
                WARN_ON_ONCE(!cpumask_equal(group_balance_mask(sg), mask));

        /*
         * Initialize sgc->capacity such that even if we mess up the
         * domains and no possible iteration will get us here, we won't
         * die on a /0 trap.
         */
        sg_span = sched_group_span(sg);
        sg->sgc->capacity = SCHED_CAPACITY_SCALE * cpumask_weight(sg_span);
        sg->sgc->min_capacity = SCHED_CAPACITY_SCALE;
        sg->sgc->max_capacity = SCHED_CAPACITY_SCALE;
}

overlap 스케줄링 그룹을 초기화한다.

  • 코드 라인 9~14에서 밸런스 마스크를 구성해온 후 첫 번째 cpu의 sgc에 대해 첫 참조인 경우 sgc->mask에 알아온 밸런스 마스크를 복사하여 사용한다.
  • 코드 라인 23~24에서 sgc에 대햔 capacity 값은 1024 * cpu 수 만큼 값으로 초기화한다.
  • 코드 라인 25~26에서 sgc에 대한 min, max capacity 값은 1024 값으로 초기화한다.

 

build_balance_mask()

kernel/sched/topology.c

/*
 * Build the balance mask; it contains only those CPUs that can arrive at this
 * group and should be considered to continue balancing.
 *
 * We do this during the group creation pass, therefore the group information
 * isn't complete yet, however since each group represents a (child) domain we
 * can fully construct this using the sched_domain bits (which are already
 * complete).
 */
static void
build_balance_mask(struct sched_domain *sd, struct sched_group *sg, struct cpumask *mask)
{
        const struct cpumask *sg_span = sched_group_span(sg);
        struct sd_data *sdd = sd->private;
        struct sched_domain *sibling;
        int i;

        cpumask_clear(mask);

        for_each_cpu(i, sg_span) {
                sibling = *per_cpu_ptr(sdd->sd, i);

                /*
                 * Can happen in the asymmetric case, where these siblings are
                 * unused. The mask will not be empty because those CPUs that
                 * do have the top domain _should_ span the domain.
                 */
                if (!sibling->child)
                        continue;

                /* If we would not end up here, we can't continue from here */
                if (!cpumask_equal(sg_span, sched_domain_span(sibling->child)))
                        continue;

                cpumask_set_cpu(i, mask);
        }

        /* We must not have empty masks here */
        WARN_ON_ONCE(cpumask_empty(mask));
}

밸런스 마스크를 구성한다. 스케줄링 그룹의 cpu들을 순회하며 하위 스케줄링 도메인의 cpu들과 동일한 cpu 비트를 설정해온다.

  • 코드 라인 9에서 @mask를 모두 클리어한다.
  • 코드 라인 11~20에서 스케줄링 그룹에 속한 cpu들을 대상으로 순회하되 스케줄링 도메인의 child가 없는 경우엔 skip 한다.
  • 코드 라인 23~26에서 하위 스케줄링 도메인에 속한 cpu들과 스케줄링 그룹의 cpu들이 동일한 경우에만 @mask에 현재 cpu를 설정해둔다.

 


스케줄 그룹 Capacity

init_sched_groups_capacity()

kernel/sched/topology.c

/*
 * Initialize sched groups cpu_capacity.
 *
 * cpu_capacity indicates the capacity of sched group, which is used while
 * distributing the load between different sched groups in a sched domain.
 * Typically cpu_capacity for all the groups in a sched domain will be same
 * unless there are asymmetries in the topology. If there are asymmetries,
 * group having more cpu_capacity will pickup more load compared to the
 * group having less cpu_capacity.
 */
static void init_sched_groups_capacity(int cpu, struct sched_domain *sd)
{
        struct sched_group *sg = sd->groups;

        WARN_ON(!sg);

        do {
                int cpu, max_cpu = -1;

                sg->group_weight = cpumask_weight(sched_group_span(sg));

                if (!(sd->flags & SD_ASYM_PACKING))
                        goto next;

                for_each_cpu(cpu, sched_group_span(sg)) {
                        if (max_cpu < 0)
                                max_cpu = cpu;
                        else if (sched_asym_prefer(cpu, max_cpu))
                                max_cpu = cpu;
                }
                sg->asym_prefer_cpu = max_cpu;

next:
                sg = sg->next;
        } while (sg != sd->groups);

        if (cpu != group_balance_cpu(sg))
                return;

        update_group_capacity(sd, cpu);
}

요청한 cpu의 스케줄링 도메인에 대한 스케줄링 그룹 capacity를 초기화한다.

  • 코드 라인 7~10에서 연결된 스케줄링 그룹들을 순회하며 스케줄링 그룹에 참여하는 cpu들 수를 sg->group_weight에 대입한다.
  • 코드 라인 12~13에서 스케줄링 도메인이 asymetric 패킹을 사용하지 않는 경우엔 next: 레이블로 이동한다.
    • powerpc의 SMT 토플로지 도메인의 경우 도메인 내 포함된 첫 번째 hw thread 가 더 성능이 좋다.
  • 코드 라인 15~21에서 스케줄링 그룹내의 cpu들을 순회하며 가장 성능 좋은 cpu를 찾아 스케줄링 그룹의 asym_prefer_cpu에 지정한다.
    • SMT 토플로지 중 asym 패킹을 사용하는 경우 powerpc 등에서 첫 번째 cpu가 가장 성능이 좋다.
    • ITMT(Intel Turbo Boost Max Technology 3.0) 기능을 지원하는 x86 시스템의 경우 특정 코어를 boost할 수 있고 SMT 및 MC 토플로지에서 사용할 수 있다.
  • 코드 라인 23~25에서 next: 레이블이다. 다음 스케줄링 그룹을 순회한다.
  • 코드 라인 27~30에서 밸런스 마스크의 첫 번째 cpu인 경우 스케줄링 그룹 capacity를 갱신한다.

 

 

sched_asym_prefer()

kernel/sched/sched.h

tatic inline bool sched_asym_prefer(int a, int b)
{
        return arch_asym_cpu_priority(a) > arch_asym_cpu_priority(b);
}

@a cpu가 @b cpu보다 더 성능이 좋은지 여부를 반환한다.

  • arch_asym_cpu_priority() 함수는 ITMT(Intel Turbo Boost Max Technology 3.0) 기능을 지원하는 x86 시스템을 위해 추가되었다.
  • 위의 기능을 지원하지 않는 아키텍처의 경우 weak 함수를 통해 -cpu 값을 반환한다.

 

group_balance_cpu()

kernel/sched/topology.c

/*
 * Return the canonical balance CPU for this group, this is the first CPU
 * of this group that's also in the balance mask.
 *
 * The balance mask are all those CPUs that could actually end up at this
 * group. See build_balance_mask().
 *
 * Also see should_we_balance().
 */
int group_balance_cpu(struct sched_group *sg)
{
        return cpumask_first(group_balance_mask(sg));
}

밸런스 마스크 중 첫 번째 cpu를 반환한다.

 

update_group_capacity()

kernel/sched/fair.c

void update_group_capacity(struct sched_domain *sd, int cpu)
{
        struct sched_domain *child = sd->child;
        struct sched_group *group, *sdg = sd->groups;
        unsigned long capacity, min_capacity, max_capacity;
        unsigned long interval;

        interval = msecs_to_jiffies(sd->balance_interval);
        interval = clamp(interval, 1UL, max_load_balance_interval);
        sdg->sgc->next_update = jiffies + interval;

        if (!child) {
                update_cpu_capacity(sd, cpu);
                return;
        }

        capacity = 0;
        min_capacity = ULONG_MAX;
        max_capacity = 0;

        if (child->flags & SD_OVERLAP) {
                /*
                 * SD_OVERLAP domains cannot assume that child groups
                 * span the current group.
                 */

                for_each_cpu(cpu, sched_group_span(sdg)) {
                        struct sched_group_capacity *sgc;
                        struct rq *rq = cpu_rq(cpu);

                        /*
                         * build_sched_domains() -> init_sched_groups_capacity()
                         * gets here before we've attached the domains to the
                         * runqueues.
                         *
                         * Use capacity_of(), which is set irrespective of domains
                         * in update_cpu_capacity().
                         *
                         * This avoids capacity from being 0 and
                         * causing divide-by-zero issues on boot.
                         */
                        if (unlikely(!rq->sd)) {
                                capacity += capacity_of(cpu);
                        } else {
                                sgc = rq->sd->groups->sgc;
                                capacity += sgc->capacity;
                        }

                        min_capacity = min(capacity, min_capacity);
                        max_capacity = max(capacity, max_capacity);
                }
        } else  {
                /*
                 * !SD_OVERLAP domains can assume that child groups
                 * span the current group.
                 */

                group = child->groups;
                do {
                        struct sched_group_capacity *sgc = group->sgc;

                        capacity += sgc->capacity;
                        min_capacity = min(sgc->min_capacity, min_capacity);
                        max_capacity = max(sgc->max_capacity, max_capacity);
                        group = group->next;
                } while (group != child->groups);
        }

        sdg->sgc->capacity = capacity;
        sdg->sgc->min_capacity = min_capacity;
        sdg->sgc->max_capacity = max_capacity;
}

요청한 cpu의 스케줄링 도메인에 해당하는 스케줄 그룹 capacity를 갱신한다.

  • 코드 라인 8~10에서 스케줄링 도메인의 로드 밸런싱 주기(ms)를 jiffies 단위로 변환하고 이 값이 1 ~ 0.1초에 해당하는 jiffies 범위로 제한시킨 후 다음 갱신 주기로 설정한다.
  • 코드 라인 12~15에서 child 없는 최하위 스케줄링 도메인의 경우 cpu capacity를 갱신하고 함수를 빠져나간다.
  • 코드 라인 21~51에서 child 스케줄링 도메인에 SD_OVERLAP 플래그가 설정된 경우(NUMA) 스케줄링 그룹의 cpu들을 대상으로 순회하며 capacity 값들을 더하면서 min,/max capacity를 갱신한다.
  • 코드 라인 52~67에서 !SD_OVERLAP 도메인의 경우 child 스케줄링 그룹을 순회하며 해당 cpu의 capacity를 더하면서 min/max capacity를 갱신한다.
  • 코드 라인 69~71에서 누적한 capacity 값을 스케줄링 그룹 capacity에 대입하고, min/max capacity도 대입한다.

 

update_cpu_capacity()

kernel/sched/fair.c

static void update_cpu_capacity(struct sched_domain *sd, int cpu)
{
        unsigned long capacity = scale_rt_capacity(sd, cpu);
        struct sched_group *sdg = sd->groups;

        cpu_rq(cpu)->cpu_capacity_orig = arch_scale_cpu_capacity(cpu);

        if (!capacity)
                capacity = 1;

        cpu_rq(cpu)->cpu_capacity = capacity;
        sdg->sgc->capacity = capacity;
        sdg->sgc->min_capacity = capacity;
        sdg->sgc->max_capacity = capacity;
}

cpu의 capacity를 갱신한다.

  • 코드 라인 3에서 요청한 @cpu의 cfs 태스크를 위한 capacity로 rt에 사용한 시간 비율 만큼을 줄인 스케일을 적용한다.
    • rt 타임: irq 처리에 사용한 시간 + rt 태스크 수행 시간 + deadline 태스크 수행시간
  • 코드 라인 6에서 런큐의 cpu_capacity_orig에 rt 스케일이 적용되지 않은 오리지날 cpu capacity 값을 대입한다.
  • 코드 라인 8~14에서 런큐의 cpu_capacity, 스케줄링 그룹의 capacity, min/max capacity에 rt 스케일된 capacity 값을 대입한다. 단 이 값이 0인 경우 최소 값 1을 사용한다.
    • 1로 나눌 때 방지하기 위한 값이다.

 

 

scale_rt_capacity()

kernel/sched/fair.c

static unsigned long scale_rt_capacity(struct sched_domain *sd, int cpu)
{
        struct rq *rq = cpu_rq(cpu);
        unsigned long max = arch_scale_cpu_capacity(cpu);
        unsigned long used, free;
        unsigned long irq;

        irq = cpu_util_irq(rq);

        if (unlikely(irq >= max))
                return 1;

        used = READ_ONCE(rq->avg_rt.util_avg);
        used += READ_ONCE(rq->avg_dl.util_avg);

        if (unlikely(used >= max))
                return 1;

        free = max - used;

        return scale_irq_capacity(free, irq, max);
}

rt를 제외한 여유(free) 유틸 로드 값을 반환한다.

  • 코드 라인 4에서 해당 cpu의 capacity 값을 알아와서 max에 대입한다.
    • 최대 값은 1024이다.
  • 코드 라인 8~11에서 irq에 대한 util 값을 알아온다. 만일 이 값이 max를 초과하는 경우 모든 util을 irq가 사용하였으므로 cfs 태스크에 줄 수 있는 capacity는 없다. 따라서 최소 값 1을 반환한다. (0으로 나누기 방지용)
  • 코드 라인 13~17에서 rt와 dl 태스크가 사용한 util 값을 알아와서 used에 대입한다. 이 값들도 max를 초과하는 경우 모든 util을 rt 및 dl 태스크가 사용하였으므로 cfs 태스크에 줄 수 있는 capacity는 없다. 따라서 이 경우도 최소 값 1을 반환한다. (0으로 나누기 방지용)
  • 코드 라인 19에서 오리지날 capacity 값에서 used(rt + dl)에서 사용한 util을 뺀 나머지 값을 산출한다.
  • 코드 라인 21에서 산출한 나머지 값을 전체 성능에서 irq util 비율을 제외한 값을 반환한다.

 

다음 그림은 rt+dl을 제외한 여유 유틸 로드 값을 알아내는 모습을 보여준다

  • scale이 적용된 rt 및 dl 유틸로드와 다르게 irq 유틸로드는 scale 되지 않은 시간이다. 따라서 rt와 dl 태스크 시간을 제외한 전체 cpu capacity에서 irq 타임을 제외하고, 스케일 조정 후 cfs에 사용된 cpu capacity를 알아낸다.
  • 이 방법은 커널 v4.19-rc1 부터 rt에 대해 PELT 트래킹이 적용되면서 변경되었다.
  • 참고:

 

scale_irq_capacity()

kernel/sched/sched.h

static inline
unsigned long scale_irq_capacity(unsigned long util, unsigned long irq, unsigned long max)
{
        util *= (max - irq);
        util /= max;

        return util;

}

다음과 같이 max capacity에 대해 irq 유틸로드가 사용한만큼 제외한 비율만큼을 util 값에 적용하여 반환한다.

  •                (@max – @irq)
  • @util * —————–
  •                     @max
  • 예) max 성능이 1024이고, 그 중 약 10%를 irq가 사용하였고, dl과 rt를 제외한 util에 약 70%인 경우
    • max=1024, irq=100, util=700
    • util 700에서 irq가 사용한 10%를 제외한 63% 값이 반환된다. (return 632)

 


cpu를 스케줄 도메인에 연결

cpu_attach_domain()

kernel/sched/topology.c

/*
 * Attach the domain 'sd' to 'cpu' as its base domain. Callers must
 * hold the hotplug lock.
 */
static void
cpu_attach_domain(struct sched_domain *sd, struct root_domain *rd, int cpu)
{
        struct rq *rq = cpu_rq(cpu);
        struct sched_domain *tmp;

        /* Remove the sched domains which do not contribute to scheduling. */
        for (tmp = sd; tmp; ) {
                struct sched_domain *parent = tmp->parent;
                if (!parent)
                        break;

                if (sd_parent_degenerate(tmp, parent)) {
                        tmp->parent = parent->parent;
                        if (parent->parent)
                                parent->parent->child = tmp;
                        /*
                         * Transfer SD_PREFER_SIBLING down in case of a
                         * degenerate parent; the spans match for this
                         * so the property transfers.
                         */
                        if (parent->flags & SD_PREFER_SIBLING)
                                tmp->flags |= SD_PREFER_SIBLING;
                        destroy_sched_domain(parent);
                } else
                        tmp = tmp->parent;
        }

        if (sd && sd_degenerate(sd)) {
                tmp = sd;
                sd = sd->parent;
                destroy_sched_domain(tmp);
                if (sd)
                        sd->child = NULL;
        }

        sched_domain_debug(sd, cpu);

        rq_attach_root(rq, rd);
        tmp = rq->sd;
        rcu_assign_pointer(rq->sd, sd);
        dirty_sched_domain_sysctl(cpu);
        destroy_sched_domains(tmp);

        update_top_cache_domain(cpu);
}

요청한 도메인 트리에서 스케줄링에 참여할 필요가 없는 스케줄링 도메인들은 해제하고 루트 도메인에 현재 cpu의 런큐를 연결한다.

  • 코드 라인 8~11에서 요청한 스케줄 도메인부터 최상위 스케줄 도메인까지 순회한다.
  • 코드 라인 13~24에서 부모 스케줄 도메인이 필요 없는 구성일 때 부모 스케줄 도메인을 skip 하도록 연결을 변경하고 제거한다. 부모 스케줄 도메인 플래그에 SD_PREFER_SIBLING이 있는 경우 자식에게 물려준다.
  • 코드 라인 25~27에서 부모 스케줄 도메인이 필요한 경우 계속 순회한다.
  • 코드 라인 29~35에서 마지막 최상위 부모 스케줄 도메인이 필요 없는 구성일 때 연결을 변경하고 제거한다.
  • 코드 라인 39에서 요청한 cpu의 런큐를 루트 도메인에 연결한다.
  • 코드 라인 40~41에서 기존 런큐에 연결되어 있었던 스케줄 도메인을 tmp에 대입하고 요청한 스케줄 도메인을 런큐에 연결한다.
  • 코드 라인 42에서 해당 cpu를 sysctl에 등록할 수 있도록 sd_sysctl_cpus 비트마스크에서 해당 cpu 비트를 설정한다.
  • 코드 라인 43에서 기존 런큐에 연결되어 있었던 스케줄 도메인부터 최상위까지 모두 rcu 방법으로 제거한다.
  • 코드 라인 45에서 최상위 캐시 도메인을 갱신한다.

 

부모 도메인 삭제 가능 여부 체크

sd_parent_degenerate()

kernel/sched/topology.c

static int
sd_parent_degenerate(struct sched_domain *sd, struct sched_domain *parent)
{
        unsigned long cflags = sd->flags, pflags = parent->flags;

        if (sd_degenerate(parent))
                return 1;

        if (!cpumask_equal(sched_domain_span(sd), sched_domain_span(parent)))
                return 0;

        /* Flags needing groups don't count if only 1 group in parent */
        if (parent->groups == parent->groups->next) {
                pflags &= ~(SD_LOAD_BALANCE |
                                SD_BALANCE_NEWIDLE |
                                SD_BALANCE_FORK |
                                SD_BALANCE_EXEC |
                                SD_SHARE_CPUCAPACITY |
                                SD_SHARE_PKG_RESOURCES |
                                SD_PREFER_SIBLING |
                                SD_SHARE_POWERDOMAIN);
                if (nr_node_ids == 1)
                        pflags &= ~SD_SERIALIZE;
        }
        if (~cflags & pflags)
                return 0;

        return 1;
}

요청한 스케줄 도메인 또는 부모 스케줄 도메인이 스케줄링에 참여할 필요가 없는 경우 true(1)를 반환한다. (true인 경우 바깥 루틴에서 이 스케줄링 도메인을 제거한다)

  • 코드 라인 4에서 자식 스케줄 도메인의 플래그와, 부모 스케줄 도메인의 플래그를 알아온다.
  • 코드 라인 6~7에서 부모 스케줄 도메인이 스케줄링에 참여할 필요가 없는 경우 true(1)를 반환한다.
  • 코드 라인 9~10에서 부모 스케줄 도메인과 자식 스케줄 도메인에 참여한 cpu 들이 같은 경우 false(0)를 반환한다.
  • 코드 라인 13~21에서 부모 스케줄 도메인에 그룹이 하나밖에 없는 경우 부모 플래그에서 밸런싱에 관련된 플래그를 모두 제거한다.
  • 코드 라인 22~23에서 그리고 노드가 1개인 경우 NUMA 로드밸런싱에서 사용하는 SD_SERIALIZE 플래그도 제거한다.
  • 코드 라인 25~26에서 자식 스케줄 도메인용 플래그에 없는 설정이 부모 플래그에 있는 경우 false(0)를 반환한다.
  • 코드 라인 28에서 true(1)를 반환한다.

 

도메인 삭제 가능 여부 체크

sd_degenerate()

kernel/sched/topology.c

static int sd_degenerate(struct sched_domain *sd)
{
        if (cpumask_weight(sched_domain_span(sd)) == 1)
                return 1;

        /* Following flags need at least 2 groups */
        if (sd->flags & (SD_LOAD_BALANCE |
                         SD_BALANCE_NEWIDLE |
                         SD_BALANCE_FORK |
                         SD_BALANCE_EXEC |
                         SD_SHARE_CPUCAPACITY |
                         SD_ASYM_CPUCAPACITY |
                         SD_SHARE_PKG_RESOURCES |
                         SD_SHARE_POWERDOMAIN)) {
                if (sd->groups != sd->groups->next)
                        return 0;
        }

        /* Following flags don't use groups */
        if (sd->flags & (SD_WAKE_AFFINE))
                return 0;

        return 1;
}

스케줄 도메인에 속한 cpu가 1개이거나 스케줄링에 참여를 할 수 없는 상태인 경우 true(1)를 반환한다. (true인 경우 바깥 루틴에서 이 스케줄링 도메인을 제거한다)

  • 코드 라인 3~4에서 요청한 스케줄링 도메인에 속한 cpu가 1개뿐이면 true(1)를 반환한다.
  • 코드 라인 7~17에서 스케줄 도메인의 플래그가 밸런싱을 요구하는데 다음 그룹이 있으면 false(0)을 반환한다.
  • 코드 라인 20~21에서 스케줄 도메인에서 SD_WAKE_AFFINE 플래그가 사용된 경우 false(0)을 반환한다.
  • 코드 라인 23에서 true(1)를 반환한다.

 

캐시 스케줄링 도메인 갱신

update_top_cache_domain()

kernel/sched/topology.c

static void update_top_cache_domain(int cpu)
{
        struct sched_domain_shared *sds = NULL;
        struct sched_domain *sd;
        int id = cpu;
        int size = 1;

        sd = highest_flag_domain(cpu, SD_SHARE_PKG_RESOURCES);
        if (sd) {
                id = cpumask_first(sched_domain_span(sd));
                size = cpumask_weight(sched_domain_span(sd));
                sds = sd->shared;
        }

        rcu_assign_pointer(per_cpu(sd_llc, cpu), sd);
        per_cpu(sd_llc_size, cpu) = size;
        per_cpu(sd_llc_id, cpu) = id;
        rcu_assign_pointer(per_cpu(sd_llc_shared, cpu), sds);

        sd = lowest_flag_domain(cpu, SD_NUMA);
        rcu_assign_pointer(per_cpu(sd_numa, cpu), sd);

        sd = highest_flag_domain(cpu, SD_ASYM_PACKING);
        rcu_assign_pointer(per_cpu(sd_asym_packing, cpu), sd);

        sd = lowest_flag_domain(cpu, SD_ASYM_CPUCAPACITY);
        rcu_assign_pointer(per_cpu(sd_asym_cpucapacity, cpu), sd);
}

캐시 스케줄링 도메인들을 갱신한다. (sd_busy, sd_llc, sd_numa, sd_asym)

  • 코드 라인 8~18에서 해당 cpu의 스케줄 도메인들 중 SD_SHARE_PKG_RESOURCES 플래그가 설정된 가장 상위의 스케줄링 도메인을 알아와서 llc(last level cache) 정보를 저장한다.
  • 코드 라인 20~21에서 해당 cpu의 스케줄 도메인들 중 SD_NUMA 플래그가 설정된 가장 하위의 스케줄 도메인을 알아와서 numa 도메인 정보를 저장한다.
  • 코드 라인 23~24에서 해당 cpu의 스케줄 도메인들 중 SD_ASYM_PACKING 플래그가 설정된 가장 상위의 스케줄 도메인을 알아와서 asym 도메인 정보를 저장한다.
  • 코드 라인 26~27에서 해당 cpu의 스케줄 도메인들 중 SD_ASYM_CPUCAPACITY 플래그가 설정된 가장 하위의 스케줄 도메인을 알아와서 asym cpu capacity 도메인 정보를 저장한다.

 

다음 그림은 llc(last level cache) 등의 스케줄링 도메인 정보를 갱신하는 모습을 보여준다.

 

highest_flag_domain()

kernel/sched/sched.h

/**
 * highest_flag_domain - Return highest sched_domain containing flag.
 * @cpu:        The cpu whose highest level of sched domain is to
 *              be returned.
 * @flag:       The flag to check for the highest sched_domain
 *              for the given cpu.
 *
 * Returns the highest sched_domain of a cpu which contains the given flag.
 */
static inline struct sched_domain *highest_flag_domain(int cpu, int flag)
{
        struct sched_domain *sd, *hsd = NULL;

        for_each_domain(cpu, sd) {
                if (!(sd->flags & flag))
                        break;
                hsd = sd;
        }

        return hsd;
}

요청한 플래그가 설정된 가장 상위의 스케줄 도메인을 반환한다.

 

lowest_flag_domain()

kernel/sched/sched.h

static inline struct sched_domain *lowest_flag_domain(int cpu, int flag)
{
        struct sched_domain *sd;

        for_each_domain(cpu, sd) {
                if (sd->flags & flag)
                        break;
        }

        return sd;
}

요청한 플래그가 설정된 가장 하위의 스케줄 도메인을 반환한다.

 


sysctl for sched_domain

다음은 rock960(4 little + 2 big) 보드가 2 단계 MC+DIE 스케줄 도메인 토플로지 레벨을 사용한 모습을 알아볼 수 있다.

$ cd /proc/sys/kernel/sched_domain
$ ls -la
total 0
dr-xr-xr-x 1 root root 0 Nov 27 21:39 .
dr-xr-xr-x 1 root root 0 Nov  4  2016 ..
dr-xr-xr-x 1 root root 0 Nov 27 21:39 cpu0
dr-xr-xr-x 1 root root 0 Nov 27 21:39 cpu1
dr-xr-xr-x 1 root root 0 Nov 27 21:39 cpu2
dr-xr-xr-x 1 root root 0 Nov 27 21:39 cpu3
dr-xr-xr-x 1 root root 0 Nov 27 21:39 cpu4
dr-xr-xr-x 1 root root 0 Nov 27 21:39 cpu5

$ cd cpu0
$ ls -la
total 0
dr-xr-xr-x 1 root root 0 Nov 27 21:39 .
dr-xr-xr-x 1 root root 0 Nov 27 21:39 ..
dr-xr-xr-x 1 root root 0 Nov 27 21:39 domain0
dr-xr-xr-x 1 root root 0 Nov 27 21:39 domain1

$ cd domain0
$ ls -la
total 0
dr-xr-xr-x 1 root root 0 Nov 27 21:39 .
dr-xr-xr-x 1 root root 0 Nov 27 21:39 ..
-rw-r--r-- 1 root root 0 Nov 27 21:39 busy_factor
-rw-r--r-- 1 root root 0 Nov 27 21:39 busy_idx
-rw-r--r-- 1 root root 0 Nov 27 21:39 cache_nice_tries
-rw-r--r-- 1 root root 0 Nov 27 21:39 flags
-rw-r--r-- 1 root root 0 Nov 27 21:39 forkexec_idx
dr-xr-xr-x 1 root root 0 Nov 27 21:39 group0
dr-xr-xr-x 1 root root 0 Nov 27 21:39 group1
dr-xr-xr-x 1 root root 0 Nov 27 21:39 group2
dr-xr-xr-x 1 root root 0 Nov 27 21:39 group3
-rw-r--r-- 1 root root 0 Nov 27 21:39 idle_idx
-rw-r--r-- 1 root root 0 Nov 27 21:39 imbalance_pct
-rw-r--r-- 1 root root 0 Nov 27 21:39 max_interval
-rw-r--r-- 1 root root 0 Nov 27 21:39 max_newidle_lb_cost
-rw-r--r-- 1 root root 0 Nov 27 21:39 min_interval
-r--r--r-- 1 root root 0 Nov 27 21:39 name
-rw-r--r-- 1 root root 0 Nov 27 21:39 newidle_idx
-rw-r--r-- 1 root root 0 Nov 27 21:39 wake_idx

$ cat name
MC

$ cat ../domain1/name
DIE

 

다음은 rpi4(4 big) 보드가 1 단계 DIE 스케줄 도메인 토플로지 레벨만을 사용한 모습을 알아볼 수 있다.

  • rpi2~4 보드와 같이 동일한 cpu capacity를 사용하는 경우 CONFIG_SCHED_MC 커널 옵션에 따라 다음과 같다.
    • 사용하지 않는 경우 DIE 단계만을 구성한다.
    • 사용한 경우 MC 단계만을 구성한다.
$ cd /proc/sys/kernel/sched_domain
$ ls -la
total 0
dr-xr-xr-x 1 root root 0 Nov 27 21:52 .
dr-xr-xr-x 1 root root 0 Jan 29  2018 ..
dr-xr-xr-x 1 root root 0 Nov 27 21:52 cpu0
dr-xr-xr-x 1 root root 0 Nov 27 21:52 cpu1
dr-xr-xr-x 1 root root 0 Nov 27 21:52 cpu2
dr-xr-xr-x 1 root root 0 Nov 27 21:52 cpu3

$ ls -la
total 0
dr-xr-xr-x 1 root root 0 Nov 27 21:52 .
dr-xr-xr-x 1 root root 0 Nov 27 21:52 ..
dr-xr-xr-x 1 root root 0 Nov 27 21:52 domain0

$ ls -la
total 0
dr-xr-xr-x 1 root root 0 Nov 27 21:52 .
dr-xr-xr-x 1 root root 0 Nov 27 21:52 ..
-rw-r--r-- 1 root root 0 Nov 27 21:52 busy_factor
-rw-r--r-- 1 root root 0 Nov 27 21:52 busy_idx
-rw-r--r-- 1 root root 0 Nov 27 21:52 cache_nice_tries
-rw-r--r-- 1 root root 0 Nov 27 21:52 flags
-rw-r--r-- 1 root root 0 Nov 27 21:52 forkexec_idx
-rw-r--r-- 1 root root 0 Nov 27 21:52 idle_idx
-rw-r--r-- 1 root root 0 Nov 27 21:52 imbalance_pct
-rw-r--r-- 1 root root 0 Nov 27 21:52 max_interval
-rw-r--r-- 1 root root 0 Nov 27 21:52 max_newidle_lb_cost
-rw-r--r-- 1 root root 0 Nov 27 21:52 min_interval
-r--r--r-- 1 root root 0 Nov 27 21:52 name
-rw-r--r-- 1 root root 0 Nov 27 21:52 newidle_idx
-rw-r--r-- 1 root root 0 Nov 27 21:52 wake_idx

$ cat name
DIE

 


구조체

sched_domain_topology_level 구조체

include/linux/sched/topology.h

struct sched_domain_topology_level {
        sched_domain_mask_f mask;
        sched_domain_flags_f sd_flags;
        int                 flags;
        int                 numa_level;
        struct sd_data      data;
#ifdef CONFIG_SCHED_DEBUG
        char                *name;
#endif
};
  • (*mask)
    • cpumask 값을 읽어오는 함수
  • (*sd_flags)
    • 스케줄 도메인 플래그를 읽어오는 함수
  • flags
    • 플래그
  • numa_level
    • 누마 레벨. NUMA가 아닌 경우 0
    • 로컬 노드도 0이며, 노드 간 distance 작은 노드부터 1씩 증가한다.
  • data
    • sd_data 구조체
  • *name
    • arm 도메인 단계명(“GMC” -> “MC” -> “DIE” -> “NODE” -> “NUMA”, …)
    • arm64 등 generic 도메인 단계명(“SMT” -> “MC” -> “DIE” -> “NODE” -> “NUMA”, …)

 

sched_domain 구조체

include/linux/sched.h

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

        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 next_decay_max_lb_cost;

        u64 avg_scan_cost;              /* select_idle_sibling */

#ifdef CONFIG_SCHEDSTATS
        /* load_balance() stats */
        unsigned int lb_count[CPU_MAX_IDLE_TYPES];
        unsigned int lb_failed[CPU_MAX_IDLE_TYPES];
        unsigned int lb_balanced[CPU_MAX_IDLE_TYPES];
        unsigned int lb_imbalance[CPU_MAX_IDLE_TYPES];
        unsigned int lb_gained[CPU_MAX_IDLE_TYPES];
        unsigned int lb_hot_gained[CPU_MAX_IDLE_TYPES];
        unsigned int lb_nobusyg[CPU_MAX_IDLE_TYPES];
        unsigned int lb_nobusyq[CPU_MAX_IDLE_TYPES];

        /* Active load balancing */
        unsigned int alb_count;
        unsigned int alb_failed;
        unsigned int alb_pushed;

        /* SD_BALANCE_EXEC stats */
        unsigned int sbe_count;
        unsigned int sbe_balanced;
        unsigned int sbe_pushed;

        /* SD_BALANCE_FORK stats */
        unsigned int sbf_count;
        unsigned int sbf_balanced;
        unsigned int sbf_pushed;

        /* try_to_wake_up() stats */
        unsigned int ttwu_wake_remote;
        unsigned int ttwu_move_affine;
        unsigned int ttwu_move_balance;
#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[0];
};
  • *parent
    • 부모 스케줄 도메인. 더 이상 부모가 없는 최상위는 null
  • *child
    • 자식 스케줄 도메인. 더 이상 자식이 없는 최하위는 null
  • *groups
    • 로드 밸런싱에 참여하는 스케줄 그룹들
  • min_interval
    • 최소 밸런싱 주기(ms)
  • max_interval
    • 최대 밸런싱 주기(ms)
  • busy_factor
    • 디폴트로 32를 사용한다.
  • imbalance_pct
    • 로컬과 기존 cpu의 로드 비교 시 둘 중 하나의 cpu 로드에 imbalance_pct 가중치를 부여한다. 밸런싱 목적에 따라 imbalance_pct 가중치가 붙는 cpu가 달라진다.
    • 디폴트로 125(%)를 사용하며 다음과 같은 도메인에 대해 밸런싱에 대한 benefit을 주기 위해 이 값을 조금 줄인다.
      • SD_SHARE_CPUCAPACITY 플래그를 사용하는 SMT 도메인에서 110(%)을 사용한다.
      • SD_SHARE_PKG_RESOURCES 플래그를 사용하여 패키지 내에서 캐시를 공유하는 MC 도메인에서 117(%)을 사용한다.
    • 커널 v5.10-rc1 부터 125%를 117%로 줄였다.
  • cache_nice_tries
    • NUMA 시스템에선 최대 값 2를 사용하고, 그 보다 밸런싱이 용이한 NODE, DIE, MC 레벨은 1 그리고 SMT 레벨은 최소 값 0을 사용한다.
  • nohz_idle
    • nohz idle 밸런싱에 사용한다. nohz idle인 경우 1이고 그 외의 경우 0이다.
  • flags
    • 도메인 특성을 나타내는 플래그들로 이 글의 앞 부분에서 별도로 설명하였다.
  • level
    • 최하위 도메인은 0부터 시작. (SMT -> MC -> DIE -> NODE -> NUMA, …)
  • last_balance
    • 밸런싱을 수행한 마지막 시각(jiffies)
  • balance_interval
    • 밸런싱 인터벌. 초기값 1(ms)
  • nr_balance_failed
    • 실패한 밸런싱 수
  • max_newidle_lb_cost
    • 도메인에 대해 idle 밸런싱을 시도 할 때마다 소요된 최대 idle 밸런싱 시간이 갱신된다.
    • 이 값은 1초에 1%씩 감소(decay) 한다.
    • 평균 idle 시간이 이 값보다 작은 경우 밸런싱을 시도하지 않게한다.
  • next_decay_max_lb_cost
    • 1초에 한 번씩 max_newidle_lb_cost를 decay를 하게 하기 위해 사용한다.
  • avg_scan_cost
  • *name
    • 도메인 명
  • *private
    • 생성 시 sd_data를 가리킨다.
  • *rcu
    • 해제 시 rcu 링크로 사용한다.
  • *shared
    • shared 정보를 가진 sched_domain_shared 구조체를 가리킨다.
  • span_weight
    • 도메인에 포함된 cpu 수
  • span[0]
    • 도메인에 포함된 cpu 비트마스크

 

참고

 

Scheduler -5- (Core)

<kernel v5.4>

스케줄러 코어

스케줄러 코어와 각각의 스케줄러들은 다음과 같이 구성되어 있다.

 

스케줄러 Operations

스케줄러 코어는 여러 개의 스케줄러들과 연동되어 사용하는데, 직접 각 스케줄러의 함수를 호출하지 않고, 다음 sched_class 구조체를 통해 현재 5가지의 스케줄러와 인터페이스하여 사용하고 있다.

 

각 스케줄러의 operation을 담당하는 후크 함수들은 다음과 같이 구성되어 있다.

 

sched_class 구조체

kernel/sched/sched.h

struct sched_class {
        const struct sched_class *next;

#ifdef CONFIG_UCLAMP_TASK
        int uclamp_enabled;
#endif

        void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
        void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
        void (*yield_task)   (struct rq *rq);
        bool (*yield_to_task)(struct rq *rq, struct task_struct *p, bool preempt);

        void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);

        /*
         * Both @prev and @rf are optional and may be NULL, in which case the
         * caller must already have invoked put_prev_task(rq, prev, rf).
         *
         * Otherwise it is the responsibility of the pick_next_task() to call
         * put_prev_task() on the @prev task or something equivalent, IFF it
         * returns a next task.
         *
         * In that case (@rf != NULL) it may return RETRY_TASK when it finds a
         * higher prio class has runnable tasks.
         */
        struct task_struct * (*pick_next_task)(struct rq *rq,
                                               struct task_struct *prev,
                                               struct rq_flags *rf);
        void (*put_prev_task)(struct rq *rq, struct task_struct *p);
        void (*set_next_task)(struct rq *rq, struct task_struct *p);

#ifdef CONFIG_SMP
        int (*balance)(struct rq *rq, struct task_struct *prev, struct rq_flags *rf);
        int  (*select_task_rq)(struct task_struct *p, int task_cpu, int sd_flag, int flags);
        void (*migrate_task_rq)(struct task_struct *p, int new_cpu);

        void (*task_woken)(struct rq *this_rq, struct task_struct *task);

        void (*set_cpus_allowed)(struct task_struct *p,
                                 const struct cpumask *newmask);

        void (*rq_online)(struct rq *rq);
        void (*rq_offline)(struct rq *rq);
#endif

        void (*task_tick)(struct rq *rq, struct task_struct *p, int queued);
        void (*task_fork)(struct task_struct *p);
        void (*task_dead)(struct task_struct *p);

        /*
         * The switched_from() call is allowed to drop rq->lock, therefore we
         * cannot assume the switched_from/switched_to pair is serliazed by
         * rq->lock. They are however serialized by p->pi_lock.
         */
        void (*switched_from)(struct rq *this_rq, struct task_struct *task);
        void (*switched_to)  (struct rq *this_rq, struct task_struct *task);
        void (*prio_changed) (struct rq *this_rq, struct task_struct *task,
                              int oldprio);

        unsigned int (*get_rr_interval)(struct rq *rq,
                                        struct task_struct *task);

        void (*update_curr)(struct rq *rq);

#define TASK_SET_GROUP          0
#define TASK_MOVE_GROUP         1

#ifdef CONFIG_FAIR_GROUP_SCHED
        void (*task_change_group)(struct task_struct *p, int type);
#endif
};
  • (*next)
    • 현재 스케줄러에서 더 이상 진행할 수 없을 때, 다음으로 진행할 스케줄링 클래스를 지정한다.
      • 각각의 스케줄러들은 다음 순서대로 지정된다.
        • stop -> deadline -> rt -> cfs -> stop
  • uclamp_enabled
    • uclamp min, max 사용 여부를 지정한다.
  •  (*enqueue_task)
    • 태스크가 실행 가능한(러너블) 상태가 되어 스케줄러가 관리하는 런큐에 엔큐할 때 호출된다.
  • (*dequeue_task)
    • 태스크가 실행 가능하지 않은 상태가 되어 스케줄러가 관리하는 런큐로부터 디큐할 때 호출된다.
  • (*yield_task)
    • yield() 함수를 실행하여 현재 태스크를 스케줄 아웃하고, 다음 태스크에 양보할 때 호출된다.
  • (*yield_to_task)
    • 이 스케줄러의 지정한 태스크로 양보할 때 호출된다.
  • (*check_preempt_curr)
    • 이 스케줄러의 현재 동작 중인 태스크를 선점할 수 있는지 체크하여 선점 필요 시 리스케줄링 요청 표식을 한다.
  • (*pick_next_task)
    • 해당 스케줄러의 내부 자료 구조로부터 실행할 다음 태스크를 선택할 때 호출된다.
  • (*put_prev_task)
    • 실행중인 태스크를 다시 해당 스케줄러의 내부 자료 구조에 넣을 때 호출된다.
  • (*set_next_task)
    • 태스크의 스케줄링 클래스나 태스크 그룹을 바꿀때 호출된다.
  • (*balance)
    • 로드 밸런싱 필요 여부를 확인하기 위해 호출된다.
    • 현재 스케줄러를 포함하여 상위 스케줄러에서 동작 중인 태스크가 있는 경우 1을 반환한다.
  • (*select_task_rq)
    • 태스크가 실행될 cpu의 런큐를 선택할 때 호출된다.
  • (*migrate_task_rq)
    • 태스크를 마이그레이션 할 때 호출된다.
  • (*task_woken)
    • 태스크가 깨어날 때 사용할 cpu의 런큐를 지정할 때 호출된다.
  • (*set_cpus_allowed)
    • 태스크에 사용될 cpu 마스크를 지정할 때 호출된다.
  • (*rq_online)
    • 런큐를 온라인 상태로 변경할 때 호출된다.
  • (*rq_offline)
    • 런큐를 오프라인 상태로 변경할 떄 호출된다.
  • (*task_tick)
    • 스케줄 틱이 발생될 때 호출된다.
      • hrtick이 사용될 때 queued=1로 호출된다.
      • 일반 고정 스케줄틱이 사용될 때 queued=0으로 호출된다.
  • (*task_fork)
    • fork한 태스크를 스케줄러가 관리하는 런큐에 엔큐할 떄 호출된다.
  • (*task_dead)
    • 지정한 태스크를 dead 처리하기 위해 런큐에서 디큐할 때 호출된다.
  • (*switched_from)
    • 스케줄러 스위칭 전에 기존 실행 중인 스케줄러에서 동작했었던 태스크를 detach할 때 호출된다.
  • (*switched_to)
    • 스케줄러 스위칭 후에 새로 실행 할 스케줄러에 태스크를 attach할 때 호출된다.
  • (*prio_changed)
    • 태스크의 우선 순위를 변경할 때 호출된다.
  • (*get_rr_interval)
    • 라운드 로빈 인터벌 타임 값을 알아올 때 호출된다.
  • (*update_curr)
    • 현재 실행 중인 태스크의 런타임을 갱신할 때 호출된다.
  • (*task_change_group)
    • 태스크의 태스크 그룹(cgroup)을 변경할 때 호출된다.
      • type이 0일 때 태스크 그룹을 설정한다.
      • type이 1일 때 태스크 그룹을 이동한다.

 


우선 순위

우선 순위는 다음과 같은 값들을 사용하며, 숫자의 크기와 우선 순위가 각각 다름에 주의해야 한다.

  • NICE
    • -20(highest) ~ 19(lowest)까지 cfs 태스크에서 사용한다.
  • PRIORITY (유저 우선 순위)
    • 0(lowest) ~ 139(highest)까지 rt 및 cfs 태스크에서 사용한다.
  • RT PRIORITY (유저 rt 우선 순위)
    • 1(lowest) ~ 99(highest)까지 rt 태스크에서 사용한다.
    • 다음에서 사용한다.
      • p->rt_priority
  • 커널 우선 순위
    • 0(highest) ~ 139(lowest)까지 rt 및 cfs 태스크에서 사용한다.
    • 다음에서 사용한다.
      • p->static_prio
      • p->normal_prio
      • p->prio

 

 

다음 ps 툴에서 보여주는 값을 확인한다.

  • PRI
    • = 139 – p->prio
  • RTPRIO
    • = p->rt_priority
    • = 99 – p->normal_prio
$ ps -e -o cmd,ni, pri,rtprio
CMD                         NI  PRI RTPRIO
/sbin/init                   0   19      -
[kthreadd]                   0   19      -
[ksoftirqd/0]                0   19      -
[kworker/0:0H]             -20   39      -
[rcu_sched]                  0   19      -
[rcu_bh]                     0   19      -
[migration/0]                -  139     99   <- p->prio = 0(highest)
[watchdog/0]                 -  139     99
[cfinteractive]              -  139     99
[rpciod]                   -20   39      -
[kvm_arch_timer]           -20   39      -
[kvm-irqfd-clean]          -20   39      -
[kswapd0]                    0   19      -
[vmstat]                   -20   39      -
[irq/230-rockchi]            -   90     50
[vcodec]                   -20   39      -
[bioset]                   -20  39      -
[nvme]                     -20  39      -
[spi32766]                   0  19      -
[fusb302_wq]               -20  39      -
[irq/26-mmc1]                -  90     50
[mmcqd/1]                    -  41      1
/lib/systemd/systemd-udevd   0  19      -
-bash                        0  19      -
./load0                    -20  39      -   
./load100                  +19   0      -   <- p->prio = 139(lowest)

 

NICE 우선 순위 변경

다음과 같이 3가지 구성에 대해 nice 우선 순위를 변경할 수 있다.

  • 태스크(process)
  • 태스크 그룹(process group)
  • 유저

 

renice

다음은 renice 유틸리티의 사용방법이다.

$ renice

Usage:
 renice [-n] <priority> [-p|--pid] <pid>...
 renice [-n] <priority>  -g|--pgrp <pgid>...
 renice [-n] <priority>  -u|--user <user>...

Alter the priority of running processes.

Options:
 -n, --priority <num>   specify the nice increment value
 -p, --pid <id>         interpret argument as process ID (default)
 -g, --pgrp <id>        interpret argument as process group ID
 -u, --user <name>|<id> interpret argument as username or user ID

 -h, --help     display this help and exit
 -V, --version  output version information and exit

For more details see renice(1).

 

다음 그림은 NICE 우선 순위를 변경할 때의 함수 호출 관계를 보여준다.

 

우선 순위 관리

다음과 같이 태스크에는 우선 순위 관련하여 4개의 멤버 변수로 관리한다. RT 태스크의 경우 러닝 중에 PI(Priority Inversion) Problem을 회피하기 위해 PI 태스크의 우선 순위만큼 부스트하여 우선 순위가 상승하였다가 다시 돌아온다. 이렇게 운영 중에 우선 순위가 변할 수 있으므로 상속되는 태스크에 부스트된 우선 순위를 상속하지 않게 하기 위해 p->normal_prio를 사용하여 관리한다. 일반 cfs 태스크로만 운영하는 경우 아래의 p->static_prio, p->prio 및 p->normal_prio는 동일한 값을 유지한다.

 p->static_prio
  • 유저가 nice 값을 사용하여 cfs 태스크에 static하게 지정한 커널 우선 순위이다.
    • cfs 태스크: 100~139
  • 지정 전의 태스크들은 부모 태스크로부터 상속된다.
    • 참고로 kthread_create_worker() API를 사용하여 만드는 태스크들(dl, rt, cfs)은 디폴트 값으로 nice 0를 사용하는 cfs 스케줄러를 사용하는 kthreadd(pid=2)를 통해 만들어진다. 따라서 nice 0에 대응하는 priority 120 값을 상속받는다.
p->prio
  • 현재 운영되는 커널 우선 순위로 rt 및 cfs 태스크를 위해 0~139까지 사용된다.
  • rt 태스크의 경우 PI boost로 인해 러닝 중에 커널 우선 순위가 상승할 수도 있다.
  • fork 된 태스크는 부모 태스크로부터 우선 순위를 상속받지만, RT 태스크의 경우엔 부스트된 부모 태스크의 우선 순위를 상속 받지 않기 위해 부모 태스크의 normal_prio 값을 상속받는다.
p->normal_prio
  • 커널 우선 순위이다.
    • deadline 태스크의 경우 -1
    • rt 태스크의 경우 0~99
    • cfs 태스크의 경우 100~139
  • 처음 부모 태스크로부터 상속된다.
  • 부스트된 우선 순위가 아닌 원래 설정된 priority로 되돌아올 때 사용할 우선 순위로 시스템이 운영 중 스스로 값을 바꾸지 않는다.
  • rt -> cfs 스케줄러로 변경되는 경우 cfs 태스크에서 사용하는 p->static_prio 값을 가져와 사용한다. (디폴트: 120)
  • cfs -> rt 스케줄러로 변경하는 경우 아래 p->rt_priority 값도 전달받는다. 이 값을 변환(99 – p->rt_priority)하여 사용한다.
p->rt_priority
  • 유저가 rt 태스크에 static하게 지정한 rt 유저 우선 순위이다.
  • rt 유저 우선 순위 1~99 값은 커널 우선 순위와 다르게 숫자가 높을 수록 우선 순위가 높아지도록 사용된다.
  • 유저가 chrt 유틸리티 등으로 지정한 유저 rt 우선 순위 값은 sched_setscheduler() syscall 이후 커널의 sys_sched_setscheduler() 함수를 통해 p->rt_priority에 그 값 그대로 설정되고, p->normal_prio에는 98 ~ 0 값으로 변환되어 사용된다.

 

CFS 태스크의 우선 순위 변경

유저가 nice 및 renice 유틸리티 등으로 지정한 nice 값을 setpriority() syscall 이후 커널의 set_user_nice() 함수를 통해 cfs 태스크의 중간 우선 순위 값인 120을 더해 p->static_prio에 저장한다.

  • -20 ~ 19 범위의 nice 우선 순위는 커널 우선 순위 100 ~ 139로 변경하여 다음 멤버 변수에 저장한다.
    • p->static_prio
    • p->normal_prio
    • p->prio

 

다음은 태스크 하나의 우선 순위를 변경하는 예를 보여준다.

$ ps
  PID TTY          TIME CMD
11636 pts/1    00:00:00 bash
11675 pts/1    00:40:25 load100
11740 pts/1    00:00:00 ps
$ renice -n -2 -p 11675
11675 (process ID) old priority 0, new priority -2

 

다음은 유저에 해당하는 모든 태스크의 우선 순위를 변경하는 예를 보여준다.

$ who
root     ttyFIQ0      2020-08-24 14:39
linaro   tty7         2020-08-24 14:39 (:0)
jake     pts/0        2020-09-14 10:31 (172.17.1.111)

$ renice -n -3 -u jake
1000 (user ID) old priority 0, new priority -3

 

RT 태스크의 우선 순위 변경

rt 태스크의 경우 cfs 태스크 처럼 nice나 renice 툴로 간단히 우선 순위를 바꾸는 경로를 사용하지 않고, 스케줄러를 지정하는 sched_setscheduler() API를 통해 우선 순위도 바꿀 수 있다.

유저가 chrt 유틸리티로 지정한 rt 유저 우선 순위 값(1~99)을 sched_setscheduler() syscall 이후 커널의 sched_setscheduler() 함수를 통해 다음의 멤버 변수에 설정한다.

  • p->rt_priority
    • 전달 받은 값 그대로 설정한다.
  • p->normal_prio
    • 전달 받은 값을 변환(99 – p->rt_priority)하여 설정한다.
  • p->prio
    • 전달 받은 값을 변환(99 – p->rt_priority)하여 설정한다.

 

다음은 rt 태스크의 우선 순위를 변경하는 방법을 보여준다.

$ chrt
Show or change the real-time scheduling attributes of a process.

Set policy:
 chrt [options] <priority> <command> [<arg>...]
 chrt [options] --pid <priority> <pid>

Get policy:
 chrt [options] -p <pid>

Policy options:
 -b, --batch          set policy to SCHED_BATCH
 -d, --deadline       set policy to SCHED_DEADLINE
 -f, --fifo           set policy to SCHED_FIFO
 -i, --idle           set policy to SCHED_IDLE
 -o, --other          set policy to SCHED_OTHER
 -r, --rr             set policy to SCHED_RR (default)

Scheduling options:
 -R, --reset-on-fork       set SCHED_RESET_ON_FORK for FIFO or RR
 -T, --sched-runtime <ns>  runtime parameter for DEADLINE
 -P, --sched-period <ns>   period parameter for DEADLINE
 -D, --sched-deadline <ns> deadline parameter for DEADLINE

Other options:
 -a, --all-tasks      operate on all the tasks (threads) for a given pid
 -m, --max            show min and max valid priorities
 -p, --pid            operate on existing given pid
 -v, --verbose        display status information

 -h, --help     display this help and exit
 -V, --version  output version information and exit

For more details see chrt(1).

$ ps -e -o pid,cmd,nice,pri,rtprio | grep watchdog
   10 [watchdog/0]                  - 139     99
   11 [watchdog/1]                  - 139     99
   16 [watchdog/2]                  - 139     99
   21 [watchdog/3]                  - 139     99
   26 [watchdog/4]                  - 139     99
   31 [watchdog/5]                  - 139     99
  184 [dhd_watchdog_th]             0  19      -
 1799 grep watchdog                 0  19      -

$ chrt -f --pid 98 11            <- 유저 rt 우선 순위를 99에서 98로 1단계 내림

$ ps -e -o pid,cmd,nice,pri,rtprio | grep watchdog
   10 [watchdog/0]                  - 139     99
   11 [watchdog/1]                  - 138     98
   16 [watchdog/2]                  - 139     99
   21 [watchdog/3]                  - 139     99
   26 [watchdog/4]                  - 139     99
   31 [watchdog/5]                  - 139     99
  184 [dhd_watchdog_th]             0  19      -
 1803 grep watchdog                 0  19      -

 

다음 그림은 policy 및 rt 유저 우선 순위를 지정하여 스케줄러 속성을 지정하는 과정을 보여준다. 이 과정에서 rt 태스크의 우선 순위를 변경할 수 있다.

 

RT 스레드 생성 및 우선 순위 지정

다음 코드 예는 SCHED_FIFO policy와 유저 rt 우선 순위 99(highest)로 RT 스레드를 생성하는 예를 보여준다.

static struct kthread_worker *foo_kworker;

int foo_thread_init(void)
{
        int err;
        struct sched_param param = { .sched_priority = MAX_RT_PRIO - 1,};

        foo_kworker = kthread_create_worker(0, "foo");
        if (IS_ERR(foo_kworker)) {
                pr_err("Failed to create foo kworker\n");
                return PTR_ERR(foo_kworker);
        }
        sched_setscheduler(foo_kworker->task, SCHED_FIFO, &param);
}

 


UCLAMP

특정 프로세스의 유틸 로드 평균을 uclamp min ~ max 범위내에서만 산출되도록 제한한다. 즉 실제보다 더 크게 하거나 작게 제한할 수 있다.

 


스케줄 틱

다음과 같이 커널에서 사용하는 두 개의 틱의 호출 과정을 알아본다.

  • 정규 스케줄 틱
  • HR 틱

 

정규 스케줄 틱

scheduler_tick()

kernel/sched/core.c

/*
 * This function gets called by the timer code, with HZ frequency.
 * We call it with interrupts disabled.
 */
void scheduler_tick(void)
{
        int cpu = smp_processor_id();
        struct rq *rq = cpu_rq(cpu);
        struct task_struct *curr = rq->curr;
        struct rq_flags rf;

        sched_clock_tick();

        rq_lock(rq, &rf);

        update_rq_clock(rq);
        curr->sched_class->task_tick(rq, curr, 0);
        calc_global_load_tick(rq);
        psi_task_tick(rq);

        rq_unlock(rq, &rf);

        perf_event_task_tick();

#ifdef CONFIG_SMP
        rq->idle_balance = idle_cpu(cpu);
        trigger_load_balance(rq);
#endif
}

스케줄 틱마다 런타임 처리 및 로드 평균 산출 등의 처리를 수행하고, 로드 밸런스 주기가 된 경우 SCHED softirq를 호출한다.

  • 코드 라인 3~5에서 현재 cpu의 런큐 및 현재 태스크를 알아온다.
  • 코드 라인 8에서 x86 및 ia64 아키텍처에 등에서 사용하는 unstable 클럭을 사용하는 중이면 per-cpu sched_clock_data 값을 현재 시각으로 갱신한다.
  • 코드 라인 10에서 런큐 정보를 수정하기 위해 런큐 락을 획득한다.
  • 코드 라인 12에서 런큐 클럭 값을 갱신한다.
    • rq->clock, rq->clock_task, rq->clock_pelt 클럭등을 갱신한다.
  • 코드 라인 13에서 현재 태스크의 스케줄링 클래스에 등록된 (*task_tick) 콜백 함수를 호출한다.
    • task_tick_fair(), task_tick_rt(), task_tick_dl()
  • 코드 라인 14에서 글로벌 로드를 갱신한다.
  • 코드 라인 15에서 psi 요청 태스크가 등록된 psi 그룹의 stat 을 매 틱마다 갱신한다.
  • 코드 라인 17에서 런큐 락을 해제한다.
  • 코드 라인 22에서 현재 cpu의 런큐가 idle 상태인지 확인하여 rq->idle_balance에 대입한다.
  • 코드 라인 23에서 매 틱마다 로드 밸런스를 할 타이밍인지 체크하여 수행하게 한다.

 

hrtick 호출

hrtick()

kernel/sched/core.c

/*
 * High-resolution timer tick.
 * Runs from hardirq context with interrupts disabled.
 */
static enum hrtimer_restart hrtick(struct hrtimer *timer)
{
        struct rq *rq = container_of(timer, struct rq, hrtick_timer);

        WARN_ON_ONCE(cpu_of(rq) != smp_processor_id());

        raw_spin_lock(&rq->lock);
        update_rq_clock(rq);
        rq->curr->sched_class->task_tick(rq, rq->curr, 1);
        raw_spin_unlock(&rq->lock);

        return HRTIMER_NORESTART;
}

런큐 클럭을 갱신하고 현재 태스크에 동작 중인 스케줄러의 (*task_tick) 후크 함수를 호출한다.

  • 각각의 스케줄러에서 사용되는 함수는 다음과 같다.
    • task_tick_rt()
    • task_tick_dl()
    • task_tick_fair()
    • task_tick_idle()
    • task_tick_stop()

 

hrtick 만료 시각 설정

hrtick_start()

kernel/sched/core.c

/*
 * Called to set the hrtick timer state.
 *              
 * called with rq->lock held and irqs disabled
 */
void hrtick_start(struct rq *rq, u64 delay)
{
        struct hrtimer *timer = &rq->hrtick_timer;
        ktime_t time;
        s64 delta;

        /*
         * Don't schedule slices shorter than 10000ns, that just
         * doesn't make sense and can cause timer DoS.
         */
        delta = max_t(s64, delay, 10000LL);
        time = ktime_add_ns(timer->base->get_time(), delta);

        hrtimer_set_expires(timer, time);

        if (rq == this_rq()) {
                __hrtick_restart(rq);
        } else if (!rq->hrtick_csd_pending) {
                smp_call_function_single_async(cpu_of(rq), &rq->hrtick_csd);
                rq->hrtick_csd_pending = 1;
        }
}

delta 만료 시간으로 런큐의 hrtick 타이머를 가동시킨다. 런큐가 현재 cpu인 경우 hrtimer를 곧바로 설정하고 그렇지 않은 경우 IPI call을 통해 hrtick에 대한 hrtimer를 설정한다.

  • 코드 라인 11~12에서 요청한 delta 값이 10000ns(10us) 보다 작지 않도록 교정하고 현재 시각에 더해 만료 시각 time을 산출한다.
  • 코드 라인 14에서 hrtimer에 만료 시각 time을 설정한다.
  • 코드 라인 16~17에서 요청 런큐가 현재 cpu에서 동작중인 경우 곧바로 hrtick에 대한 hrtimer를 가동시킨다.
    • 만료 시간이 되면 init_rq_hrtick()에서 초기화 설정해둔 hrtick() 함수가 호출된다.
  • 코드 라인 18~21에서 런큐에서 hrtick_csd_pending 상태가 아니면 hrtick에 대한 IPI 호출을 수행하고 런큐의 hrtick_csd_pending을 1로 설정한다.
    • init_rq_hrtick()에서 초기화 설정해둔 __hrtick_start() 함수가 IPI 호출 시 동작된다.

 

hrtick IPI 호출

__hrtick_start()

kernel/sched/core.c

/*
 * called from hardirq (IPI) context
 */
static void __hrtick_start(void *arg)
{
        struct rq *rq = arg;

        raw_spin_lock(&rq->lock);
        __hrtick_restart(rq);
        rq->hrtick_csd_pending = 0;
        raw_spin_unlock(&rq->lock);
}

다른 cpu로부터 IPI 호출을 받아 동작되는 이 함수에서는 런큐의 hrtick을 리프로그램한다.

  • 코드 라인 6에서 런큐의 hrtick을 리프로그램한다.
  • 코드 라인 7에서 런큐의 hrtick_csd_pending에 0을 대입하여 hrtick에 대한 call single data IPI 가 처리되었음을 알린다.

 


태스크 Fork

sched_fork()

kernel/sched/core.c

/*
 * fork()/clone()-time setup:
 */
int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
        unsigned long flags;

        __sched_fork(clone_flags, p);
        /*
         * We mark the process as NEW here. This guarantees that
         * nobody will actually run it, and a signal or other external
         * event cannot wake it up and insert it on the runqueue either.
         */
        p->state = TASK_NEW;

        /*
         * Make sure we do not leak PI boosting priority to the child.
         */
        p->prio = current->normal_prio;

        uclamp_fork(p);

        /*
         * Revert to default priority/policy on fork if requested.
         */
        if (unlikely(p->sched_reset_on_fork)) {
                if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
                        p->policy = SCHED_NORMAL;
                        p->static_prio = NICE_TO_PRIO(0);
                        p->rt_priority = 0;
                } else if (PRIO_TO_NICE(p->static_prio) < 0)
                        p->static_prio = NICE_TO_PRIO(0);

                p->prio = p->normal_prio = __normal_prio(p);
                set_load_weight(p, false);

                /*
                 * We don't need the reset flag anymore after the fork. It has
                 * fulfilled its duty:
                 */
                p->sched_reset_on_fork = 0;
        }

        if (dl_prio(p->prio))
                return -EAGAIN;
        else if (rt_prio(p->prio))
                p->sched_class = &rt_sched_class;
        else
                p->sched_class = &fair_sched_class;

        init_entity_runnable_average(&p->se);

        /*
         * The child is not yet in the pid-hash so no cgroup attach races,
         * and the cgroup is pinned to this child due to cgroup_fork()
         * is ran before sched_fork().
         *
         * Silence PROVE_RCU.
         */
        raw_spin_lock_irqsave(&p->pi_lock, flags);
        /*
         * We're setting the CPU for the first time, we don't migrate,
         * so use __set_task_cpu().
         */
        __set_task_cpu(p, smp_processor_id());
        if (p->sched_class->task_fork)
                p->sched_class->task_fork(p);
        raw_spin_unlock_irqrestore(&p->pi_lock, flags);

#ifdef CONFIG_SCHED_INFO
        if (likely(sched_info_on()))
                memset(&p->sched_info, 0, sizeof(p->sched_info));
#endif
#if defined(CONFIG_SMP)
        p->on_cpu = 0;
#endif
        init_task_preempt_count(p);
#ifdef CONFIG_SMP
        plist_node_init(&p->pushable_tasks, MAX_PRIO);
        RB_CLEAR_NODE(&p->pushable_dl_tasks);
#endif
        return 0;
}
  • 코드 라인 5에서 fork된 태스크의 cfs, dl 및 rt 스케줄링 엔티티의 멤버 값들과 numa 밸런싱 관련 값들을 초기화한다.
  • 코드 라인 11에서 처음 태스크가 fork될 때 TASK_NEW 상태로 시작한다.
  • 코드 라인 16에서 PI(Priority Inversion) 문제를 회피하기 위해 부모 태스크가 boosting 상태의 우선 순위로 변경되어 운영하고 있는 경우가 있으므로, 부스팅 되기 직전에 원래 사용하던 우선 순위를 사용해야 한다.
  • 코드 라인 18에서 fork된 태스크의 uclamp 설정이 동작되지 않게 한다. 만일 fork된 태스크의 스케줄러 설정을 reset 요청한 경우 uclamp 값을 min=0, max=100으로 초기화한다. 단 rt 태스크의 경우 min 값을 100으로 설정하여 항상 부스팅한다.
  • 코드 라인 23~39에서 fork된 태스크의 스케줄러 설정을 reset 요청한 경우에 대해 부모의 priority/policy를 사용하지 않고, 디폴트 priority/policy 값을 사용하게 한다.
  • 코드 라인 41~42에서 dl 태스크의 경우 -EAGAIN 값을 반환한다.
  • 코드 라인 43~46에서 태스크가 사용할 스케줄러(rt, cfs)를 지정한다.
    • 태스크에 설정된 prio 값에 따라 rt 또는 cfs 스케줄러를 지정한다.
  • 코드 라인 48에서 엔티티의 로드 및 러너블 로드 평균을 설정된 가중치(scale_load_down(load.weight))값으로 초기화한다.
  • 코드 라인 57~65에서 태스크에 대해 spin 락을 획득한 채로 태스크가 사용할 cpu로 현재 cpu를 지정하고, 스케줄러의 (*task_fork) 후크 함수를 호출하여 런큐에 엔큐하도록 한다.
  • 코드 라인 68~69에서 태스크의 sched_info를 초기화한다.
  • 코드 라인 72에서 태스크가 러닝(running) 상태에 있음을 표시하는 on_cpu를 0으로 초기화한다.
  • 코드 라인 74에서 preempt 카운터를 초기화한다.
  • 코드 라인 76에서 rt 태스크의 마이그레이션에 사용할 pushabnle_tasks를 초기화한다.
  • 코드 라인 77에서 deadline 태스크의 마이그레이션에 사용할 pushable_dl_tasks를 초기화한다.
  • 코드 라인 79에서 정상 값 0을 반환한다.

 

__sched_fork()

kernel/sched/core.c

/*
 * Perform scheduler related setup for a newly forked process p.
 * p is forked by current.
 *
 * __sched_fork() is basic setup used by init_idle() too:
 */
static void __sched_fork(unsigned long clone_flags, struct task_struct *p)
{
        p->on_rq                        = 0;

        p->se.on_rq                     = 0;
        p->se.exec_start                = 0;
        p->se.sum_exec_runtime          = 0;
        p->se.prev_sum_exec_runtime     = 0;
        p->se.nr_migrations             = 0;
        p->se.vruntime                  = 0;
        INIT_LIST_HEAD(&p->se.group_node);

#ifdef CONFIG_FAIR_GROUP_SCHED
        p->se.cfs_rq                    = NULL;
#endif

#ifdef CONFIG_SCHEDSTATS
        /* Even if schedstat is disabled, there should not be garbage */
        memset(&p->se.statistics, 0, sizeof(p->se.statistics));
#endif

        RB_CLEAR_NODE(&p->dl.rb_node);
        init_dl_task_timer(&p->dl);
        init_dl_inactive_task_timer(&p->dl);
        __dl_clear_params(p);

        INIT_LIST_HEAD(&p->rt.run_list);
        p->rt.timeout           = 0;
        p->rt.time_slice        = sched_rr_timeslice;
        p->rt.on_rq             = 0;
        p->rt.on_list           = 0;

#ifdef CONFIG_PREEMPT_NOTIFIERS
        INIT_HLIST_HEAD(&p->preempt_notifiers);
#endif

#ifdef CONFIG_COMPACTION
        p->capture_control = NULL;
#endif
        init_numa_balancing(clone_flags, p);
}

fork된 태스크의 cfs, dl 및 rt 스케줄링 엔티티의 멤버 값들과 numa 밸런싱 관련 값들을 초기화한다. 이 태스크는 현재 태스크에서 새롭게 fork되었으며 다음 두 곳 함수에서 호출되어 사용된다.

  • kernel/fork.c – copy_process() 함수 -> sched_fork() 함수
  • kernel/sched/core.c – init_idle() 함수

 

  • 코드 라인 3에서 on_rq를 0으로 초기화한다. 태스크가 아직 런큐에 엔큐되지 않았음을 의미한다.
  • 코드 라인 5에서 se.on_rq를 0으로 초기화한다. 엔티티가 아직 cfs 런큐에 엔큐되지 않았음을 의미한다.
  • 코드 라인 6~11에서 cfs 스케줄링 엔티티 값들을 초기화한다.
  • 코드 라인 14에서 그룹 스케줄링을 위해 엔티티가 소속된 cfs 런큐를 null로 초기화한다.
  • 코드 라인 19에서 엔티티 통계 값을 초기화한다.
  • 코드 라인 22에서 dl 스케줄링 엔티티의 rb_node를 클리어하여 dl 스케줄러의 RB 트리에 태스크가 하나도 대기하지 않음을 의미한다.
  • 코드 라인 23~24에서 dl 태스크 타이머와 inactive 태스크 타이머를 초기화한다.
  • 코드 라인 25에서 dl 엔티티용 파라미터들을 초기화한다.
  • 코드 라인 27에서 rt 엔티티용 run_list를 초기화한다.
  • 코드 라인 28~31에서 rt 엔티티용 파라미터들을 초기화한다.
  • 코드 라인 34에서 현재 태스크에서 preemption이 발생되면 preempt_notifiers에 등록된 함수들을 동작시키게 하기 위해 preempt_notifiers 리스트를 초기화한다.
  • 코드 라인 38에서 compaction에서 사용할 capture_control 구조체 포인터를 null로 초기화한다.
  • 코드 라인 40에서 numa 밸런싱에서 사용할 값들을 초기화한다.

 

init_dl_task_timer()

kernel/sched/deadline.c

void init_dl_task_timer(struct sched_dl_entity *dl_se)
{
        struct hrtimer *timer = &dl_se->dl_timer;

        hrtimer_init(timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_HARD);
        timer->function = dl_task_timer;
}

dl 엔티티의 태스크 timer를 초기화하고 만료 시 호출될 함수를 지정한다.

 

init_dl_inactive_task_timer()

kernel/sched/deadline.c

void init_dl_inactive_task_timer(struct sched_dl_entity *dl_se)
{
        struct hrtimer *timer = &dl_se->inactive_timer;

        hrtimer_init(timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_HARD);
        timer->function = inactive_task_timer;
}

dl 엔티티의 inactive 태스크 timer를 초기화하고 만료 시 호출될 함수를 지정한다.

 

__dl_clear_params()

kernel/sched/core.c

void __dl_clear_params(struct task_struct *p)
{
        struct sched_dl_entity *dl_se = &p->dl;

        dl_se->dl_runtime               = 0;
        dl_se->dl_deadline              = 0;
        dl_se->dl_period                = 0;
        dl_se->flags                    = 0;
        dl_se->dl_bw                    = 0;
        dl_se->dl_density               = 0;

        dl_se->dl_throttled             = 0;
        dl_se->dl_yielded               = 0;
        dl_se->dl_non_contending        = 0;
        dl_se->dl_overrun               = 0;
}

dl 엔티티용 파라메터들을 초기화한다.

 


태스크 깨우기

 

다음 그림은 wake_up_process() 함수 이후의 호출 관계를 보여준다.

 

wake_up_process()

kernel/sched/core.c

/**
 * wake_up_process - Wake up a specific process
 * @p: The process to be woken up.
 *
 * Attempt to wake up the nominated process and move it to the set of runnable
 * processes.
 *
 * Return: 1 if the process was woken up, 0 if it was already running.
 *
 * This function executes a full memory barrier before accessing the task state.
 */
int wake_up_process(struct task_struct *p)
{
        WARN_ON(task_is_stopped_or_traced(p));
        return try_to_wake_up(p, TASK_NORMAL, 0);
}
EXPORT_SYMBOL(wake_up_process);

요청한 태스크가 TASK_INTERRUPTIBLE 또는 TAKS_UNINTERRUPTIBLE 상태인 경우 깨운다. 깨운 경우 1을 반환하고, 이미 깨어나 동작 중인 경우 0을 반환한다.

  • #define TASK_NORMAL      (TASK_INTERRUPTIBLE | TAKS_UNINTERRUPTIBLE )

 

try_to_wake_up()

kernel/sched/core.c – 1/2

/**
 * try_to_wake_up - wake up a thread
 * @p: the thread to be awakened
 * @state: the mask of task states that can be woken
 * @wake_flags: wake modifier flags (WF_*)
 *
 * If (@state & @p->state) @p->state = TASK_RUNNING.
 *
 * If the task was not queued/runnable, also place it back on a runqueue.
 *
 * Atomic against schedule() which would dequeue a task, also see
 * set_current_state().
 *
 * This function executes a full memory barrier before accessing the task
 * state; see set_current_state().
 *
 * Return: %true if @p->state changes (an actual wakeup was done),
 *         %false otherwise.
 */
static int
try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
{
        unsigned long flags;
        int cpu, success = 0;

        preempt_disable();
        if (p == current) {
                /*
                 * We're waking current, this means 'p->on_rq' and 'task_cpu(p)
                 * == smp_processor_id()'. Together this means we can special
                 * case the whole 'p->on_rq && ttwu_remote()' case below
                 * without taking any locks.
                 *
                 * In particular:
                 *  - we rely on Program-Order guarantees for all the ordering,
                 *  - we're serialized against set_special_state() by virtue of
                 *    it disabling IRQs (this allows not taking ->pi_lock).
                 */
                if (!(p->state & state))
                        goto out;

                success = 1;
                cpu = task_cpu(p);
                trace_sched_waking(p);
                p->state = TASK_RUNNING;
                trace_sched_wakeup(p);
                goto out;
        }

        /*
         * If we are going to wake up a thread waiting for CONDITION we
         * need to ensure that CONDITION=1 done by the caller can not be
         * reordered with p->state check below. This pairs with mb() in
         * set_current_state() the waiting thread does.
         */
        raw_spin_lock_irqsave(&p->pi_lock, flags);
        smp_mb__after_spinlock();
        if (!(p->state & state))
                goto unlock;

        trace_sched_waking(p);

        /* We're going to change ->state: */
        success = 1;
        cpu = task_cpu(p);

        /*
         * Ensure we load p->on_rq _after_ p->state, otherwise it would
         * be possible to, falsely, observe p->on_rq == 0 and get stuck
         * in smp_cond_load_acquire() below.
         *
         * sched_ttwu_pending()                 try_to_wake_up()
         *   STORE p->on_rq = 1                   LOAD p->state
         *   UNLOCK rq->lock
         *
         * __schedule() (switch to task 'p')
         *   LOCK rq->lock                        smp_rmb();
         *   smp_mb__after_spinlock();
         *   UNLOCK rq->lock
         *
         * [task p]
         *   STORE p->state = UNINTERRUPTIBLE     LOAD p->on_rq
         *
         * Pairs with the LOCK+smp_mb__after_spinlock() on rq->lock in
         * __schedule().  See the comment for smp_mb__after_spinlock().
         */
        smp_rmb();
        if (p->on_rq && ttwu_remote(p, wake_flags))
                goto unlock;

태스크의 상태가 요청한 state 마스크에 포함된 경우 태스크를 깨운다. 깨운 경우 1을 반환하고, 이미 깨어나 동작 중인 경우 0을 반환한다.

  • 코드 라인 8~29에서 요청한 태스크가 현재 동작 중인 태스크인 경우 다음과 같이 처리한다.
    • 러닝 상태가 아닌 경우 이미 꺠어 있는 상태이므로 먼저 러닝 상태로 변경하고 함수를 빠져나가기 위해 success=1로 변경하고 out 레이블로 이동한다.
    • 이미 러닝 상태인 경우엔 꺠울 필요 없으므로 그냥 함수를 빠져나가기 위해 out 레이블로 이동한다.
  • 코드 라인 37~40에서 스핀락을 획득하고, 다시 상태를 확인해보아 이미 러닝 상태인 경우 깨울 필요 없으므로 unlock 레이블로 이동한다.
  • 코드 라인 45에서 잠들어 있는 태스크를 깨울 예정이므로 미리 success=1로 변경해둔다.
  • 코드 라인 46에서 슬립전에 동작하던 cpu를 알아온다.
  • 코드 라인 68~70에서 태스크가 이미 런큐에 있는 경우 기존 태스크가  슬립해 있었던 cpu의 런큐에서 태스크를 깨우고 러닝 상태로 바꾼다음 out 레이블로 이동한다.

 

kernel/sched/core.c – 2/2

#ifdef CONFIG_SMP
        /*
         * Ensure we load p->on_cpu _after_ p->on_rq, otherwise it would be
         * possible to, falsely, observe p->on_cpu == 0.
         *
         * One must be running (->on_cpu == 1) in order to remove oneself
         * from the runqueue.
         *
         * __schedule() (switch to task 'p')    try_to_wake_up()
         *   STORE p->on_cpu = 1                  LOAD p->on_rq
         *   UNLOCK rq->lock
         *
         * __schedule() (put 'p' to sleep)
         *   LOCK rq->lock                        smp_rmb();
         *   smp_mb__after_spinlock();
         *   STORE p->on_rq = 0                   LOAD p->on_cpu
         *
         * Pairs with the LOCK+smp_mb__after_spinlock() on rq->lock in
         * __schedule().  See the comment for smp_mb__after_spinlock().
         */
        smp_rmb();

        /*
         * If the owning (remote) CPU is still in the middle of schedule() with
         * this task as prev, wait until its done referencing the task.
         *
         * Pairs with the smp_store_release() in finish_task().
         *
         * This ensures that tasks getting woken will be fully ordered against
         * their previous state and preserve Program Order.
         */
        smp_cond_load_acquire(&p->on_cpu, !VAL);

        p->sched_contributes_to_load = !!task_contributes_to_load(p);
        p->state = TASK_WAKING;

        if (p->in_iowait) {
                delayacct_blkio_end(p);
                atomic_dec(&task_rq(p)->nr_iowait);
        }

        cpu = select_task_rq(p, p->wake_cpu, SD_BALANCE_WAKE, wake_flags);
        if (task_cpu(p) != cpu) {
                wake_flags |= WF_MIGRATED;
                psi_ttwu_dequeue(p);
                set_task_cpu(p, cpu);
        }

#else /* CONFIG_SMP */

        if (p->in_iowait) {
                delayacct_blkio_end(p);
                atomic_dec(&task_rq(p)->nr_iowait);
        }

#endif /* CONFIG_SMP */

        ttwu_queue(p, cpu, wake_flags);
unlock:
        raw_spin_unlock_irqrestore(&p->pi_lock, flags);
out:
        if (success)
                ttwu_stat(p, cpu, wake_flags);
        preempt_enable();

        return success;
}
  • 코드 라인 34에서 태스크가 로드에 기여를 하는지 여부를 알아온다.
    • frozen 태스크가 아니고 noload 없는 uniterruptible 상태의 태스크는 로드에 기여한다.
  • 코드 라인 35에서 태스크를 TASK_WAKING 상태로 바꾼다.
  • 코드 라인 37~40에서 태스크가 in_iowait 상태이면 io_wait 상태를 해제한다.
  • 코드 라인 42~47에서 태스크가 깨어날 cpu를 알아온다. 만일 태스크의 슬립전 cpu가 아닌 다른 cpu를 선택한 경우 WF_MIGRATED 플래그를 추가하고, psi 정보를 전달한 후 태스크에 선택한 cpu를 기록한다.
  • 코드 라인 58에서 태스크를 알아온 cpu의 런큐에서 깨운다.
  • 코드 라인 59~60에서 unlock 레이블이다. 스핀락을 복구한다.
  • 코드 라인 61~63에서 out 레이블이다. wakeup이 성공한 경우 관련 stat을 갱신한다.
  • 코드 라인 64~66에서 preemption을 다시 enable하고 success 상태를 반환한다.

 

런큐에 있는 태스크 깨우기

ttwu_remote()

kernel/sched/core.c

/*
 * Called in case the task @p isn't fully descheduled from its runqueue,
 * in this case we must do a remote wakeup. Its a 'light' wakeup though,
 * since all we need to do is flip p->state to TASK_RUNNING, since
 * the task is still ->on_rq.
 */
static int ttwu_remote(struct task_struct *p, int wake_flags)
{
        struct rq *rq;
        int ret = 0;

        rq = __task_rq_lock(p);
        if (task_on_rq_queued(p)) {
                /* check_preempt_curr() may use rq clock */
                update_rq_clock(rq);
                ttwu_do_wakeup(rq, p, wake_flags);
                ret = 1;
        }
        __task_rq_unlock(rq);

        return ret;
}

태스크가 이미 런큐에 있는 경우 태스크를 TASK_RUNNING 상태로 바꾸고 preemption 가능한지 체크한다. 런큐에 태스크가 있었으면 1을 반환한다.

 

ttwu_do_wakeup()

kernel/sched/core.c

/*
 * Mark the task runnable and perform wakeup-preemption.
 */
static void ttwu_do_wakeup(struct rq *rq, struct task_struct *p, int wake_flags,
                           struct rq_flags *rf)
{
        check_preempt_curr(rq, p, wake_flags);
        p->state = TASK_RUNNING;
        trace_sched_wakeup(p);

#ifdef CONFIG_SMP
        if (p->sched_class->task_woken) {
                /*
                 * Our task @p is fully woken up and running; so its safe to
                 * drop the rq->lock, hereafter rq is only used for statistics.
                 */
                rq_unpin_lock(rq, rf);
                p->sched_class->task_woken(rq, p);
                rq_repin_lock(rq, rf);
        }

        if (rq->idle_stamp) {
                u64 delta = rq_clock(rq) - rq->idle_stamp;
                u64 max = 2*rq->max_idle_balance_cost;

                update_avg(&rq->avg_idle, delta);

                if (rq->avg_idle > max)
                        rq->avg_idle = max;

                rq->idle_stamp = 0;
        }
#endif
}

태스크를 러너블(TASK_RUNNING) 상태로 변경하고 wake-up preemption을 수행한다.

  • 코드 라인 4에서 preemption이 필요하면 리스케줄 요청을 하도록 체크한다.
  • 코드 라인 5에서 태스크를 TASK_RUNNING 상태로 바꾼다.
  • 코드 라인 9~17에서 해당 태스크의 스케줄러에 해당하는 task_woken에 연결된 함수를 호출한다.
    • dl 및 rt 스케줄러만 관련 함수를 제공한다.
      • task_woken_dl()
        • 다른 dl 태스크가 동작 중이고 요청한 dl 태스크가 동작 중이 아닌 경우 밸런싱을 위해 필요 시 push 한다.
      • task_woken_rt()
        • 다른 rt 태스크가 동작 중이고 요청한 rt 태스크가 동작 중이 아닌 경우 밸런싱을 위해 필요 시 push 한다.
  • 코드 라인 19~29에서 idle 동안의 시간과 기존 avg_idle의 차이 기간의 1/8만 avg_idle에 추가한다. 단 max_idle_balance_cost의 2배를 초과하지 못하게 제한한다.

 

update_avg()

kernel/sched/core.c

static void update_avg(u64 *avg, u64 sample)
{
        s64 diff = sample - *avg;
        *avg += diff >> 3;
}

avg += (sample – avg) / 8 를 산출한다.

 

런큐에 없는 태스크를 깨우기

ttwu_queue()

kernel/sched/core.c

static void ttwu_queue(struct task_struct *p, int cpu, int wake_flags)
{
        struct rq *rq = cpu_rq(cpu);
        struct rq_flags rf;

#if defined(CONFIG_SMP)
        if (sched_feat(TTWU_QUEUE) && !cpus_share_cache(smp_processor_id(), cpu)) {
                sched_clock_cpu(cpu); /* Sync clocks across CPUs */
                ttwu_queue_remote(p, cpu, wake_flags);
                return;
        }
#endif

        rq_lock(rq, &rf);
        update_rq_clock(rq);
        ttwu_do_activate(rq, p, wake_flags, &rf);
        rq_unlock(rq, &rf);
}

태스크를 요청한 cpu의 런큐에서 깨운다.

  • 코드 라인 7~11에서 TTWU_QUEUE(디폴트=true) feature가 설정되었고 요청한 cpu와 현재 cpu가 같은 캐시를 공유하지 않으면 요청한 cpu의 런큐의 wake_list에 태스크를 추가하고 IPI를 통해 해당 cpu에 wakeup 요청을 한다.
  • 코드 라인 14~17에서 현재 cpu에서 직접 해당 cpu의 런큐 락을 획득한 후 태스크를 런큐에 엔큐한다. 그런 후 태스크를 러닝 상태로 변경한 후 wake-up preemption을 수행한다.

 

ttwu_queue_remote()

kernel/sched/core.c

static void ttwu_queue_remote(struct task_struct *p, int cpu, int wake_flags)
{
        struct rq *rq = cpu_rq(cpu);

        p->sched_remote_wakeup = !!(wake_flags & WF_MIGRATED);

        if (llist_add(&p->wake_entry, &cpu_rq(cpu)->wake_list)) {
                if (!set_nr_if_polling(rq->idle))
                        smp_send_reschedule(cpu);
                else
                        trace_sched_wake_idle_without_ipi(cpu);
        }
}

태스크를 리모트 cpu에서 깨운다.

  • 코드 라인 5에서 태스크에 remote wakeup 여부를 기록한다.
    • 태스크가 원래 슬립했었던 cpu가 아닌 다른 cpu에서 깨워졌는지 여부가 기록된다.
  • 코드 라인 7~12에서 태스크를 wake_list에 추가하고 IPI를 통해 해당 cpu에 리스케줄 요청한다.

 

ttwu_do_activate()

kernel/sched/core.c

static void
ttwu_do_activate(struct rq *rq, struct task_struct *p, int wake_flags,
                 struct rq_flags *rf)
{
        int en_flags = ENQUEUE_WAKEUP | ENQUEUE_NOCLOCK;

        lockdep_assert_held(&rq->lock);

#ifdef CONFIG_SMP
        if (p->sched_contributes_to_load)
                rq->nr_uninterruptible--;

        if (wake_flags & WF_MIGRATED)
                en_flags |= ENQUEUE_MIGRATED;
#endif

        activate_task(rq, p, en_flags);
        ttwu_do_wakeup(rq, p, wake_flags, rf);
}

태스크를 런큐에 엔큐하고 러너블 상태로 변경한 후 wake-up preemption을 수행한다.

  • 코드 라인 4에서 런큐 엔큐시 사용할 플래그를 지정한다.
  • 코드 라인 6에서 런큐 락을 획득한다.
  • 코드 라인 9~10에서 로드에 참여하는 uninterruptible 태스크인 경우 nr_uninterruptible을 1 감소시킨다.
  • 코드 라인 12~13에서 슬립 했었던 cpu가 아니라 다른 cpu에서 깨워지는 경우 ENQUEUE_MIGRATED 플래그를 추가한다.
  • 코드 라인 16에서 태스크를 런큐에 엔큐하여 동작시킨다.
  • 코드 라인 17에서 태스크를 러너블(TASK_RUNNING) 상태로 변경하고 wake-up preemption을 수행한다.

 


다음 스케줄할 태스크 선택

pick_next_task()

kernel/sched/core.c

/*
 * Pick up the highest-prio task:
 */
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
        const struct sched_class *class;
        struct task_struct *p;

        /*
         * Optimization: we know that if all tasks are in the fair class we can
         * call that function directly, but only if the @prev task wasn't of a
         * higher scheduling class, because otherwise those loose the
         * opportunity to pull in more work from other CPUs.
         */
        if (likely((prev->sched_class == &idle_sched_class ||
                    prev->sched_class == &fair_sched_class) &&
                   rq->nr_running == rq->cfs.h_nr_running)) {

                p = fair_sched_class.pick_next_task(rq, prev, rf);
                if (unlikely(p == RETRY_TASK))
                        goto restart;

                /* Assumes fair_sched_class->next == idle_sched_class */
                if (unlikely(!p))
                        p = idle_sched_class.pick_next_task(rq, prev, rf);

                return p;
        }

restart:
#ifdef CONFIG_SMP
        /*
         * We must do the balancing pass before put_next_task(), such
         * that when we release the rq->lock the task is in the same
         * state as before we took rq->lock.
         *
         * We can terminate the balance pass as soon as we know there is
         * a runnable task of @class priority or higher.
         */
        for_class_range(class, prev->sched_class, &idle_sched_class) {
                if (class->balance(rq, prev, rf))
                        break;
        }
#endif

        put_prev_task(rq, prev);

        for_each_class(class) {
                p = class->pick_next_task(rq, NULL, NULL);
                if (p)
                        return p;
        }

        /* The idle class should always have a runnable task: */
        BUG();
}

다음 스케줄할 최우선 순위의 태스크를 알아온다. (stop -> dl -> rt -> fair -> idle 스케줄러 순)

  • 코드 라인 13~26에서 높은 확률로 idle 상태였거나 cfs 태스크만 동작하는 경우 cfs 스케줄러의 pick_next_task() 함수를 호출하여 다음 처리할 태스크를 반환한다.처리할 태스크가 없는 경우 idle 태스크를 반환한다. 만일 낮은 확률로 RETRY_TASK 결과를 가져온 경우 restart 레이블로 이동한다.
  • 코드 라인 27~40에서 stop 스케줄러부터 idle 스케줄 클래스까지 순서대로 돌며 (*balance) 루틴을 수행하여 true인 경우 다음 클래스는 무시하고 루프를 빠져나온다.
  • 코드 라인 52에서 기존 태스크를 런큐에 다시 엔큐한다.
  • 코드 라인 54~58에서 stop 스케줄러부터 idle 스케줄 클래스까지 순서대로 돌며 다음 태스크를 가져와서 반환한다.
  • 코드 라인 61에서 이 라인으로 내려오는 일이 없다.

 

다음 그림은 런큐에서 태스크가 수행될 때의 스케줄 순서를 보여준다.

  • 마지막 idle-task 스케줄러 이전까지 수행시킬 task가 없는 경우 부트 프로세스 중에 처음 사용했던 idle 태스크가 사용된다.

 


activate & deactivate 태스크

엔큐 및 디큐 플래그들

다음은 태스크의 엔큐에 사용되는 플래그들이다.

  • ENQUEUE_WAKEUP
    • 슬립된 태스크를 외부에서 깨웠을 때 사용된다.
  • ENQUEUE_RESTORE
    • save/restore 페어로 사용되며 설정 변경으로 인해 다시 엔큐할 때 사용된다.
  • ENQUEUE_MOVE
    • save/restore 페어와 같이 사용되며 태스크 그룹을 이동하여 엔큐할 때 사용된다.
  • ENQUEUE_NOCLOCK
    • 엔큐 시 런큐 클럭을 갱신하지 않게 한다.
  • ENQUEUE_HEAD
    • 엔큐 시 런큐의 앞부분에 위치하게 한다.
  • ENQUEUE_REPLENISH
    • 밴드위드 사용으로 인해 스로틀된 태스크가 다시 런타임 보충되어 엔큐되는 상황에서 사용된다.
  • ENQUEUE_MIGRATED
    • 태스크가 다른 cpu의 런큐에 migrate되어 엔큐하는 상황에서 사용된다.

 

다음은 태스크의 디큐에 사용되는 플래그들이다.

  • DEQUEUE_SLEEP
    • 태스크의 슬립으로 인해 디큐되는 상황에서 사용된다.
  • DEQUEUE_SAVE
    • save/restore 페어로 사용되며 설정 변경으로 인해 잠시 디큐한다.
  • DEQUEUE_MOVE
    • save/restore 페어와 같이 사용되며 태스크 그룹을 이동하기 위해 디큐할 때 사용된다.
  • DEQUEUE_NOCLOCK
    • 엔큐시 런큐 클럭을 갱신하지 않게 한다.

 

activate_task()

kernel/sched/core.c

void activate_task(struct rq *rq, struct task_struct *p, int flags)
{
        if (task_contributes_to_load(p))
                rq->nr_uninterruptible--;

        enqueue_task(rq, p, flags);

        p->on_rq = TASK_ON_RQ_QUEUED;
}

태스크를 런큐에 추가한다.

  • 로드 기여중인 uninterruptible 태스크인 경우 런큐의 nr_uninterrtible 카운터를 감소시킨다.

 

task_contributes_to_load()

include/linux/sched.h

#define task_contributes_to_load(task)  ((task->state & TASK_UNINTERRUPTIBLE) != 0 && \
                                         (task->flags & PF_FROZEN) == 0 && \
                                         (task->state & TASK_NOLOAD) == 0)

uninterruptible 태스크 상태이면서 suspend 된 것은 아닌 경우 true를 반환한다.

  • 시스템 suspend 시 frozen 플래그가 설정된다.
  • uninterruptible 태스크가 forzen 플래그와 noload 상태가 없어야 로드 기여 상태가 된다.

 

enqueue_task()

kernel/sched/core.c

static void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
{
        if (!(flags & ENQUEUE_NOCLOCK))
                update_rq_clock(rq);

        if (!(flags & ENQUEUE_RESTORE)) {
                sched_info_queued(rq, p);
                psi_enqueue(p, flags & ENQUEUE_WAKEUP);
        }

        uclamp_rq_inc(rq, p);
        p->sched_class->enqueue_task(rq, p, flags);
}

태스크를 런큐에 추가한다.

  • 런큐 클럭을 갱신시키고 요청 태스크의 스케줄러에 있는 (*enqueue_task) 후크에 연결된 함수를 호출하여 런큐에 추가한다.
  • 각 스케줄러마다 다음의 함수를 호출한다.
    • stop 스케줄러 – enqueue_task_stop()
    • deadline 스케줄러 – enqueue_task_dl()
    • rt 스케줄러 – enqueue_task_rt()
    • cfs 스케줄러 – enqueue_task_fair()
    • idle 스케줄러 – enqueue_task_idle()

 

deactivate_task()

kernel/sched/core.c

void deactivate_task(struct rq *rq, struct task_struct *p, int flags)
{
        p->on_rq = (flags & DEQUEUE_SLEEP) ? 0 : TASK_ON_RQ_MIGRATING;

        if (task_contributes_to_load(p))
                rq->nr_uninterruptible++;

        dequeue_task(rq, p, flags);
}

태스크를 런큐에서 제거한다.

  • 로드 기여중인 uninterruptible 태스크인 경우 런큐의 nr_uninterrtible 카운터를 증가시킨다.

 

dequeue_task()

kernel/sched/core.c

static void dequeue_task(struct rq *rq, struct task_struct *p, int flags)
{
        if (!(flags & DEQUEUE_NOCLOCK))
                update_rq_clock(rq);

        if (!(flags & DEQUEUE_SAVE)) {
                sched_info_dequeued(rq, p);
                psi_dequeue(p, flags & DEQUEUE_SLEEP);
        }

        uclamp_rq_dec(rq, p);
        p->sched_class->dequeue_task(rq, p, flags);
}

태스크를 런큐에서 제거한다.

  • 런큐 클럭을 갱신시키고 요청 태스크의 스케줄러에 있는 (*dequeue_task) 후크에 연결된 함수를 호출한다.
  • 각 스케줄러마다 다음의 함수를 호출한다.
    • stop 스케줄러 – dequeue_task_stop()
    • deadline 스케줄러 – dequeue_task_dl()
    • rt 스케줄러 – dequeue_task_rt()
    • cfs 스케줄러 – dequeue_task_fair()
    • idle 스케줄러 – dequeue_task_idle()

 


기타

스케줄링 클래스 변경

__sched_setscheduler() 에서 스케줄 클래스를 변경하거나 우선 순위를 변경할 때 check_class_changed() 함수를 호출한다.

check_class_changed()

  • (*switched_from)
  • (*switched_to)
  • (*prio_changed)

 

preemption 체크

check_preempt_curr()

  • (*check_preempt_curr)

 

실행 가능 cpu 설정

do_set_cpus_allowed()

  • (*set_cpus_allowed)

 

실행 cpu 변경

set_task_cpu()

  • (*migrate_task_rq)

 

새 태스크 실행

wake_up_new_task()

  • (*select_task_rq)
  • (*task_woken)

 

태스크 킬

finish_task_switch()

  • (*task_dead)

 

실행 도메인을 통해 선택한 cpu에서 태스크 실행(execve)

sched_exec()

  • (*select_task_rq)
  • migration_cpu_stop() -> 디큐 -> cpu 지정 -> 엔큐

 

태스크의 총 실행시간 조회

task_sched_runtime()

  • (*update_curr)
    • 현재 실행 중인 경우 정확한 산정을 위해 현재 까지 실행한 시간을 추가하기 위해 위의 후크를 통해 현재 태스크의 런타임 갱신을 요청한다.

 

다음 태스크에 양보

태스크는 러닝 상태를 유지하여 런큐에서 디큐되지 않은 채로 리스케줄한다. 현재 태스크가 cfs 태스크인 경우 리스케줄 시 현재 태스크는 가능한한 제외한다.

  • 현재 태스크가 cfs 태스크인 경우 skip 버디에 지정되어 pick_next_task()에서 리스케줄 시 선택되지 않게 한다.

sys_sched_yield()

  • (*yield_task)

 

지정된 태스크에 양보

태스크는 러닝 상태를 유지하여 런큐에서 디큐되지 않은 채로 지정한 태스크로 리스케줄한다. 현재 태스크가 cfs 태스크인 경우 리스케줄 시 현재 태스크는 가능한한 제외한다.

  • cfs 태스크인 경우 현재 태스크는 skip 버디에 지정되어 pick_next_task()에서 리스케줄 시 선택되지 않게 한다.
  • cfs 태스크인 경우 지정된 태스크는 next 버디에 지정되어 pick_next_task()에서 리스케줄 시 선택되도록 한다.

yield_to()

  • (*yield_to_task)

 

RR 인터벌 조회

round robin rt 태스크인 경우 rr 인터벌을 반환한다. (디폴트=100ms) cfs 태스크인 경우엔 해당 태스크의 time slice 값을 반환한다.

sched_rr_get_interval()

  • (*get_rr_interval)

 

Migrate 태스크

migrate_tasks()

  • (*put_prev_task)

 

태스크 그룹 변경

sched_change_group()

  • (*task_change_group)

 

런큐 선택

select_task_rq()

 


Wait for Blocked I/O

wait queue와 wait event에 대한 내용은 별도의 페이지에서 분석하기로 하고 관련된 문서는 다음을 먼저 참고한다.

 


스케줄러 Features

sched_feat() 매크로

kernel/sched/sched.h

/*
 * Each translation unit has its own copy of sysctl_sched_features to allow
 * constants propagation at compile time and compiler optimization based on
 * features default.
 */
#define SCHED_FEAT(name, enabled)       \
        (1UL << __SCHED_FEAT_##name) * enabled |
static const_debug __maybe_unused unsigned int sysctl_sched_features =
#include "features.h"
        0;
#undef SCHED_FEAT

#define sched_feat(x) !!(sysctl_sched_features & (1UL << __SCHED_FEAT_##x))

 

kernel/sched/features.h

/*
 * Only give sleepers 50% of their service deficit. This allows
 * them to run sooner, but does not allow tons of sleepers to
 * rip the spread apart.
 */
SCHED_FEAT(GENTLE_FAIR_SLEEPERS, true)

/*
 * Place new tasks ahead so that they do not starve already running
 * tasks
 */
SCHED_FEAT(START_DEBIT, true)

/*
 * Prefer to schedule the task we woke last (assuming it failed
 * wakeup-preemption), since its likely going to consume data we
 * touched, increases cache locality.
 */
SCHED_FEAT(NEXT_BUDDY, false)

/*
 * Prefer to schedule the task that ran last (when we did
 * wake-preempt) as that likely will touch the same data, increases
 * cache locality.
 */
SCHED_FEAT(LAST_BUDDY, true)

/*
 * Consider buddies to be cache hot, decreases the likelyness of a
 * cache buddy being migrated away, increases cache locality.
 */
SCHED_FEAT(CACHE_HOT_BUDDY, true)

/*
 * Allow wakeup-time preemption of the current task:
 */
SCHED_FEAT(WAKEUP_PREEMPTION, true)

SCHED_FEAT(HRTICK, false)
SCHED_FEAT(DOUBLE_TICK, false)

/*
 * Decrement CPU capacity based on time not spent running tasks
 */
SCHED_FEAT(NONTASK_CAPACITY, true)

/*
 * Queue remote wakeups on the target CPU and process them
 * using the scheduler IPI. Reduces rq->lock contention/bounces.
 */
SCHED_FEAT(TTWU_QUEUE, true)

/*
 * When doing wakeups, attempt to limit superfluous scans of the LLC domain.
 */
SCHED_FEAT(SIS_AVG_CPU, false)
SCHED_FEAT(SIS_PROP, true)

/*
 * Issue a WARN when we do multiple update_rq_clock() calls
 * in a single rq->lock section. Default disabled because the
 * annotations are not complete.
 */
SCHED_FEAT(WARN_DOUBLE_CLOCK, false)

#ifdef HAVE_RT_PUSH_IPI
/*
 * In order to avoid a thundering herd attack of CPUs that are
 * lowering their priorities at the same time, and there being
 * a single CPU that has an RT task that can migrate and is waiting
 * to run, where the other CPUs will try to take that CPUs
 * rq lock and possibly create a large contention, sending an
 * IPI to that CPU and let that CPU push the RT task to where
 * it should go may be a better scenario.
 */
SCHED_FEAT(RT_PUSH_IPI, true)
#endif

SCHED_FEAT(RT_RUNTIME_SHARE, true)
SCHED_FEAT(LB_MIN, false)
SCHED_FEAT(ATTACH_AGE_LOAD, true)

SCHED_FEAT(WA_IDLE, true)
SCHED_FEAT(WA_WEIGHT, true)
SCHED_FEAT(WA_BIAS, true)

/*
 * UtilEstimation. Use estimated CPU utilization.
 */
SCHED_FEAT(UTIL_EST, true)

다음과 같은 스케줄러 feature들이 있다. (디폴트: 주황색=true, 파란색=false)

  • GENTLE_FAIR_SLEEPERS
    • 슬립 후 깨어나는 태스크에 대해 스케줄 레이턴시의 절반 만큼 더 빨리 실행할 수 있도록 한다. (50% 보너스)
    • 이 기능을 disable하면 저가형(low-end) 디바이스에서 응답성이 좋아진다.
    • 참고: sched: Implement a gentler fair-sleepers feature (2009, v2.6.32-rc1)
  • START_DEBIT
    • 새(fork) 태스크에 대해 한 타임(스케줄 레이턴시) 뒤에서 실행하도록 한다.
    • 새 태스크가 이미 실행 중인 태스크를 방해하지 못하게 한다.
    • 참고: Improving scheduler latency (2010) | LWN.net
  • NEXT_BUDDY
    • 캐시 지역성을 높이기 위해 깨어난 태스크를 다음 스케줄 시 우선 처리한다.
  • LAST_BUDDY
    • 캐시 지역성을 향상시키기 위해 웨이크 업 preemption이 성공하면 preemption 직전에 실행된 작업 옆에 둔다.
  • CACHE_HOT_BUDDY
    • 캐시 지역성을 높이기 위해 마이그레이션 할 작업을 항상 캐시 hot 상태로 판단한다. (다른 cpu로의 마이그레이션 비율을 축소시킨다)
  • WAKEUP_PREEMPTION
    • 헤비 로드를 갖는 시스템에서 이 옵션을 disable하면 preemption을 하지 않도록 하여 성능을 높일 수 있다. 단 반응성이 떨어진다.
  • HRTICK
    • hrtick을 사용하면 태스크마다 주어지는 런타임을 hrtimer를 사용하여 틱을 만들어낸다.
  • DOUBLE_TICK
    • 정규 틱과 hrtick을 동시에 운영하게 한다.
  • NONTASK_CAPACITY
  • TTWU_QUEUE
    • 캐시 지역성을 높이기 위해서 태스크가 로컬 cpu가 아닌 다른 cpu에서 깨어나야 할 때 IPI를 사용하는 리모트 큐의 사용 여부를 결정한다.
    • 이 기능을 사용하면서 깨어날 태스크가 로컬 캐시를 공유하지 않는 cpu인 경우 IPI를 통해 원격 cpu 런큐에 태스크를 깨운다.
    • 이 기능을 사용하지 않거나 현재 cpu가 깨어나야 할 cpu가 서로 로컬 캐시를 공유하는 경우 다음과 같이 기존 wakeup 방법을 사용한다.
      • 로컬 cpu가 리모트 cpu의 런큐락을 획득하고 직접 enqueue한 후 preempt 요청한다.
  • SIS_AVG_CPU
    • wake 밸런싱의 캐시 친화 cpu 관련 기능이다. wake 밸런싱에서 cpu의 평균 idle 시간(rq->avg_idle)이 스케줄 도메인의 wakeup cost(sd->avg_scan_cost)에 비해 너무 짧은 경우 밸런싱을 방지한다.
    • 참고: sched/fair: Make select_idle_cpu() more aggressive (2017, v4.11)
  • SIS_PROP
  • WARN_DOUBLE_CLOCK
  • RT_PUSH_IPI
  • RT_RUNTIME_SHARE
    • SMP 시스템에서 RT 그룹 스케줄링을 사용할 때 런타임이 부족해진 경우 다른 cpu로 부터 빌려올 수 있게 한다.
    • 이 기능은 빌려오는 런타임때문에 cfs 태스크의 기아(starving) 현상이 발생할 수 있어 커널 v5.10-rc1에서 디폴트 값을 disable 하였다.
  • LB_MIN
  • ATTACH_AGE_LOAD
  • WA_IDLE
    • wake 밸런싱의 캐시 친화 cpu 관련 기능이다. 태스크의 기존 cpu가 캐시 친화 idle인 경우 약간의 성능을 개선하기 위해 밸런싱을 방지한다.
    • 참고: sched/core: Fix wake_affine() performance regression (2017, v4.14-rc5)
  • WA_WEIGHT
    • wake 밸런싱의 캐시 친화 cpu 관련 기능이다. 태스크의 기존 cpu와 현재 cpu간의 러너블 로드가 작은 쪽으로 밸런싱을 수행하게 한다. 이렇게 하여 약간의 성능을 개선한다.
    • 참고: sched/core: Address more wake_affine() regressions (2017, v4.14-rc5)
  • WA_BIAS
    • wake 밸런싱의 캐시 친화 cpu 관련 기능이다. 위의 WA_WEIGHT 기능을 사용할 때 태스크의 기존 cpu 로드에 약간의 바이어스(sd->imbalance_pct의 100% 초과분 절반)를 추가하여 this cpu 쪽으로 조금 더 유리한 선택이되게 한다.
  • UTIL_EST
    • EAS를 사용한 wake 밸런싱에서 관련 기능이다. 매우 짧은 시간 실행되는 태스크의 경우 1ms 단위로 갱신되는 PELT 시그널을 사용한 유틸 로드가 적을 수 있다. 따라서 태스크의 디큐 시마다 산출되는 추정 유틸(estimated utilization)과 기존 유틸 중 큰 값을 사용해야 cpu 간의 더 정확한 유틸 비교를 할 수 있다.
    • 참고: sched/fair: Update util_est only on util_avg updates (2018, v4.17-rc1)

 

참고: Tweak Kernel’s Task Scheduler to Boost Performance on Android [Part 2] (2018) | DroidViews.com

 

다음은 qemu에서 동작중인 커널 v5.4의 스케줄러 feature들 예를 보여준다.

$ cat /sys/kernel/debug/sched_features
GENTLE_FAIR_SLEEPERS START_DEBIT NO_NEXT_BUDDY LAST_BUDDY CACHE_HOT_BUDDY WAKEUP_PREEMPTION 
NO_HRTICK NO_DOUBLE_TICK         NONTASK_CAPACITY TTWU_QUEUE NO_SIS_AVG_CPU SIS_PROP NO_WARN_DOUBLE_CLOCK 
RT_PUSH_IPI                      RT_RUNTIME_SHARE NO_LB_MIN ATTACH_AGE_LOAD WA_IDLE WA_WEIGHT WA_BIAS 
UTIL_EST

 

다음은 rpi4 시스템에서 동작중인 커널 v4.19의 스케줄러 feature들 예를 보여준다.

$ cat /sys/kernel/debug/sched_features
GENTLE_FAIR_SLEEPERS START_DEBIT NO_NEXT_BUDDY LAST_BUDDY CACHE_HOT_BUDDY WAKEUP_PREEMPTION 
NO_HRTICK NO_DOUBLE_TICK LB_BIAS NONTASK_CAPACITY TTWU_QUEUE NO_SIS_AVG_CPU SIS_PROP NO_WARN_DOUBLE_CLOCK 
RT_PUSH_IPI                      RT_RUNTIME_SHARE NO_LB_MIN ATTACH_AGE_LOAD WA_IDLE WA_WEIGHT WA_BIAS 
UTIL_EST

 

다음은 rock960 시스템에서 동작중인 커널 v4.4의 스케줄러 feature들 예를 보여준다.

$ cat /sys/kernel/debug/sched_features
GENTLE_FAIR_SLEEPERS START_DEBIT NO_NEXT_BUDDY LAST_BUDDY CACHE_HOT_BUDDY WAKEUP_PREEMPTION 
NO_HRTICK NO_DOUBLE_TICK LB_BIAS NONTASK_CAPACITY TTWU_QUEUE
RT_PUSH_IPI NO_FORCE_SD_OVERLAP  RT_RUNTIME_SHARE NO_LB_MIN ATTACH_AGE_LOAD                 
ENERGY_AWARE

 

참고

 

 

Scheduler -2- (Global Cpu Load)

<kernel v5.4>

Scheduler -2- (Global Cpu Load)

cpu 로드 관련 소스의 위치는 다음과 같은 변화가 있어왔다.

 

추세 관련 사전 산술 지식

주식 시장에서 주가 흐름에 단순이동평균(SMA), 가중이동평균(WMA), 지수이동평균(EMA) 등을 사용하여 기간별 평균흐름을 나타내는데 먼저 머리를 정리하기 위해 다음과 같은 방법이 있음을 미리 산출해보자.

  • 예) 최근 6일간 종가(raw data): 1020, 1030, 1000, 1010, 1020, 1060일 때
    • 3일 단순 이동 평균(SMA: Simple Moving Average)
      • 과거 데이터와 최근 데이터를 고르게 반영
      • = (1010 + 1020 + 1060) / 3
      • = 1030
    • 3일 가중 이동 평균(WMA; Weighted Moving Average)
      • 최근 데이터에 높은 가중치를 주는 방법 (예: w1=1/6, w2=2/6, w3=3/6)
      • = 1010 * w1 + 1020 * w2 + 1060 * w3
      • = 168 + 340 + 530
      • = 1038
    • 3일 지수 이동 평균(EMA: Exponential Moving Average 또는 EWMA: Exponential Weighted Moving Average)
      • 최근 데이터에 더 높은 가중치를 주는 방법으로 전일 지수 이동 평균 값과 금일 값만 사용하여 간단히 계산한다.
      • 지수 평활 계수(Exponential Percentage) k를 사용하여 1-k를 전일 지수 이동 평균값에 곱하여 사용하고, 금일 값에 k를 곱해 사용한다.
        • 평활 계수는 다음과 같은 지수 함수 모델을 사용한다. (n=기간)
          • k = a * (b^n)
      • 지수 평활 계수(Exponential Percentage) k는 환경에 따라 다르다.
      • 주식 시장의 평활 계수 k는 지수 평균 기간에 의한 추정법을 사용하여 다음과 같이 결정된다.
        • (a=2, b=1/(n+1))
        • k= 2 * 1/(n+1) = 2/(n+1)
          • 예: n=2일, k=2/(n+1)=0.666666…
          • 예: n=3일, k=2/(n+1)=0.5
          • 예: n=10일, k=2/(n+1)=0.181818…
      • 매일 지수 이동 평균 값 * (1-k) + 금일 값 * (k)을 반영하여 산출한다.
        • 1일차: 1020 = 100% 반영
        • 2일차: 2일간 이동 평균 반영 = 1020 * 33% + 1030 * 66% = 1026
        • 3일차: 3일간 이동 평균 반영 = 1026 * 50% + 1000 * 50% = 1013
        • 4일차: 3일간 이동 평균 반영 = 1013 * 50% + 1010 * 50% = 1012
        • 5일차: 3일간 이동 평균 반영 = 1012 * 50% + 1020 * 50% = 1016
        • 6일차: 3일간 이동 평균 반영 = 1016 * 50% + 1060 * 50% = 1038
  • 가중 이동 평균과 지수 이동 평균에서 적용되는 가중치와 평활 계수가 적용 일자에 따라 변화되는 그래프 값은 다음 자료를 참고한다.

 

리눅스 커널에서의 지수 이동 평균

리눅스 커널에서는 cpu 로드 산출에 필요한 모든 raw 데이터를 저장하지 않는다. 따라서 가장 적합한 모델로 raw 데이터를 저장하지 않는 특성을 가진 지수 이동 평균(EMA)를 사용한다. 그리고 지수 평활법(ES: Exponential Smoothing) 중 하나인 지수 감소 평균(EDA)을 사용하여 지수 평활 계수 k값을 결정하는데 글로벌 cpu 로드와 PELT 산출에 사용하는 k 값이 다르며 각각의 주제에서 다루기로 한다.

  • 지수 감소 평균(EDA: Exponential Decaying Average, EDMA: Exponential Damped Moving Average)

글로벌 cpu 로드 산출식

지정된 기간에 어떠한 일을 cpu가 쉬지 않고 일을 한 경우 그 값을 1.0이라고 한다. 만일 지정된 기간에 절반 동안 idle 상태에 빠져 있으면 0.5라고 한다. 그런데 밀려 있는 일이 있으면 그 량만큼 더해 산정하여 1을 초과하게 된다.

예) 1분 동안 같은 양의 작업을 10개를 수행하는 cpu가 있다 할 때 1분 동안 30개의 작업이 주어지면 10개는 처리를 하고 20개는 처리하지 못한 상태가 된다. 이러한 경우 cpu 로드는 3.0이라 한다.

예) 위와 동일한 조건으로 cpu가 2개 준비된 경우 cpu 로드는 1.5라 한다.

 

cpu 로드의 이진화 정수 사용

cpu 로드 값은 32비트 시스템에서는 11비트를 사용한다. 실수 로드 값 1.0에 대응하는 2^11=2048 이진화 정수 값을 사용한다.

  • 1개의 cpu가 있는 시스템에서 태스크 2개가 동작 상태(runnable)인 경우 cpu 로드 값은 2.0이 된다.
  • cpu가 4개가 동작하는 시스템에서는 이 값을 cpu 수 4로 나누게 되므로 0.5의 cpu 로드 값이 된다.

 

글로벌 로드 평균 (1min ~ 15min)

최근 1분, 5분, 15분 간의 평균 cpu 로드 값을 산출하여 avenrun[]에 저장하고 글로벌 로드 평균(global load average)라고도 불린다.

  • “uptime” 명령의 load average 필드의 3개의 값을 참고한다.
  • 또한 “/proc/loadavg” 파일을 확인하여 최초 3개의 값을 통해서도 확인할 수 있다.

 

$ uptime
 20:41:18 up  1:09,  2 users,  load average: 0.57, 0.15, 0.11
$ cat /proc/loadavg 
0.55 0.28 0.16 1/318 2112
  • nr_active = 각 cpu의 런큐에 있는 running 및 uninterruptible 태스크의 수를 더한다.
  • avenrun[0] = avenrun[0]*e1 + nr_active*(1-e1) + 0.5    <- e1 = n=12(1분)에서의 지수 평활 계수 k
  • avenrun[1] = avenrun[1]*e2 + nr_active*(1-e2) + 0.5    <- e2 = n=60(5분)에서의 지수 평활 계수 k
  • avenrun[2] = avenrun[2]*e3 + nr_active*(1-e3) + 0.5    <-e3 = n=180(15분)에서의 지수 평활 계수 k

 

커널의 글로벌 cpu 로드를 위한 지수 평활 계수는 다음과 같다.

  • 지수 함수 모델 k = a * (b^n)
    • a=1, b=e^(-1/n), n=반영할 기간
  • k=e^(-1/n)
  • 지수 평활 계수 k를 사용하여 cpu 로드 값은 다음과 같이 산출된다.
    • cpu 로드 값 = 기존 cpu 로드 값 * k + 새 cpu 로드 값 * (1 – k)
  • 예) 커널이 5초에 한번씩 측정된 과거 1분 동안의 누적 데이터를 사용하므로 기간 n=12가 된다.
    • k=e^(-1/12) = 0.920044 (약 92%)
      • Excel Spread Sheet 산출 예)
        • =@exp(-1/12)

 

런큐 로드 평균 (1 tick ~ 16 ticks)

cpu 로드 밸런스에 사용하였던 이 방법은  커널 v5.3-rc1에서 제거되었다.

 


글로벌 로드 평균

avenrun[] 산출 – (1, 5, 15분)

글로벌 로드 평균은 매 스케줄 틱에서 호출되지만 산출 주기(calc_load_update: 기본 5초)에 한 번씩 갱신한다. 엄밀히 말하자면 산출 주기가 도래하면 그 후 10틱의 시간이 지난 후에 갱신한다. 글로벌 로드 평균을 산출하는 이 함수의 호출 경로는 다음과 같다. (ARM64 기준)

  • 고정 틱 핸들러
    • tick_handle_periodic() -> tick_periodic() -> do_timer() -> calc_global_load()
  • 틱 nohz 핸들러
    • tick_nohz_handler() -> tick_sched_do_timer() -> tick_do_update_jiffies64() -> do_timer() -> calc_global_load()
    • tick_nohz_update_jiffies() -> tick_do_update_jiffies64() -> do_timer() -> calc_global_load()
    • tick_nohz_restart_sched_tick() -> tick_do_update_jiffies64() -> do_timer() -> calc_global_load()
  • hrtimer 틱 핸들러
    • tick_sched_timer() -> tick_sched_do_timer() -> tick_do_update_jiffies64() -> do_timer() -> calc_global_load()

 

calc_global_load()

kernel/sched/loadavg.c

/*
 * calc_load - update the avenrun load estimates 10 ticks after the
 * CPUs have updated calc_load_tasks.
 *
 * Called from the global timer code.
 */
void calc_global_load(unsigned long ticks)
{
        unsigned long sample_window;
        long active, delta;

        sample_window = READ_ONCE(calc_load_update);
        if (time_before(jiffies, sample_window + 10))
                return;

        /*
         * Fold the 'old' NO_HZ-delta to include all NO_HZ CPUs.
         */
        delta = calc_load_nohz_fold();
        if (delta)
                atomic_long_add(delta, &calc_load_tasks);

        active = atomic_long_read(&calc_load_tasks);
        active = active > 0 ? active * FIXED_1 : 0;

        avenrun[0] = calc_load(avenrun[0], EXP_1, active);
        avenrun[1] = calc_load(avenrun[1], EXP_5, active);
        avenrun[2] = calc_load(avenrun[2], EXP_15, active);

        WRITE_ONCE(calc_load_update, sample_window + LOAD_FREQ);

        /*
         * In case we went to NO_HZ for multiple LOAD_FREQ intervals
         * catch up in bulk.
         */
        calc_global_nohz();
}

최근 1분, 5분, 15분 주기 지수 이동 평균을 사용한 글로벌 cpu 로드를 산출하여 avenrun[]에 대입한다. 이 함수가 매 틱마다 호출되지만 실제 산출은 5초 주기로 수행한다. 엄밀히 5초 주기 이후 lockless flip 인덱스 기법을 사용하기 위해 10틱이 지난 후에 수행한다.

  • 코드 라인 6~8에서 현재 시각(jiffies)이 cpu 로드 산출 주기(5초)+10틱 이전이면 함수를 빠져나간다.
  • 코드 라인 13~15에서 현재 cpu에 대해 active 태스크 수의 변화분 delta를 얻어와서 calc_load_tasks에 반영한다.
  • 코드 라인 17~18에서 active 태스크가 있는 경우 이를 11비트 정밀도를 갖는 이진화 정수로 변환시킨다.이 값을 전일 지수 이동 평균값에 적용할 예정이다.
    • 예) cpu 로드 값이 2이라면 2048(실수 1.0)을 곱한 값을 현재 로드 값으로 반영할 예정이다.
  • 코드 라인 20~22에서 기존 cpu 로드 값인 avenrun[]과 새로 추가 반영할 cpu 로드 값인 active 값으로 각각 1분, 5분, 15분에 해당하는 지수 평활 계수 EXP_n을 사용하여 avenrun[]에 반영한다.
  • 코드 라인 24에서 다음 cpu 로드 산출 주기를 위해 5초를 더한다.
  • 코드 라인 30에서 nohz idle로 인해 다음 cpu 로드 산출 주기를 이미 초과한 경우 miss된 cpu 로드 갱신 주기 수만큼 적용하여 cpu 로드를 산출하여 avenrun[]을 다시 갱신한다.
    • 예) 20여초 nohz idle로 인해 cpu 로드를 갱신하지 못한 경우 처음 5초에 해당하는 시간의 cpu 로드가 이미 갱신되었으므로 나머지 3번에 해당하는 cpu 로드를 산출하도록 한다.

 

cpu 로드 계산을 위한 11비트 지수 상수 값

11비트 정밀도를 가진 이진화 정수값을 산출하기 위한 각종 상수들이다.

include/linux/sched/loadavg.h

/*
 * These are the constant used to fake the fixed-point load-average
 * counting. Some notes:
 *  - 11 bit fractions expand to 22 bits by the multiplies: this gives
 *    a load-average precision of 10 bits integer + 11 bits fractional
 *  - if you want to count load-averages more often, you need more
 *    precision, or rounding will get you. With 2-second counting freq,
 *    the EXP_n values would be 1981, 2034 and 2043 if still using only
 *    11 bit fractions.
 */
extern unsigned long avenrun[];         /* Load averages */
extern void get_avenrun(unsigned long *loads, unsigned long offset, int shift);

#define FSHIFT          11              /* nr of bits of precision */
#define FIXED_1         (1<<FSHIFT)     /* 1.0 as fixed-point */
#define LOAD_FREQ       (5*HZ+1)        /* 5 sec intervals */
#define EXP_1           1884            /* 1/exp(5sec/1min) as fixed-point */
#define EXP_5           2014            /* 1/exp(5sec/5min) */ 
#define EXP_15          2037            /* 1/exp(5sec/15min) */
  • FSHIFT
    • 11비트 정확도를 사용하는데 필요한 비트 수
  • FIXED_1
    • 11비트 정확도 값 (2048)
    • 고정 소숫점 형태인 cpu 로드 1.0은 리눅스 구현에서는 2048이라는 이진화 정수로 표현한다.
  • LOAD_FREQ
    • 디폴트 5초 주기 산출
  • EXP_1
    • 마지막 1분 동안의 cpu 로드를 산출하기 위해 11비트 정확도 값 2048의 약 92%를 적용하여 반영한다.
    • 1/(exp(1/12)) * FIXED_1 = 92.00% * 2048 = 1884
  • EXP_5
    • 마지막 5분 동안의 cpu 로드를 산출하기 위해 11비트 정확도 값 2048의 약 98%를 적용하여 반영한다.
    • 1/(exp(1/60)) * FIXED_1 = 98.35% * 2048 = 2014
  • EXP_15
    • 마지막 15분 동안의 cpu 로드를 산출하기 위해 11비트 정확도 값 2048의 약 99%를 적용하여 반영한다.
    • 1/(exp(1/180)) * FIXED_1 = 99.45% * 2048 = 2037

 

다음 그림은 1분, 5분, 15분에 해당하는 지수 팩터 EXP_1, EXP_5, EXP_15가 어떻게 산출되었는지를 보여준다.

  • k(12) = EXP_1 = 0.9200 (=1884)
  • k(60) = EXP_5 = 0.9835 (=2014)
  • k(180) = EXP_15 = 0.9945 (=2037)

 

다음 그림은 글로벌 cpu 로드를 산출하는 식을 보여준다. (커널 v4.7~)

 

다음 그림은 처음 cpu 로드 0.5에서 출발하여 매 5초 마다 cpu 로드가 산출되는 과정을 보여주며 마지막에서는 nohz idle로 인해 20초간 틱이 발생되지 않아 밀려 있는 cpu 로드 계산이 한꺼번에 이루어지는 것을 보여준다.

  • 주의: 커널 v4.6 이하에서 반올림이 적용되어 산출한 값이다. 커널 v4.7 이상에서는 새 로드가 기존 로드보다 동등하거나 상승하는 경우 올림처리한다.

 

active 태스크 수 이론

cpu 로드를 산출하기 위해 active 태스크 수를 파악해야 한다. 리눅스에서의 active 태스크 수는 다음을 포함한다.

  • runnable 태스크 수
    • running 태스크(rq->curr 태스크)
    • 런큐에서 엔큐되어 대기중인 태스크들
  • uninterruptible 상태로 슬립한 태스크 수

 

리눅스 시스템에서의 글로벌 cpu 로드는 cpu 로드라기 보다는 시스템 로드에 더 가깝다. cpu 로드를 산출하기 위해 active 태스크 수를 파악해야 한다.

  • active 태스크 산출 요소
    • runnable 태스크의 수
      • running 태스크 (cfs_rq->curr 엔티티)
      • RB Tree enqueue 태스크들
      • 유닉스 및 리눅스에서 사용
    • Sleep 상태의 uninterruptible 태스크(주로 io 요청 대기 스레드) 수
      • 리눅스에서만 사용

 

유닉스 등과 다르게 리눅스에서는 uninterruptible 상태로 슬립 중인 태스크도 포함시키는 이유로 로드를 산출하는데 이 uninterruptible 태스크가 I/O 요청에 대한 대기 상태인 경우가 많고 이에 따라 I/O 요청에 대한 로드를 포함하는 것이 합리적인 판단이라서 포함시켰다.  확실히 이와 같은 형태의 글로벌 로드 산출 구현은 정확한 로드를 산출하는 것이 힘들다는 것을 알 수 있다. 그러므로 cpu 글로벌 로드 뿐만 아니라 여러 상태 지표를 동시에 사용하여야 시스템의 로드 상태를 파악할 수 있다. (결국 지금도 완전하지 않은 상태로 추후 바뀔 가능성도 있다)

 

다음 그림은 루트 태스크 그룹만을 가진 2개 cpu에서 동작하는 각 cfs 런큐의 태스크 상태들을 보여준다. 이들 중 로드에 포함될 active 태스크 수는 8이다.

  • 로드에 포함될 태스크들 (총 8개)
    • 4 개의 runnable 태스크들
      • 2 개의 running 태스크들
      • 2 개의 enqueue 태스크들
    • 4 개의 슬립 태스크들
      • 4 개의uninterruptible 태스크들

 

런큐에서의 active 태스크 수 산출 실전

active 태스크 수를 산출할 때 마다 각 cpu의 런큐를 뒤지지 않고 전역 변수 calc_load_tasks에 필요 시마다 전파하여 사용하는 방식을 사용하여 조금이라도 빠른 산출 방법을 적용하였다. nohz idle을 위해 중간 단계에 calc_load_nohz[2] 변수를 사용하여 active 태스크의 변동 분 delta를 저장하고 5초마다 로드가 산출될 때 calc_load_nohz[2] 배열에서 인덱스를 교대로 사용하여 읽어낸다. 배열의 사용법은 조금 복잡하니 calc_load_nohz_fold()에서 더 알아보자.

 

다음 그림은 nohz idle 상태로 진입할 때 active 태스크의 변동분을 저장하는 함수와 5초 주기로 글로벌 로드를 산출하는 함수의 흐름을 보여준다.

 

calc_load_nohz_fold()

kernel/sched/loadavg.c

static long calc_load_nohz_fold(void)
{
        int idx = calc_load_read_idx();
        long delta = 0;
        
        if (atomic_long_read(&calc_load_nohz[idx]))
                delta = atomic_long_xchg(&calc_load_nohz[idx], 0);

        return delta;
}

현재 cpu의 런큐에서 다시 계산된 active 태스크 수의 변경된 값 delta를 얻어온다. (lockless를 위해 flip 인덱스로 접근하도록 설계되었다.)

  • 코드 라인 3에서 calc_load_nohz[] 배열 중 어떤 값을 사용할지 인덱스 값을 알아온다.
  • 코드 라인 6에서 인덱스에 해당하는 calc_load_nohz[] 값이 존재하는 경우 이 값을 0으로 초기화하고 저장되었던 값을 반환한다.

 

calc_load_nohz[]

kernel/sched/loadavg.c

/*
 * Handle NO_HZ for the global load-average.
 *
 * Since the above described distributed algorithm to compute the global
 * load-average relies on per-CPU sampling from the tick, it is affected by
 * NO_HZ.
 *
 * The basic idea is to fold the nr_active delta into a global NO_HZ-delta upon
 * entering NO_HZ state such that we can include this as an 'extra' CPU delta
 * when we read the global state.
 *
 * Obviously reality has to ruin such a delightfully simple scheme:
 *
 *  - When we go NO_HZ idle during the window, we can negate our sample
 *    contribution, causing under-accounting.
 *
 *    We avoid this by keeping two NO_HZ-delta counters and flipping them
 *    when the window starts, thus separating old and new NO_HZ load.
 *
 *    The only trick is the slight shift in index flip for read vs write.
 *
 *        0s            5s            10s           15s
 *          +10           +10           +10           +10
 *        |-|-----------|-|-----------|-|-----------|-|
 *    r:0 0 1           1 0           0 1           1 0
 *    w:0 1 1           0 0           1 1           0 0
 *
 *    This ensures we'll fold the old NO_HZ contribution in this window while
 *    accumlating the new one.
 *
 *  - When we wake up from NO_HZ during the window, we push up our
 *    contribution, since we effectively move our sample point to a known
 *    busy state.
 *
 *    This is solved by pushing the window forward, and thus skipping the
 *    sample, for this CPU (effectively using the NO_HZ-delta for this CPU which
 *    was in effect at the time the window opened). This also solves the issue
 *    of having to deal with a CPU having been in NO_HZ for multiple LOAD_FREQ
 *    intervals.
 *
 * When making the ILB scale, we should try to pull this in as well.
 */
static atomic_long_t calc_load_nohz[2];
static int calc_load_idx;

 

nohz idle 영향으로 인해 변경된 해당 cpu의 active 태스크 수 delta 값을 calc_load_nohz[2]에 저장을 하는데 저장 위치는 5초 갱신 주기마다 교대로 바뀐다. 이러한 delta 값을 읽어내는 루틴에서는 매 5초 + 10 틱마다 지난 5초 이내에 변동된 delta 값을 읽어낸다. 읽어내는 시점의 10 tick 범위는 다음에 읽도록 한다.

  • calc_load_read_idx() 함수를 통해 calc_load_idx 값의 하위 1비트만 읽어서 0과 1의 인덱스 값으로 calc_load_nohz[] 값을읽어온다.
  • 매 5초+10틱에서 calc_load_idx 값을 증가시킨다.
  • calc_load_write_idx() 함수를 통해 calc_load_idx 값의 하위 1비트만 읽어내는데 10 tick 구간은 flip 시킨다. 이렇게 알아온 0과 1의 인덱스 값으로 calc_load_nohz[] 값을읽어온다.

 

calc_load()

kernel/sched/loadavg.c

/*
 * a1 = a0 * e + a * (1 - e)
 */
static inline unsigned long
calc_load(unsigned long load, unsigned long exp, unsigned long active)
{
        unsigned long newload;

        newload = load * exp + active * (FIXED_1 - exp);
        if (active >= load)
                newload += FIXED_1-1;

        return newload / FIXED_1;
}

지수 이동 평균 기간이 적용된 지수 평활 계수 @exp를 사용하여 새 로드 값을 산출한다.

  • 코드 라인 5에서 기존 로드 값 @load에 이동 평균 기간이 적용된 지수 평활 계수 @exp를 곱해 감소시킨 후 현재 로드 값인 active 값을 반영한다.
    • @load 값은 기존 로드 값으로 11비트 정확도를 사용한 이진화 정수 값이다.
    • @active 값은 반영할 현재 로드 값으로 11비트 정확도를 사용한 이진화 정수 값이다.
  • 코드 라인 6~9에서 반영할 로드 값이 기존 로드 값보다 더 큰 경우 상승 추세이므로 소숫점이하 올림 처리하고, 그 외에는 버린다.

 

다음 그림은 글로벌 cpu 로드를 산출하는 예를 보여준다. (커널 v4.7~)

 

예) 기존 1,5,15분간 cpu 로드=0.5이고 이번 틱에 active 태스크=2인 상태로 3번의 틱 만큼 진행을 하면

  • 산출 전:
    • avenrun[0]=1024, active=4096
    • avenrun[1]=1024, active=4096
    • avenrun[2]=1024,active=4096
  • 1st 틱:
    • avenrun[0]=(1024 * 1884 + 4096 * (2048-1884) + 2047) / 2048 = 1270
    • avenrun[1]=(1024 * 2014 + 4096 * (2048-2014) + 2047) / 2048 = 1075
    • avenrun[2]=(1024 * 2037 + 4096 * (2048-2037) + 2047) / 2048 = 1041
  • 2nd 틱:
    • avenrun[0]=(1271 * 1884 + 4096 * (2048-1884) + 2047) / 2048 = 1497
    • avenrun[1]=(1076 * 2014 + 4096 * (2048-2014) + 2047) / 2048 = 1126
    • avenrun[2]=(1041 * 2037 + 4096 * (2048-2037) + 2047) / 2048 = 1058
  • 3rd 틱:
    • avenrun[0]=(1498 * 1884 + 4096 * (2048-1884) + 2047) / 2048 = 1706
    • avenrun[1]=(1127 * 2014 + 4096 * (2048-2014) + 2047) / 2048 = 1176
    • avenrun[2]=(1058 * 2037 + 4096 * (2048-2037) + 2047) / 2048 = 1075

 

예) 기존 1,5,15분간 cpu 로드=0.5이고 이번 틱에 active 태스크=0인 상태로 3번의 틱 만큼 진행을 하면

  • 산출 전:
    • avenrun[0]=1024, active=0
    • avenrun[1]=1024, active=0
    • avenrun[2]=1024,active=0
  • 1st 틱:
    • avenrun[0]=(1024 * 1884 + 0 * (2048-1884) + 0) / 2048 = 942
    • avenrun[1]=(1024 * 2014 + 0 * (2048-2014) + 0) / 2048 = 1007
    • avenrun[2]=(1024 * 2037 + 0 * (2048-2037) + 0) / 2048 = 1018
  • 2nd 틱:
    • avenrun[0]=(1271 * 1884 + 0 * (2048-1884) + 0) / 2048 = 866
    • avenrun[1]=(1076 * 2014 + 0 * (2048-2014) + 0) / 2048 = 990
    • avenrun[2]=(1041 * 2037 + 0 * (2048-2037) + 0) / 2048 = 1012
  • 3rd 틱:
    • avenrun[0]=(1498 * 1884 + 0 * (2048-1884) + 0) / 2048 = 796
    • avenrun[1]=(1127 * 2014 + 0 * (2048-2014) + 0) / 2048 = 973
    • avenrun[2]=(1058 * 2037 + 0 * (2048-2037) + 0) / 2048 = 1006

 

cpu 로드 소숫점 처리

글로벌 cpu 로드를 처리하는데 장시간 idle 상태인데에도 각 주기별로 0.00, 0.01, 0.05 이하 값이 더 이상 하강하지 않는 버그가 있어 수정하였다.

  • 커널 v4.7이후 현재 코드와 동일하게 상승시 올림 처리하고, 하강시 내림 처리한다.
  • 커널 v4.7 이전 코드들은 상승/하강시 모두 반올림 처리하였다.
    • load *= exp;
    • load += active * (FIXED_1 – exp);
    • load += 1UL << (FSHIFT – 1);
    • return load >> FSHIFT;

 

다음 그림은 로드 값의 변화를 산출할때 반올림하여 처리하는 모습을 보여준다. (커널 v4.6 이하)

 

calc_global_nohz()

kernel/sched/loadavg.c

/*
 * NO_HZ can leave us missing all per-CPU ticks calling
 * calc_load_fold_active(), but since a NO_HZ CPU folds its delta into
 * calc_load_nohz per calc_load_nohz_start(), all we need to do is fold
 * in the pending NO_HZ delta if our NO_HZ period crossed a load cycle boundary.
 *
 * Once we've updated the global active value, we need to apply the exponential
 * weights adjusted to the number of cycles missed.
 */
static void calc_global_nohz(void)
{
        unsigned long sample_window;
        long delta, active, n;

        sample_window = READ_ONCE(calc_load_update);
        if (!time_before(jiffies, sample_window + 10)) {
                /*
                 * Catch-up, fold however many we are behind still
                 */
                delta = jiffies - sample_window - 10;
                n = 1 + (delta / LOAD_FREQ);

                active = atomic_long_read(&calc_load_tasks);
                active = active > 0 ? active * FIXED_1 : 0;

                avenrun[0] = calc_load_n(avenrun[0], EXP_1, active, n);
                avenrun[1] = calc_load_n(avenrun[1], EXP_5, active, n);
                avenrun[2] = calc_load_n(avenrun[2], EXP_15, active, n);

                WRITE_ONCE(calc_load_update, sample_window + n * LOAD_FREQ);
        }

        /*
         * Flip the NO_HZ index...
         *
         * Make sure we first write the new time then flip the index, so that
         * calc_load_write_idx() will see the new time when it reads the new
         * index, this avoids a double flip messing things up.
         */
        smp_wmb();
        calc_load_idx++;
}

nohz idle로 인해 갱신이 밀려 있는 경우 그 밀려 있는 횟수 만큼 한 번에 산출을 하여 avenrun[]에 대입한다.

  • 코드 라인 6~7에서 현재 시간(jiffies)이 갱신 주기+10틱보다 뒤에 있는 경우 즉, 갱신이 밀려 있는 경우
  • 코드 라인 11~12에서 몇 번 밀려 있는지 횟수를 산출한다.
    • 예) jiffies와ㅓ calc_load_update의 차이가 21초인 경우 21/5+1 = 정수 5가된다.
  • 코드 라인 14~15에서 현재 active 태스크 수를 읽어와서 그 값을 11비트 정밀도를 가진 이진화 정수로 바꾼다.
  • 코드 라인 17~21에서 avenrun[] 값을 갱신하고 갱신 주기도 update한다.
  • 코드 라인 32에서 calc_load_idx를 증가시켜 새로운 인덱스를 사용하도록 한다.

 

calc_load_n()

kernel/sched/loadavg.c

/*
 * a1 = a0 * e + a * (1 - e)
 *
 * a2 = a1 * e + a * (1 - e)
 *    = (a0 * e + a * (1 - e)) * e + a * (1 - e)
 *    = a0 * e^2 + a * (1 - e) * (1 + e)
 *
 * a3 = a2 * e + a * (1 - e)
 *    = (a0 * e^2 + a * (1 - e) * (1 + e)) * e + a * (1 - e)
 *    = a0 * e^3 + a * (1 - e) * (1 + e + e^2)
 *
 *  ...
 *
 * an = a0 * e^n + a * (1 - e) * (1 + e + ... + e^n-1) [1]
 *    = a0 * e^n + a * (1 - e) * (1 - e^n)/(1 - e)
 *    = a0 * e^n + a * (1 - e^n)
 *
 * [1] application of the geometric series:
 *
 *              n         1 - x^(n+1)
 *     S_n := \Sum x^i = -------------
 *             i=0          1 - x
 */
static unsigned long
calc_load_n(unsigned long load, unsigned long exp,
            unsigned long active, unsigned int n)
{

        return calc_load(load, fixed_power_int(exp, FSHIFT, n), active);
}

기존 load 값에 새 active 값이 exp(간격)으로 FSHIFT(2048) 정확도로 산출한다.

  • 예) exp=1884(1분 주기), FSHIFT=2048(정확도), load=1024, active=2048, n=4
    • = load * (exp / FSHIFT) + (active * (FSHIFT – exp) / 2048) + 0.5 를 4번 수행
    • = load * (exp^n / 2048^n) + (active * (1 – (exp^n / 2048^n)) + 0.5와 동일
    • = 1024 * (1884^4 / 2048^4) + (active * (1 – (1884^4) / 2048^4) + 0.5
    • = 1896

 

active 태스크 수가 변함 없이 n 번의 횟수만큼 계산되는 결과와 동일하다.

 

fixed_power_int()

kernel/sched/loadavg.c

/**
 * fixed_power_int - compute: x^n, in O(log n) time
 *
 * @x:         base of the power
 * @frac_bits: fractional bits of @x
 * @n:         power to raise @x to.
 *
 * By exploiting the relation between the definition of the natural power
 * function: x^n := x*x*...*x (x multiplied by itself for n times), and
 * the binary encoding of numbers used by computers: n := \Sum n_i * 2^i,
 * (where: n_i \elem {0, 1}, the binary vector representing n),
 * we find: x^n := x^(\Sum n_i * 2^i) := \Prod x^(n_i * 2^i), which is
 * of course trivially computable in O(log_2 n), the length of our binary
 * vector.
 */
static unsigned long
fixed_power_int(unsigned long x, unsigned int frac_bits, unsigned int n)
{
        unsigned long result = 1UL << frac_bits;

        if (n) for (;;) {
                if (n & 1) {
                        result *= x;
                        result += 1UL << (frac_bits - 1);
                        result >>= frac_bits;
                }
                n >>= 1;
                if (!n)
                        break;
                x *= x;
                x += 1UL << (frac_bits - 1);
                x >>= frac_bits;
        }

        return result;
}

(x^n) / (2^frac_bits)^(n-1) 를 산출한다. 산출 시 n번 만큼 반복하지 않고 O(log2 n)번으로 산출되는 것이 특징으로 n 값이 클 때 효과적이다.

  • 예) x=1884 (EXP_1), frac_bits=11 (11비트 정밀도), n=5
    • = (1884 * 1884 * 1884 * 1884 * 1884) / (2048 * 2048 * 2048 * 2048) = 1,349

 

참고