关于腾讯云:Linux-内核调度器源码分析-初始化

32次阅读

共计 13855 个字符,预计需要花费 35 分钟才能阅读完成。

导语

上篇系列文 混部之殇 - 论云原生资源隔离技术之 CPU 隔离 (一) 介绍了云原生混部场景中 CPU 资源隔离核心技术:内核调度器,本系列文章《Linux 内核调度器源码剖析》将从源码的角度分析内核调度的具体原理和实现,咱们将以 Linux kernel 5.4 版本(TencentOS Server3 默认内核版本) 为对象,从调度器子系统的初始化代码开始,剖析 Linux 内核调度器的设计与实现。

调度器 (Scheduler) 子系统是内核的外围子系统之一,负责零碎内 CPU 资源的正当调配,须要能解决纷繁复杂的不同类型工作的调度需要,还须要能解决各种简单的并发竞争环境,同时还须要兼顾整体吞吐性能和实时性要求(自身是一对矛盾体),其设计与实现都极具挑战。

为了可能了解 Linux 调度器的设计与实现,咱们将以 Linux kernel 5.4 版本 (TencentOS Server3 默认内核版本) 为对象,从调度器子系统的初始化代码开始,剖析 Linux 内核调度器的设计与实现。

本 (系列) 文通过剖析 Linux 调度器 (次要针对 CFS) 的设计与实现,心愿可能让读者理解:

  • 调度器的基本概念
  • 调度器的初始化(包含调度域相干的种种)
  • 过程的创立、执行与销毁
  • 过程切换原理与实现
  • CFS 过程调度策略(单核)
  • 如何在全局零碎的调度上保障 CPU 资源的正当应用
  • 如何均衡 CPU 缓存热度与 CPU 负载之间的关系
  • 很 special 的调度器 features 剖析

调度器的基本概念

在剖析调度器的相干代码之前,须要先理解一下调度器波及的外围数据 (构造) 以及它们的作用

运行队列(rq)

内核会为每个 CPU 创立一个运行队列,零碎中的就绪态 (处于 Running 状态的) 过程 (task) 都会被组织到内核运行队列上,而后依据相应的策略,调度运行队列上的过程到 CPU 上执行。

调度类(sched_class)

内核将调度策略(sched_class)进行了高度的形象,造成调度类 (sched_class)。通过调度类能够将调度器的公共代码(机制) 和具体不同调度类提供的调度策略进行充沛解耦,是典型的 OO(面向对象)的思维。通过这样的设计,能够让内核调度器极具扩展性,开发者通过很少的代码 (根本不需改变公共代码) 就能够减少一个新的调度类,从而实现一种全新的调度器(类),比方,deadline 调度类就是 3.x 中新增的,从代码层面看只是减少了 dl_sched_class 这个构造体的相干实现函数,就很不便的增加了一个新的实时调度类型。

目前的 5.4 内核,有 5 种调度类,优先级从高到底散布如下:

stop_sched_class:

优先级最高的调度类,它与 idle_sched_class 一样,是一个专用的调度类型(除了 migration 线程之外,其余的 task 都是不能或者说不应该被设置为 stop 调度类)。该调度类专用于实现相似 active balance 或 stop machine 等依赖于 migration 线程执行的“紧急”工作。

dl_sched_class:

deadline 调度类的优先级仅次于 stop 调度类,它是一种基于 EDL 算法实现的实时调度器(或者说调度策略)。

rt_sched_class:

rt 调度类的优先级要低于 dl 调度类,是一种基于优先级实现的实时调度器。

fair_sched_class:

CFS 调度器的优先级要低于下面的三个调度类,它是基于偏心调度思维而设计的调度类型,是 Linux 内核的默认调度类。

idle_sched_class:

idle 调度类型是 swapper 线程,次要是让 swapper 线程接管 CPU,通过 cpuidle/nohz 等框架让 CPU 进入节能状态。

调度域(sched_domain)

调度域是在 2.6 里引入内核的,通过多级调度域引入,可能让调度器更好的适应硬件的物理个性(调度域能够更好的适配 CPU 多级缓存以及 NUMA 物理个性对负载平衡所带来的挑战),实现更好的调度性能(sched_domain 是为 CFS 调度类负载平衡而开发的机制)。

调度组(sched_group)

调度组是与调度域一起被引入内核的,它会与调度域一起配合,帮助 CFS 调度器实现多核间的负载平衡。

根域(root_domain)

