网站首页 > 开源技术 正文
现代各种处理器采用了许多不同的技术,但从物理结构上,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的状态表示,分别是possible、online、present和active。
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_id、core_id、package_id和llc_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的前两个成员mask和flag分别是两个函数,用于指定该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_domain、sched_domain_shared、sched_group和sched_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_domain的span为自己该层级的兄弟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是其child的span成员共享的。这里注意,sched_group_capacity和sched_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的关系已经建立完成。
猜你喜欢
- 2024-10-30 常见的Socket网络异常场景分析(socket异常连接怎么重置)
- 2024-10-30 JVM性能调优监控工具jps、jstack、jmap、jhat、hprof使用详解
- 2024-10-30 网络安全学习(5) - hping3 学习(hunter网络安全)
- 2024-10-30 一文读懂如何查看网络的性能指标(如何查看网络强度)
- 2024-10-30 如何使用网上公开的exp(如何使用网上邻居)
- 2024-10-30 C++ Socket 编程(c++ socket教程)
- 2024-10-30 FM2+ APU 最后逆袭,搭载 USB3.1 技嘉 F2A88XM-D3HP
- 2024-10-30 hprose for java源码分析-16 服务器启动
- 2024-10-30 得力DM34ADN国产打印机在银河麒麟系统上安装和设置的方法
- 2024-10-30 Shopee新品爆品 | Shopee总下载量全球购物类APP第一
你 发表评论:
欢迎- 最近发表
-
- 6月游戏推荐(二)(6月份新出的游戏)
- 37【源码】数据可视化:基于 Echarts + Python 动态实时大屏
- Kubernetes Kube-Proxy 组件 IPVS 模式工作原理及常用故障排查
- 《茶余饭后顶级英文歌曲精选》(茶余饭后的经典句子)
- rainx和MediaTek携手推出101产品生态,为5G FWA提供创新
- KAPITAL 推出蓝染风格 Aloha Shirt 系列
- 欧美经典怀旧歌曲Free loop-管不住的音符
- Mac 下php5.3-7.0的二进制包 ── PHP-OS
- 如何把一个Python应用程序装进Docker
- 为何推荐 JsonTree.js 做 JSON 可视化?
- 标签列表
-
- jdk (81)
- putty (66)
- rufus (78)
- 内网穿透 (89)
- okhttp (70)
- powertoys (74)
- windowsterminal (81)
- netcat (65)
- ghostscript (65)
- veracrypt (65)
- asp.netcore (70)
- wrk (67)
- aspose.words (80)
- itk (80)
- ajaxfileupload.js (66)
- sqlhelper (67)
- express.js (67)
- phpmailer (67)
- xjar (70)
- redisclient (78)
- wakeonlan (66)
- tinygo (85)
- startbbs (72)
- webftp (82)
- vsvim (79)
本文暂时没有评论,来添加一个吧(●'◡'●)