分享

一文读懂CPU的层次结构

 幽州浅水 2022-02-22

现代各种处理器采用了许多不同的技术,但从物理结构上,CPU的层次结构却可以被统一的表示如下:

最底层为超线程层,此技术为Intel公司研发,目前在ARM等其他架构处理器中并未采用。超线程技术充分利用空闲CPU资源,在单一时间内可以让一个物理核心同时处理两个线程的工作。因此,在开启超线程的4物理核心的机器上,往往可以看到存在有8个逻辑CPU。

再往上的层次就是物理核心层。单独的一个物理核心的标志往往是其独占的私有缓存(Cache)。通常在两级缓存的情况下,每个物理核心独享L1 Cache,L2 Cache则为几个物理核心共享;在三级缓存的情况下,L1与L2为私有,L3为共享。当然前面说的是通常情况,不能武断地认为二级缓存与三级缓存的情况都是统一的。

继续往上层走就是处理器层级,多个物理核心可以共享最后一级缓存(LLC),这多个物理核心就被称为是一个Cluster或者Socket。芯片厂商会把多个物理核心(Core)封装到一个片(Chip)上,主板上会留有插槽,一个插槽可以插一个Chip。

最上层就到了NUMA的概念了,为了减少对内存总线的访问竞争,可以将CPU分属于不同的Node节点,通常情况下,CPU只访问Node内的memory。

在不考虑NUMA的情况下,CPU所属层次从高到低可以表示为Cluster ---> Core ---> Threads

了解CPU的层次结构有什么作用呢?

以服务器为例,负载均衡是调度器常要考虑的问题。负载均衡在进行任务迁移的时候,把任务移动到哪个CPU上是重中之重。如果开启了超线程,那么移动到超线程层次的兄弟CPU上,被迁移后,原Cache上进程的数据仍然可用,可以说是最优解;如果移动到物理核心层的兄弟CPU上,则LLC上的原先该进程的数据仍然可以被访问到。可以说,迁移到离原CPU层次更近的兄弟CPU上带来的影响更小。

以手机等小型设备为例,功耗是核心问题。尤其是ARM多采用big core和little core的组合结构,即一个Cluster的物理核心是功耗和性能都比较高的CPU,另一个Cluster的物理核心是功耗和性能都比较低的CPU。这时候任务迁移,除了要考虑层次的兄弟临近关系,还要考虑移动到哪个CPU上可以保证功耗和性能的平衡问题。

那么,CPU的层次结构在Linux kernel中是如何表示的呢?

我们以ARM64下CPU的初始化开始入手。ARM64平台下CPU的初始化需要读取DTS(Device Tree Source,设备树)文件。

【配套的Linux内核技术教程文档资料】可以私信我【内核】免费获取。

随意挑选arm64目录下一个处理器的dtsi文件的部分为例:

