SMP负载均衡CPU域初始化之sched_group拓扑关系建立

本篇文章主要讲解4.9和4.14 Linux Kernel中sched_group拓扑关系建立过程。
在Linux Kernel中build_sched_domians()方法是真正开始建立调度域拓扑关系的函数,其中包括建立sched_group的拓扑关系:

static int build_sched_domains(const struct cpumask *cpu_map,
                               struct sched_domain_attr *attr)
{
	......
	/* 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;
                        }
                }
        }
      ......
}

该段代码的含义是:对于每个包含在cpu_map(即cpu_active_mask)中的CPU,遍历其在每个SDTL层级中的调度域,然后调用build_overlap_sched_groups()或build_sched_groups()方法建立该CPU在该SDTL层级下的sched_group的拓扑关系。假设SDTL只包含MC、DIE两个层级,则sched_group建立的拓扑关系如下图(本图来自《奔跑吧Linux内核》):
SMP负载均衡CPU域初始化之sched_group拓扑关系建立
下面我们结合着sched_group的拓扑关系图主要分析build_sched_groups()方法的实现,首先分析4.9 Linux Kernel中的实现:

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

        get_group(cpu, sdd, &sd->groups);----------------------------3
        atomic_inc(&sd->groups->ref);

        if (cpu != cpumask_first(span))------------------------------4
                return 0;

        lockdep_assert_held(&sched_domains_mutex);
        covered = sched_domains_tmpmask;

        cpumask_clear(covered);-------------------------------------5

		for_each_cpu(i, span) {
                struct sched_group *sg;
                int group, j;

                if (cpumask_test_cpu(i, covered))-------------------6
                        continue;

                group = get_group(i, sdd, &sg);---------------------7
                cpumask_setall(sched_group_mask(sg));

                for_each_cpu(j, span) {
                        if (get_group(j, sdd, NULL) != group)--------8
                                continue;

                        cpumask_set_cpu(j, covered);-----------------9
                        cpumask_set_cpu(j, sched_group_cpus(sg));----10
                }

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

        return 0;
}

下面具体分析该函数:
1、获取当前sched_domain对应的SDTL层级中的struct sd_data数据结构;
2、获取当前sched_domain中包含的兄弟CPU的bitmap;
3、get_group()方法获取当前CPU对应的sched_group并存放在sd->groups指针中,我们来看一下get_group()方法的实现;

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

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

        if (sg) {
                *sg = *per_cpu_ptr(sdd->sg, cpu);
                (*sg)->sgc = *per_cpu_ptr(sdd->sgc, cpu);
                atomic_set(&(*sg)->sgc->ref, 1); /* for claim_allocations */
        }

        return cpu;
}

在build_sched_groups()方法中有3、7、8三个地方调用了get_group()方法,该方法是build_sched_groups()方法实现的核心方法。
get_count()的返回值是CPU,但是如果child存在,则返回的是child sched_domain span中的第一个CPU;如果child不存在,则返回的是当前传进来的CPU。
那如何理解sched_domain之间的父子关系呢?
上文中我们已经假设当前SDTL只包含MC、DIE两个层级,那么MC层级的sched_domain是DIE层级的sched_domain的子sched_domain;那么DIE层级的sched_domain是MC层级的sched_domain的父sched_domain。因此如果当前我们处理的是MC层级的sched_domain(即get_group()方法中struct sched_domain *sd = *per_cpu_ptr(sdd->sd, cpu)得到的是MC层级的sched_domain),那么child不存在,直接返回当前传进来的CPU;如果当前我们处理的是DIE层级的sched_domain(即get_group()方法中struct sched_domain *sd = *per_cpu_ptr(sdd->sd, cpu)得到的是DIE层级的sched_domain),那么child存在,则返回的是child sched_domain span中的第一个CPU(即对应的MC层级中sched_domain span中的第一个CPU)。

下面接着分析build_sched_groups()方法:
4、只处理该sched_domain中第一个CPU的情况,因为没必要重复计算其他兄弟CPU。为什么呢?
我们看到build_sched_groups()方法中有一个for_each_cpu(i, span)循环,在for_each_cpu(i, span)循环中有如下代码:

static int build_sched_groups(struct sched_domain *sd, int cpu)
{
	......
	for_each_cpu(i, span) {
		......
		if (!first)
        	first = sg;
        if (last)
            last->next = sg;
        last = sg;
		......
	}
	last->next = first;
	......
}

通过这段代码,我们可以清楚的发现:我们只需要在build_sched_groups()方法中处理该sched_domain中第一个CPU的情况,通过for_each_cpu(i, span)循环,就可以将兄弟CPU的sched_group通过指针串联起来,从而形成兄弟CPU sched_group的拓扑关系,如下图(本图来自《奔跑吧Linux内核》):
SMP负载均衡CPU域初始化之sched_group拓扑关系建立
通过该拓扑关系图,我们可以看到兄弟sched_domain的groups指针都指向同一个sched_group链表,既然在build_sched_groups()方法中只处理当前sched_domain兄弟CPU中的第一个CPU的情况,那么其他兄弟CPU sched_domain中的group指针是怎样指向sched_group链表的呢?
这其实是在build_sched_groups()方法中的3代码中实现的:

static int build_sched_groups(struct sched_domain *sd, int cpu)
{
	......
	get_group(cpu, sdd, &sd->groups);----------------------------3
    atomic_inc(&sd->groups->ref);

     if (cpu != cpumask_first(span))------------------------------4
     	return 0;
	......	
}

我们可以看到在4之前调用了3(即get_group(cpu, sdd, &sd->groups)),该处调用get_group()目的就是为了让兄弟CPU sched_domain中的group指针指向sched_group链表。
5、clear一下covered这个cpumask;
6、7、8、9、10这几处的代码配合使用。

static int build_sched_groups(struct sched_domain *sd, int cpu)
{
       ......
		for_each_cpu(i, span) {
                struct sched_group *sg;
                int group, j;

                if (cpumask_test_cpu(i, covered))-------------------6
                        continue;

                group = get_group(i, sdd, &sg);---------------------7
                cpumask_setall(sched_group_mask(sg));

                for_each_cpu(j, span) {
                        if (get_group(j, sdd, NULL) != group)--------8
                                continue;

                        cpumask_set_cpu(j, covered);-----------------9
                        cpumask_set_cpu(j, sched_group_cpus(sg));----10
                }
		......
        return 0;
}

7处的返回值:如果是MC层级,则返回的是i的值;如果是DIE层级,则返回的是child sched_domain span中的第一个CPU。8处代码的含义需要结合图3.10来分析(本图来自《奔跑吧Linux内核》):
SMP负载均衡CPU域初始化之sched_group拓扑关系建立
假设现在是MC层级,以domain_mc_0为例分析8处的代码。首先7处代码的返回值是CPU0, 由于MC层级没有child层级,因此8处代码每次循环的返回值都不一样(即只有两次循环,返回值分别是cpu0, cpu1),因此当返回值为cpu0时,会执行的9和10,当返回值为cpu1时,不会执行9和10,即group_mc_0中的cpumask只包含cpu0;在下一次进行for_each_cpu(i, span)循环时,i=cpu1,由于cpu1不包含在covered中,因此可以执行到7,此时7处的返回值为CPU1,经过8处代码的过滤只有j=cpu1时,才能执行到9和10,即group_mc_1中的cpumask只包含cpu1。
假设现在是DIE层级,以domain_die_0为例分析8处代码。for_each_cpu(i, span)第一次循环时i=cpu0。由于DIE层级存在child层级(即MC层级),则7处返回的是cpu0在DIE层级child层级(即MC层级)对应的sched_domain(即domain_mc_0) span中的第一个cpu(即cpu0)。在for_each_cpu(j, span)第一次循环时j=cpu0,8处的get_group()的返回值为domain_mc_0 span中的第一个cpu(即cpu0),因此可以执行到9和10;for_each_cpu(j, span)第二次循环时j=cpu1,8处的get_group()的返回值为domain_mc_1 span中的第一个cpu(即cpu0),因此可以执行到9和10;for_each_cpu(j, span)第三、四次循环时j分别为cpu2,cpu3,8处的get_group()的返回值为domain_mc_2和domain_mc_3 span中的第一个cpu(即cpu2),因此不能执行到9和10,因此DIE层级的group_die_0的cpumask中只包含CPU0和CPU1。当 for_each_cpu(i, span)第二次循环时i=cpu1,在7处发现cpu1已经包含在了covered中,因此就不再执行下面的代码。for_each_cpu(i, span)的第三和第四次循环参考第一和第二次循环。
下面我们来看一下build_sched_groups()方法在4.14 Linux Kernel中的实现:

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

在4.14 Linux Kernel中,理解build_sched_groups()方法方法的实现,关键是要理解for_each_cpu_wrap()的实现:

#define for_each_cpu_wrap(cpu, mask, start)                                     \
        for ((cpu) = cpumask_next_wrap((start)-1, (mask), (start), false);      \
             (cpu) < nr_cpumask_bits;                                           \
             (cpu) = cpumask_next_wrap((cpu), (mask), (start), true))

for_each_cpu_wrap()的实现主要依赖于cpumask_next_wrap()的实现:

int cpumask_next_wrap(int n, const struct cpumask *mask, int start, bool wrap)
{
        int next;

again:
        next = cpumask_next(n, mask);

        if (wrap && n < start && next >= start) {
                return nr_cpumask_bits;

        } else if (next >= nr_cpumask_bits) {
                wrap = true;
                n = -1;
                goto again;
        }

        return next;
}
EXPORT_SYMBOL(cpumask_next_wrap);

我们还是以图3.10为例来分析cpumask_next_wrap()方法为什么要如此实现。
cpumask_next()方法要不返回mask中的某个cpu,要不返回>=nr_cpumask_bits。
首先假设#define for_each_cpu_wrap(cpu, mask, start)中的第二个参数mask为cpu0|cpu1|cpu2, 第三个参数start为cpu1。
那么在我们开始遍历mask时,由于start为cpu1,则cpu0将会被遗漏,因此在cpumask_next_wrap()方法中当next >= nr_cpumask_bits时,我们设置wrap=true, n=-1同时跳转到again处重新使用cpumask_next()方法获取cpu,此时返回的刚好是cpu0,因此cpu0被遗漏的情况可以避免掉。