根域次要是负责实时调度类(包含 dl 和 rt 调度类)负载平衡而设计的数据结构,帮助 dl 和 rt 调度类实现实时工作的正当调度。在没有用 isolate 或者 cpuset cgroup 批改调度域的时候,那么默认状况下所有的 CPU 都会处于同一个根域。

组调度(group_sched)

为了可能对系统里的资源进行更精密的管制,内核引入了 cgroup 机制来进行资源管制。而 group_sched 就是 cpu cgroup 的底层实现机制,通过 cpu cgroup 咱们能够将一些过程设置为一个 cgroup,并且通过 cpu cgroup 的管制接口配置相应的带宽和 share 等参数,这样咱们就能够依照 group 为单位,对 CPU 资源进行精密的管制。

调度器初始化(sched_init)

上面进入正题,开始剖析内核调度器的初始化流程,心愿能通过这里的剖析,让大家理解:

1、运行队列是如何被初始化的

2、组调度是如何与 rq 关联起来的(只有关联之后能力通过 group_sched 进行组调度)

3、CFS 软中断 SCHED_SOFTIRQ 注册

调度初始化(sched_init)

start_kernel

​ |—-setup_arch

​ |—-build_all_zonelists

​ |—-mm_init

​ |—-sched_init 调度初始化

调度初始化位于 start_kernel 绝对靠后的地位,这个时候内存初始化曾经实现,所以能够看到 sched_init 外面曾经能够调用 kzmalloc 等内存申请函数了。

sched_init 须要为每个 CPU 初始化运行队列(rq)、dl/rt 的全局默认带宽、各个调度类的运行队列以及 CFS 软中断注册等工作。

接下来咱们看看 sched_init 的具体实现(省略了局部代码):

void __init sched_init(void)
{
    unsigned long ptr = 0;
    int i;
 
    /*
     * 初始化全局默认的 rt 和 dl 的 CPU 带宽管制数据结构
     *
     * 这里的 rt_bandwidth 和 dl_bandwidth 是用来管制全局的 DL 和 RT 的应用带宽,避免实时过程
     * CPU 应用过多,从而导致一般的 CFS 过程呈现饥饿的状况
     */
    init_rt_bandwidth(&def_rt_bandwidth, global_rt_period(), global_rt_runtime());
    init_dl_bandwidth(&def_dl_bandwidth, global_rt_period(), global_rt_runtime());
 
#ifdef CONFIG_SMP
    /*
     * 初始化默认的根域
     *
     * 根域是 dl/rt 等实时过程做全局平衡的重要数据结构,以 rt 为例
     * root_domain->cpupri 是这个根域范畴内每个 CPU 上运行的 RT 工作的最高优先级,以及
     * 各个优先级工作在 CPU 上的散布状况,通过 cpupri 的数据,那么在 rt enqueue/dequeue
     * 的时候,rt 调度器就能够依据这个 rt 工作散布状况来保障高优先级的工作失去优先
     * 运行
     */
    init_defrootdomain();
#endif
 
#ifdef CONFIG_RT_GROUP_SCHED
    /*
     * 如果内核反对 rt 组调度(RT_GROUP_SCHED), 那么对 RT 工作的带宽管制将能够用 cgroup
     * 的粒度来管制每个 group 里 rt 工作的 CPU 带宽应用状况
     *
     * RT_GROUP_SCHED 能够让 rt 工作以 cpu cgroup 的模式来整体管制带宽
     * 这样能够为 RT 带宽管制带来更大的灵活性(没有 RT_GROUP_SCHED 的时候,只能管制 RT 的全局
     * 带宽应用,不能通过指定 group 的模式管制局部 RT 过程带宽)
     */
    init_rt_bandwidth(&root_task_group.rt_bandwidth,
            global_rt_period(), global_rt_runtime());
#endif /* CONFIG_RT_GROUP_SCHED */
 
    /* 为每个 CPU 初始化它的运行队列 */
    for_each_possible_cpu(i) {
        struct rq *rq;
 
        rq = cpu_rq(i);
        raw_spin_lock_init(&rq->lock);
        /*
         * 初始化 rq 上 cfs/rt/dl 的运行队列
         * 每个调度类型在 rq 上都有各自的运行队列,每个调度类都是各自治理本人的过程
         * 在 pick_next_task()的时候,内核依据调度类优先级的程序,从高到底抉择工作
         * 这样就保障了高优先级调度类工作会优先失去运行
         *
         * stop 和 idle 是非凡的调度类型,是为专门的目标而设计的调度类,并不容许用户
         * 创立相应类型的过程,所以内核也没有在 rq 里设计对应的运行队列
         */
        init_cfs_rq(&rq->cfs);
        init_rt_rq(&rq->rt);
        init_dl_rq(&rq->dl);
#ifdef CONFIG_FAIR_GROUP_SCHED
        /*
         * CFS 的组调度(group_sched),能够通过 cpu cgroup 来对 CFS 进行进行管制
         * 能够通过 cpu.shares 来提供 group 之间的 CPU 比例控制(让不同的 cgroup 依照对应
         * 的比例来分享 CPU),也能够通过 cpu.cfs_quota_us 来进行配额设定(与 RT 的
         * 带宽管制相似)。CFS group_sched 带宽管制是容器实现的根底底层技术之一
         *
         * root_task_group 是默认的根 task_group,其余的 cpu cgroup 都会以它做为
         * parent 或者 ancestor。这里的初始化将 root_task_group 与 rq 的 cfs 运行队列
         * 关联起来,这里做的很有意思,间接将 root_task_group->cfs_rq[cpu] = &rq->cfs
         * 这样在 cpu cgroup 根下的过程或者 cgroup tg 的 sched_entity 会间接退出到 rq->cfs
         * 队列里,能够缩小一层查找开销。*/
        root_task_group.shares = ROOT_TASK_GROUP_LOAD;
        INIT_LIST_HEAD(&rq->leaf_cfs_rq_list);
        rq->tmp_alone_branch = &rq->leaf_cfs_rq_list;
        init_cfs_bandwidth(&root_task_group.cfs_bandwidth);
        init_tg_cfs_entry(&root_task_group, &rq->cfs, NULL, i, NULL);
#endif /* CONFIG_FAIR_GROUP_SCHED */
 
        rq->rt.rt_runtime = def_rt_bandwidth.rt_runtime;
#ifdef CONFIG_RT_GROUP_SCHED
        /* 初始化 rq 上的 rt 运行队列,与下面的 CFS 的组调度初始化相似 */
        init_tg_rt_entry(&root_task_group, &rq->rt, NULL, i, NULL);
#endif
 
#ifdef CONFIG_SMP
        /*
         * 这里将 rq 与默认的 def_root_domain 进行关联,如果是 SMP 零碎,那么前面
         * 在 sched_init_smp 的时候,内核会创立新的 root_domain,而后替换这里
         * def_root_domain
         */
        rq_attach_root(rq, &def_root_domain);
#endif /* CONFIG_SMP */
    }
 
    /*
     * 注册 CFS 的 SCHED_SOFTIRQ 软中断服务函数
     * 这个软中断住要是周期性负载平衡以及 nohz idle load balance 而筹备的
     */
    init_sched_fair_class();
 
    scheduler_running = 1;
}

