<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[] 배열의 초기화 및 파싱 함수는 다음과 같다.
- arm64_numa_init() -> numa_init() -> of_numa_init() -> of_numa_parse_distance_map()
- 참고: NUMA -1- (ARM64 초기화) | 문c
 
 
- 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이 현재 도메인내에 포함될 수 없는 경우 더 하위 도메인을 선택하게 하도록 수정되었다.
- 참고: sched/topology: fix the issue groups don’t span domain->span for NUMA diameter > 2 (v5.13-rc1, 2021)
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 트래킹이 적용되면서 변경되었다.
- 참고:
- sched/core: Use PELT for scale_rt_capacity() (2018, v4.19-rc1)
- sched/core: Remove the rt_avg code (2018, v4.19-rc1)
 
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의 런큐를 루트 도메인에 연결한다.
- 참고: sched_init() | 문c
 
- 코드 라인 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%로 줄였다.
- 참고: sched/fair: Reduce minimal imbalance threshold (2020, v5.10-rc1)
 
 
- 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 -1- (Basic) | 문c
- Scheduler -2- (Global Cpu Load) | 문c
- Scheduler -3- (PELT) | 문c
- Scheduler -4- (Group Scheduling) | 문c
- Scheduler -5- (Scheduler Core) | 문c
- Scheduler -6- (CFS Scheduler) | 문c
- Scheduler -7- (Preemption & Context Switch) | 문c
- Scheduler -8- (CFS Bandwidth) | 문c
- Scheduler -9- (RT Scheduler) | 문c
- Scheduler -10- (Deadline Scheduler) | 문c
- Scheduler -11- (Stop Scheduler) | 문c
- Scheduler -12- (Idle Scheduler) | 문c
- Scheduler -13- (Scheduling Domain 1) | 문c
- Scheduler -14- (Scheduling Domain 2) | 문c – 현재 글
- Scheduler -15- (Load Balance 1) | 문c
- Scheduler -16- (Load Balance 2) | 문c
- Scheduler -17- (Load Balance 3 NUMA) | 문c
- Scheduler -18- (Load Balance 4 EAS) | 문c
- Scheduler -19- (초기화) | 문c
- PID 관리 | 문c
- do_fork() | 문c
- cpu_startup_entry() | 문c
- 런큐 로드 평균(cpu_load[]) – v4.0 | 문c
- PELT(Per-Entity Load Tracking) – v4.0 | 문c
- Scheding Domains | LWN.net
- [CFS] scheduling domain | NZCV
- sched-domains.txt | Kernel.org – 번역
- Overview of the Current Approaches to Enhance the Linux Scheduler | IBM





