cpus { #address-cells = <1>; #size-cells = <0>; cpu-map { cluster0 { core0 { cpu = <&cpu0>; }; ...... }; ...... }; cpu0: cpu@20000 { device_type = 'cpu'; compatible = 'arm,cortex-a57'; reg = <0x20000>; enable-method = 'psci'; next-level-cache = <&cluster0_l2>; }; ...... cluster0_l2: l2-cache0 { compatible = 'cache'; }; ...... };

以上是DTS文件中CPU定义的一个统一的格式。start_kernel --> setup_arch --> smp_init_cpus

smp_init_cpus在系统启动的时候读取DTS文件中CPU信息,获得CPU数量,并调用smp_cpu_setup函数进行possible位图的设置。

 static int __init smp_cpu_setup(int cpu) { const struct cpu_operations *ops;  if (init_cpu_ops(cpu)) return -ENODEV;  ops = get_cpu_ops(cpu); if (ops->cpu_init(cpu)) return -ENODEV;  set_cpu_possible(cpu, true);  return 0; }

这里先了解一下内核中存在的四种CPU的状态表示,分别是possibleonlinepresentactive

typedef struct cpumask { DECLARE_BITMAP(bits, NR_CPUS); } cpumask_t; #define DECLARE_BITMAP(name,bits) \ unsigned long name[BITS_TO_LONGS(bits)] extern struct cpumask __cpu_possible_mask; extern struct cpumask __cpu_online_mask; extern struct cpumask __cpu_present_mask; extern struct cpumask __cpu_active_mask; #define cpu_possible_mask ((const struct cpumask *)&__cpu_possible_mask) #define cpu_online_mask ((const struct cpumask *)&__cpu_online_mask) #define cpu_present_mask ((const struct cpumask *)&__cpu_present_mask) #define cpu_active_mask ((const struct cpumask *)&__cpu_active_mask)

可以从smp_cpu_setup函数看出,如果一个CPU编号对应的struct cpu_operations被建立成功,并且其对应的init函数没有执行失败,则将该CPU标记为possible。

 struct cpu_operations { const char*name; int(*cpu_init)(unsigned int); int(*cpu_prepare)(unsigned int); int(*cpu_boot)(unsigned int); void(*cpu_postboot)(void); #ifdef CONFIG_HOTPLUG_CPU bool(*cpu_can_disable)(unsigned int cpu); int(*cpu_disable)(unsigned int cpu); void(*cpu_die)(unsigned int cpu); int(*cpu_kill)(unsigned int cpu); #endif #ifdef CONFIG_CPU_IDLE int(*cpu_init_idle)(unsigned int); int(*cpu_suspend)(unsigned long); #endif };
  • cpu_init:读取提出一个逻辑CPU的激活方法的必要数据;
  • cpu_prepare:测试是否可以启动给定CPU;
  • cpu_boot:启动CPU;
  • cpu_postboot:启动CPU后必要的清理工作。

除了上述的几个成员函数,可以看到其他成员函数是分别和CPU热插拔和CPUIDLE相关的电源操作,kernel抽象出了struct cpu_operations这个结构体,将启动、热插拔、空闲态接口统一起来,交给各个体系结构自己完成。

start_kernel --> arch_call_rest_init --> rest_init --> kernel_init --> kernel_init_freezable --> smp_prepare_cpus

接下来一直到smp_prepare_cpus函数,才到设置present位图的时候。

以下是smp_prepare_cpus的部分代码:

void __init smp_prepare_cpus(unsigned int max_cpus) { const struct cpu_operations *ops; int err; unsigned int cpu; unsigned int this_cpu; ...... for_each_possible_cpu(cpu) { per_cpu(cpu_number, cpu) = cpu; if (cpu == smp_processor_id()) continue; ops = get_cpu_ops(cpu); if (!ops) continue; err = ops->cpu_prepare(cpu); if (err) continue; set_cpu_present(cpu, true); numa_store_cpu_info(cpu); } }

前面看到,possible位图的设置依赖于cpu_operations结构体的初始化以及是否读取到正确的CPU信息;那这里present位图的设置则依赖于cpu_prepare的执行结果,也就是检测对应CPU是否可以启动。

kernel_init_freezable --> smp_init --> bringup_nonboot_cpus

 void bringup_nonboot_cpus(unsigned int setup_max_cpus) { unsigned int cpu;  for_each_present_cpu(cpu) { if (num_online_cpus() >= setup_max_cpus) break; if (!cpu_online(cpu)) cpu_up(cpu, CPUHP_ONLINE); } }

cpu_up --> _cpu_up --> cpuhp_up_callbacks --> cpuhp_invoke_callback --> bringup_cpu --> __cpu_up --> boot_secondary

cpu_up,这里可以这么简单的理解,最开始系统中只有一个CPU,当代码执行到这里的时候,开始启动其他的CPU,其他CPU的启动过程,首先是需要复制主CPU的0号线程(即空闲线程),然后去执行热插拔相关的回调函数,一直执行到boot_secondary,在这里最终会执行到struct cpu_operations中的回调函数cpu_boot

static int boot_secondary(unsigned int cpu, struct task_struct *idle) { const struct cpu_operations *ops = get_cpu_ops(cpu); if (ops->cpu_boot) return ops->cpu_boot(cpu); return -EOPNOTSUPP; }

而通常在struct cpu_operations中的成员cpu_boot的执行中,会执行到secondary_entry --> secondary_startup --> __secondary_switched --> secondary_start_kernel

而在secondary_start_kernel中会调用set_cpu_online函数设置online位图。

active这个位图的修改函数定义在CPU热插拔的回调函数数组中,也就是struct cpuhp_step cpuhp_hp_states[]中,该位图服务于调度器,在开启热插拔的情况下,调度器需要实时监控CPU热插拔的每个信息,因此需要该位图实时将某个CPU移除或添加。

说完了四种位图,突然发现还没有说到CPU的层次结构在代码上的表示,CPU的层次结构使用一个结构体struct cpu_topology来表示。

 struct cpu_topology { int thread_id; int core_id; int package_id; int llc_id; cpumask_t thread_sibling; cpumask_t core_sibling; cpumask_t llc_sibling; };  #ifdef CONFIG_GENERIC_ARCH_TOPOLOGY extern struct cpu_topology cpu_topology[NR_CPUS];

其实,代码上CPU的层次结构的赋值代码与CPU启动的时间基本一致,主CPU的赋值是在smp_prepare_cpus函数中;其余CPU是在secondary_start_kernel函数中。

直接来看cpu_topology结构体的赋值函数store_cpu_topology。在调用该函数之前,已经在init_cpu_topology函数中进行过cpu_topology数组的初始化了,其中thread_idcore_idpackage_idllc_id皆初始化为-1。

void store_cpu_topology(unsigned int cpuid) { struct cpu_topology *cpuid_topo = &cpu_topology[cpuid]; u64 mpidr; if (cpuid_topo->package_id != -1) goto topology_populated; mpidr = read_cpuid_mpidr(); /* Uniprocessor systems can rely on default topology values */ if (mpidr & MPIDR_UP_BITMASK) return; cpuid_topo->thread_id = -1; cpuid_topo->core_id = cpuid; cpuid_topo->package_id = cpu_to_node(cpuid); pr_debug('CPU%u: cluster %d core %d thread %d mpidr %#016llx\n', cpuid, cpuid_topo->package_id, cpuid_topo->core_id, cpuid_topo->thread_id, mpidr); topology_populated: update_siblings_masks(cpuid); }

以arm64为例,thread_id应该是超线程情况下才有意义,这里不考虑。core_id等同于物理核心id,也就是逻辑核心id,二者一致,package_id指向了其所属的NUMA的Node号。update_sibling_masks则用于更新用于表示各个层级的兄弟关系。

说了这么多,一直在说CPU的物理层次结构在代码上的表示,却还没有说到调度中CPU物理结构的参与。

这里关注kernel_init_freezable函数有这样一段代码:

     smp_init(); sched_init_smp();

前面已经了解到smp_init函数执行完成时,各个次CPU已经启动完成,此时进入sched_init_smp函数。

sched_init_smp --> sched_init_domains --> build_sched_domains

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; // 核心函数1,此时的cpu_map是active的位图 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; int dflags = 0; sd = NULL; for_each_sd_topology(tl) { if (tl == tl_asym) { dflags |= SD_ASYM_CPUCAPACITY; has_asym = true; } if (WARN_ON(!topology_span_sane(tl, cpu_map, i))) goto error; // 核心函数2 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 */ 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 { // 核心函数3 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; }

首先,分析第1个核心函数
__visit_domain_allocation_hell

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

首先进入__sdt_alloc函数。

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

在这个函数中我们接触到第一个比较关键的数据结构struct
sched_domain_topology_level

 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 };  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, }, };  static struct sched_domain_topology_level *sched_domain_topology = default_topology;  #define for_each_sd_topology(tl)\ for (tl = sched_domain_topology; tl->mask; tl++)  void set_sched_topology(struct sched_domain_topology_level *tl) { if (WARN_ON_ONCE(sched_smp_initialized)) return;  sched_domain_topology = tl; }