多核调度初始化(sched_init_smp)

start_kernel

​ |—-rest_init

​ |—-kernel_init

​ |—-kernel_init_freeable

​ |—-smp_init

​ |—-sched_init_smp

​ |—- sched_init_numa

​ |—- sched_init_domains

​ |—- build_sched_domains

多核调度初始化次要是实现调度域 / 调度组的初始化(当然根域也会做,但相对而言,根域的初始化会比较简单)。

Linux 是一个能够跑在多种芯片架构,多种内存架构(UMA/NUMA)上运行的操作系统,所以 Linu x 须要可能适配多种物理构造,所以它的调度域设计与实现也是绝对比较复杂的。

调度域实现原理

在讲具体的调度域初始化代码之前,咱们须要先理解调度域与物理拓扑构造之间的关系(因为调度域的设计是与物理拓扑构造非亲非故的,如果不了解物理拓扑构造,那么就没有方法真正了解调度域的实现)

CPU 的物理拓扑图

咱们假如一个计算机系统(与 intel 芯片相似,但放大 CPU 外围数,以不便示意):

双 socket 的计算机系统,每个 socket 都是 2 核 4 线程组成,那么这个计算机系统就应该是一个 4 核 8 线程的 NUMA 零碎(下面只是 intel 的物理拓扑构造,而像 AMD ZEN 架构采纳了 chiplet 的设计,它在 MC 与 NUMA 域之间会多一层 DIE 域)。

第一层(SMT 域):

如上图的 CORE0,2 个超线程形成了 SMT 域。对于 intel cpu 而言,超线程共享了 L1 与 L2(甚至连 store buffe 都在肯定水平上共享),所以 SMT 域之间相互迁徙是没有任何缓存热度损失的

第二层(MC 域):

如上图 CORE0 与 CORE1,他们位于同一个 SOCKET,属于 MC 域。对于 intel cpu 而言,他们个别共享 LLC(个别是 L3),在这个域里过程迁徙尽管会失去 L1 与 L2 的热度,但 L3 的缓存热度还是能够放弃的

第三层(NUMA 域):

如上图的 SOCKET0 和 SOCKET1,它们之间的过程迁徙会导致所有缓存热度的损失,会有较大的开销,所以 NUMA 域的迁徙须要绝对的审慎。

正是因为这样的硬件物理个性(不同层级的缓存热度、NUMA 拜访提早等硬件因素),所以内核形象了 sched_domain 和 sched_group 来示意这样的物理个性。在做负载平衡的时候,依据相应的调度域个性,做不同的调度策略(例如负载平衡的频率、不均衡的因子以及唤醒选核逻辑等),从而在 CPU 负载与缓存亲和性上做更好的均衡。

调度域具体实现

接下来咱们能够看看内核如何在下面的物理拓扑构造上建设调度域与调度组的

内核会依据物理拓扑构造建设对应档次的调度域,而后在每层调度域上再建设相应的调度组。调度域在做负载平衡,是在对应档次的调度域里找到负载最重的 busiest sg(sched_group),而后再判断 buiest sg 与 local sg(但前 CPU 所在的调度组)的负载是否不均。如果存在负载不均的状况,则会从 buiest sg 里抉择 buisest cpu,而后进行 2 个 CPU 间的负载平衡。

SMT 域是最底层的调度域,能够看到每个超线程对就是一个 smt domain。smt domain 里有 2 个 sched_group,而每个 sched_group 则只会有一个 CPU。所以 smt 域的负载平衡就是执行超线程间的过程迁徙,这个负载平衡的工夫最短,条件最宽松。

而对于不存在超线程的架构(或者说芯片没有开启超线程),那么最底层域就是 MC 域(这个时候就只有 2 层域,MC 与 NUMA)。这样 MC 域里每个 CORE 都是一个 sched_group,内核在调度的时候也能够很好的适应这样的场景。

MC 域则是 socket 上 CPU 所有的 CPU 组成,而其中每个 sg 则为下级 smt domain 的所有 CPU 形成。所以对于上图而言,MC 的 sg 则由 2 个 CPU 组成。内核在 MC 域这样设计,能够让 CFS 调度类在唤醒负载平衡以及闲暇负载平衡时,要求 MC 域的 sg 间须要平衡。

这个设计对于超线程来说很重要,咱们在一些理论的业务里也能够察看到这样的状况。例如,咱们有一项编解码的业务,发现它在某些虚拟机里的测试数据较好,而在某些虚拟机里的测试数据较差。通过剖析后发现,这是因为是否往虚拟机透传超线程信息导致的。当咱们向虚拟机透传超线程信息后,虚构机会造成 2 层调度域(SMT 与 MC 域),而在唤醒负载平衡的时候,CFS 会偏向于将业务调度到闲暇的 sg 上(即闲暇的物理 CORE,而不是闲暇的 CPU),这个时候业务在 CPU 利用率不高(没有超过 40%)的时候,能够更加充沛的利用物理 CORE 的性能(还是老问题,一个物理 CORE 上的超线程对,它们同时运行 CPU 消耗型业务时,所取得的性能增益只相当于单线程 1.2 倍左右。),从而取得较好的性能增益。而如果没有透传超线程信息,那么虚拟机只有一层物理拓扑构造(MC 域),那么因为业务很可能被调度通过一个物理 CORE 的超线程对上,这样会导致系统无奈充分利用物理 CORE 的性能,从而导致业务性能偏低。

NUMA 域则是由零碎里的所有 CPU 形成,SOCKET 上的所有 CPU 形成一个 sg,上图的 NUMA 域由 2 个 sg 形成。NUMA 的 sg 之间须要有较大的不均衡时(并且这里的不均衡是 sg 级别的,即要 sg 上所有 CPU 负载总和与另外一个 sg 不均衡),能力进行跨 NUMA 的过程迁徙(因为跨 NUMA 的迁徙会导致 L1 L2 L3 的所有缓存热度损失,以及可能引发更多的跨 NUMA 内存拜访,所以须要小心应答)。

从下面的介绍能够看到,通过 sched_domain 与 sched_group 的配合,内核可能适配各种物理拓扑构造(是否开启超线程、是否开启应用 NUMA),高效的应用 CPU 资源。

smp_init