可以看到,struct
sched_domain_topology_level
使用一个数组default_topology给出,当然各个体系结构下也可以修改该数组。该数组一般按照CPU层次从低到高排列。

struct
sched_domain_toplogy_level
的前两个成员maskflag分别是两个函数,用于指定该CPU层次下的兄弟cpumask和flag标志位。而struct sd_data是该数据结构的核心,其成员将在后续完成赋值。

这里简单看下物理核心层的mask成员cpu_coregroup_mask

const struct cpumask *cpu_coregroup_mask(int cpu) { const cpumask_t *core_mask = cpumask_of_node(cpu_to_node(cpu)); /* Find the smaller of NUMA, core or LLC siblings */ if (cpumask_subset(&cpu_topology[cpu].core_sibling, core_mask)) { /* not numa in package, lets use the package siblings */ core_mask = &cpu_topology[cpu].core_sibling; } if (cpu_topology[cpu].llc_id != -1) { if (cpumask_subset(&cpu_topology[cpu].llc_sibling, core_mask)) core_mask = &cpu_topology[cpu].llc_sibling; } return core_mask; }

可以看到,本质上是利用cpu_topology数组来获得core_sibling的值,到这里就和物理层级联系起来了。

 struct sd_data { struct sched_domain *__percpu *sd; struct sched_domain_shared *__percpu *sds; struct sched_group *__percpu *sg; struct sched_group_capacity *__percpu *sgc; };