/*
 * Called by boot processor to activate the rest.
 *
 * 在 SMP 架构里,BSP 须要将其余的非 boot cp 全副 bring up
 */
void __init smp_init(void)
{
    int num_nodes, num_cpus;
    unsigned int cpu;
 
    /* 为每个 CPU 创立其 idle thread */
    idle_threads_init();
    /* 向内核注册 cpuhp 线程 */
    cpuhp_threads_init();
 
    pr_info("Bringing up secondary CPUs ...\n");
 
    /*
     * FIXME: This should be done in userspace --RR
     *
     * 如果 CPU 没有 online,则用 cpu_up 将其 bring up
     */
    for_each_present_cpu(cpu) {if (num_online_cpus() >= setup_max_cpus)
            break;
        if (!cpu_online(cpu))
            cpu_up(cpu);
    }
     
    .............
}

在真正开始 sched_init_smp 调度域初始化之前,须要先 bring up 所有非 boot cpu,保障这些 CPU 处于 ready 状态,而后能力开始多核调度域的初始化。

sched_init_smp

那这里咱们来看看多核调度初始化具体的代码实现(如果没有配置 CONFIG_SMP,那么则不会执行到这里的相干实现)

sched_init_numa

sched_init_numa() 是用来检测零碎里是否为 NUMA,如果是的则须要动静增加 NUMA 域。

/*
 * Topology list, bottom-up.
 *
 * Linux 默认的物理拓扑构造
 *
 * 这里只有三级物理拓扑构造,NUMA 域是在 sched_init_numa()自动检测的
 * 如果存在 NUMA 域,则会增加对应的 NUMA 调度域
 *
 * 注:这里默认的 default_topology 调度域可能会存在一些问题,例如
 * 有的平台不存在 DIE 域(intel 平台),那么就可能呈现 LLC 与 DIE 域重叠的状况
 * 所以内核会在调度域建设好后,在 cpu_attach_domain()里扫描所有调度
 * 如果存在调度重叠的状况,则会 destroy_sched_domain 对应的重叠调度域
 */
static struct sched_domain_topology_level default_topology[] = {
#ifdef CONFIG_SCHED_SMT
    {cpu_smt_mask, cpu_smt_flags, SD_INIT_NAME(SMT) },
#endif
#ifdef CONFIG_SCHED_MC
    {cpu_coregroup_mask, cpu_core_flags, SD_INIT_NAME(MC) },
#endif
    {cpu_cpu_mask, SD_INIT_NAME(DIE) },
    {NULL,},
};

Linux 默认的物理拓扑构造

/*
 * NUMA 调度域初始化(依据硬件信息创立新的 sched_domain_topology 物理拓扑构造)
 *
 * 内核在默认状况下并不会被动增加 NUMA topology,须要依据配置(如果开启了 NUMA)
 * 如果开启了 NUMA,这里就要依据硬件拓扑信息来判断是否须要增加
 * sched_domain_topology_level 域(只有增加了这个域之后,内核才会在前面初始化
 * sched_domain 的时候创立 NUMA DOMAIN)
 */
void sched_init_numa(void)
{
    ...................
    /*
     * 这里会依据 distance 查看是否存在 NUMA 域(甚至存在多级 NUMA 域),而后依据
     * 状况将其更新到物理拓扑构造里。前面的建设调度域的时候,就会这个新的
     * 物理拓扑构造来建设新的调度域
     */
    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();}

检测零碎的物理拓扑构造,如果存在 NUMA 域则须要将其加到 sched_domain_topology 里,前面就会依据 sched_domain_topology 这个物理拓扑构造来建设相应的调度域。

sched_init_domains

上面接着剖析 sched_init_domains 这个调度域建设函数

/*
 * 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;
    /*
     * doms_cur[0] 示意调度域须要笼罩的 cpumask
     *
     * 如果零碎里用 isolcpus= 对某些 CPU 进行了隔离,那么这些 CPU 是不会退出到调度
     * 域外面,即这些 CPU 不会参于到负载平衡(这里的负载平衡包含 DL/RT 以及 CFS)。* 这里用 cpu_map & housekeeping_cpumask(HK_FLAG_DOMAIN) 的形式将 isolate
     * cpu 去除掉,从而在保障建设的调度域里不蕴含 isolate cpu
     */
    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;
}
/*
 * 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;
 
    /*
     * Linux 里的绝大部分过程都为 CFS 调度类,所以 CFS 里的 sched_domain 将会被频繁
     * 的拜访与批改(例如 nohz_idle 以及 sched_domain 里的各种统计),所以 sched_domain
     * 的设计须要优先思考到效率问题,于是内核采纳了 percpu 的形式来实现 sched_domain
     * CPU 间的每级 sd 都是独立申请的 percpu 变量,这样能够利用 percpu 的个性解决它们
     * 间的并发竞争问题(1、不须要锁爱护 2、没有 cachline 伪共享)
     */
    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:
     *
     * 这里会遍历 cpu_map 里所有 CPU,为这些 CPU 创立与物理拓扑构造对应(* for_each_sd_topology)的多级调度域。*
     * 在调度域建设的时候,会通过 tl->mask(cpu)取得 cpu 在该级调度域对应
     * 的 span(即 cpu 与其余对应的 cpu 组成了这个调度域),在同一个调度域里
     * 的 CPU 对应的 sd 在刚开始的时候会被初始化成一样的(包含 sd->pan、* sd->imbalance_pct 以及 sd->flags 等参数)。*/
    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;
        }
    }
 
    /*
     * Build the groups for the domains
     *
     * 创立调度组
     *
     * 咱们能够从 2 个调度域的实现看到 sched_group 的作用
     * 1、NUMA 域 2、LLC 域
     *
     * numa sched_domain->span 会蕴含 NUMA 域上所有的 CPU,当须要进行平衡的时候
     * NUMA 域不应该以 cpu 为单位,而是应该以 socket 为单位,即只有 socket1 与 socket2
     * 极度不均衡的时候才在这两个 SOCKET 间迁徙 CPU。如果用 sched_domain 来实现这个
     * 形象则会导致灵活性不够(前面的 MC 域能够看到),所以内核会以 sched_group 来
     * 示意一个 cpu 汇合,每个 socket 属于一个 sched_group。当这两个 sched_group 不均衡
     * 的时候才会容许迁徙
     *
     * MC 域也是相似的,CPU 可能是超线程,而超线程的性能与物理核不是对等的。一对
     * 超线程大略等于 1.2 倍于物理核的性能。所以在调度的时候,咱们须要思考超线程
     * 对之间的均衡性,即先要满足 CPU 间平衡,而后才是 CPU 内的超线程平衡。这个时候
     * 用 sched_group 来做形象,一个 sched_group 示意一个物理 CPU(2 个超线程),这个时候
     * LLC 保障 CPU 间的平衡,从而防止一种极其状况:超线程间平衡,然而物理核上不平衡
     * 的状况,同时能够保障调度选核的时候,内核会优先实现物理线程,只有物理线程
     * 用完之后再思考应用另外的超线程,让零碎能够更充沛的利用 CPU 算力
     */
    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
     *
     * sched_group_capacity 是用来示意 sg 可应用的 CPU 算力
     *
     * sched_group_capacity 是思考了每个 CPU 自身的算力不同(最高主频设置不同、* ARM 的大小核等等)、去除掉 RT 过程所应用的 CPU(sg 是为 CFS 筹备的,所以须要
     * 去掉 CPU 上 DL/RT 过程等所应用的 CPU 算力)等因素之后,留给 CFS sg 的可用算力(因为
     * 在负载平衡的时候,不仅应该思考到 CPU 上的负载,还应该思考这个 sg 上的 CFS
     * 可用算力。如果这个 sg 上过程较少,然而 sched_group_capacity 也较小,也是
     * 不应该迁徙过程到这个 sg 上的)
     */
    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();
    /*
     * 将每个 CPU 的 rq 与 rd(root_domain)进行绑定,并且会查看 sd 是否有重叠
     * 如果是的则须要用 destroy_sched_domain()将其去掉(所以咱们能够看到
     * intel 的服务器是只有 3 层调度域,DIE 域其实与 LLC 域重叠了,所以在这里
     * 会被去掉)
     */
    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;
}

到目前为止,咱们曾经将内核的调度域构建起来了,CFS 能够利用 sched_domain 来实现多核间的负载平衡了。

结语

本文次要介绍了内核调度器的基本概念,并通过剖析 5.4 内核中调度器的初始化代码,介绍了调度域、调度组等基本概念的具体落地形式。整体上,5.4 内核相比 3.x 内核,在调度器初始化逻辑,以及调度器相干的根本设计 (概念 / 要害构造) 上没有实质的变动,也从侧面印证了内核调度器设计的“稳固”和“优雅”。

预报:本系列下一篇文章将聚焦 Linux 内核调度器的基本原理和根底框架构及相干源码,敬请期待。

【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!

正文完
 0