分析__sdt_alloc,可以看到为每个层次的struct
sched_domain_topology_level
sd_data分配percpu的sched_domainsched_domain_sharedsched_groupsched_group_capacity。这样每个CPU可以通过default_topology数组轻易找到自己的sched_domain等数据。

接下来,继续分析build_sched_domains的第2个核心函数build_sched_domain,该函数在每个CPU的每个层次上都要去调用一次。

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

其中sd_init函数完成对应
sched_domain_topology_level
对应层次的sched_domain的一些数据的填充以及sd_data中mask和flag的修改。

 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= 16, .imbalance_pct= 117,  .cache_nice_tries= 0,  .flags= 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 };  cpumask_and(sched_domain_span(sd), cpu_map, tl->mask(cpu)); sd_id = cpumask_first(sched_domain_span(sd));  /*  * Convert topological properties into behaviour.  */  /* Don't attempt to spread across CPUs of different capacities. */ if ((sd->flags & SD_ASYM_CPUCAPACITY) && sd->child) sd->child->flags &= ~SD_PREFER_SIBLING;  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; }

这里可以看到,sched_domain中的成员往往是与调度中负载均衡操作相关的一些字段,例如min_interval是最小的load balance间隔,max_interval是最大的load balance间隔等等。

这里应该注意,sched_domain的父子关系,高层级的sched_domain是低层级的sched_domain的parent,低层级的sched_domain是高层级的child。sched_domainspan为自己该层级的兄弟CPU以及下面几个层级的兄弟CPU的并集。

创建完sched_domain之后,开始从CPU开始由低到高依次建立sched_group,这就是第3个核心函数,build_sched_groups

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

build_sched_groups函数的核心是get_group函数。

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

可以从get_group函数看出来,如果是最底层的sched_domain,即不存在child,那么get_group中建立的sched_group是该CPU独有的。而如果是非最底层的sched_domain,那么get_group中建立的sched_group是其childspan成员共享的。这里注意,sched_group_capacitysched_group两位一体,表示该调度组的计算能力。

到这里,调度域和调度组的关系基本可以理清楚。在CPU层次结构上,CPU在每个层次都有一个调度域,该CPU在高层次的调度域与该CPU在低层次的调度域互成父子关系。

而调度域下皆有一个调度组,调度组是呈链表组织的。一个调度域将其调度域中所有CPU按照兄弟关系分成不同的调度组,然后以链表形式组织起来,一个调度域中的所有CPU在该CPU层次上是共享这个调度组的。

更新完成sched_group_capacity之后,最后一部分cpu_attach_domain --> rq_attach_root

rq_attach_root中有这样的一行代码:rq->rd = rd;。至此,从rq --> root_domain --> sched_domain的关系已经建立完成。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多