Linux CPU物理拓扑&CPU capacity

这篇博客主要探讨Linux系统中CPU拓扑的管理,特别是对于ARM64架构的实现。内容包括CPU拓扑表的结构,如cluster_id、core_id和thread_id的含义,以及thread_sibling和core_sibling掩码的作用。初始化过程在开机时通过DTS配置完成,而cluster和core的解析以及mask的设置则在后续阶段。用户可以通过sysfs接口查看CPU拓扑信息。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Linux支持非常多的硬件,这些硬件有各种各样的CPU搭配,大到多个NUMA节点组成的大型机,小到只有一个处理器的小型嵌入式设备。内核会在开机时根据硬件配置建立一组数据结构来描述系统的CPU物理拓扑。CPU物理拓扑的一个典型使用者就是调度域。CPU物理拓扑相关的概念也可以见这篇博客的介绍。

此外,一个系统中的CPU架构可以有多种,典型的如ARM的big-LITTLE架构,这些CPU的能力是不一样的,这种设计可以较好的兼顾性能和功耗。Linux内核也需要建立一组数据结构来描述各cpu的能力。

由于cpu能力的维护和cpu物理拓扑的在实现时紧密相关,这篇笔记对二者的实现代码进行分析。分析的代码基于Linux 5.10内核,涉及体系结构相关部分以ARM64为例,主要涉及如下几个文件:

文件

描述

arch/arm64/kernel/topology.c

ARM64的CPU拓扑实现文件,包含了核心的构建CPU拓扑表的流程

drivers/base/topology.c

CPU拓扑驱动,通过sysfs向用户态暴露了CPU拓扑结构

include/linux/topology.h

体系结构无关的CPU拓扑头文件,提供了默认的CPU拓扑相关接口实现。外部模块总是应该引用该头文件

include/linux/arch_topology.h

体系结构相关的CPU拓扑信息

CPU物理拓扑

数据结构

内核用struct cpu_topology结构体来描述一个逻辑cpu的物理拓扑关系。逻辑cpu指的是可以执行一个线程的处理器,系统有几个逻辑cpu,就意味着可以并行执行几个线程。

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;
};
struct cpu_topology cpu_topology[NR_CPUS]; // CPU拓扑信息表

理解这些字段的含义需要先了解下硬件上的几个基本概念(只谈和cpu物理拓扑相关的含义,并非属于描述):

  • NUMA节点:一个复杂系统可以有多个NUMA节点组成,每个NUMA节点可以有多个物理处理器。
  • Package/Socket/Cluster:一个物理处理器,外观上为一个封装(Package),ARM上也称作Cluster,X86上称作插槽(Socket),可以由多个物理Core组成。
  • SMT:超线程。不支持超线程时,一个物理Core就是一个逻辑CPU;支持超线程时,一个物理Core可以有多个逻辑cpu,典型的为一个物理core有2个逻辑cpu。

之后再来理解cpu_topology中各字段就很容易了:

  • llc_id、llc_sibling:NUMA系统时才有用,ARM平台不支持NUMA,实际情况没见过,不太清楚真实值是怎么样的。猜测llc_id就是NUMA节点ID,llc_sibling就是同一个NUMA节点中所有逻辑cpu的mask。
  • package_id:即Cluster ID。
  • core_id、core_sibling:core_id为同一个Cluster下面物理Core的编号,从0开始;core_sibling表示同一个Cluster下面所有逻辑cpu的掩码。
  • thread_id、thread_sibling:thread_id为同一个物理Core下面的线程编号,从0开始;thread_sibling表示同一个物理Core下面所有逻辑cpu的掩码。

构建CPU物理拓扑

Linux内核在开机初始化阶段完成cpu拓扑表的构建,不同体系结构触发构建的时机不同,下面是ARM64的建立过程。

首先是是调用init_cpu_topology()从DTS中解析基本的拓扑信息:

void __init init_cpu_topology(void)
{
    reset_cpu_topology();

    /*
     * Discard anything that was parsed if we hit an error so we
     * don't use partial information.
     */
    if (parse_acpi_topology())
        reset_cpu_topology();
    // ARM64是从DTS中解析
    else if (of_have_populated_dt() && parse_dt_topology())
        reset_cpu_topology();
}

static int __init parse_dt_topology(void)
{
    struct device_node *cn, *map;
    int ret = 0;
    int cpu;

    // 从/cpus/cpu_map节点中解析拓扑
    cn = of_find_node_by_path("/cpus");

    map = of_get_child_by_name(cn, "cpu-map");


    ret = parse_cluster(map, 0); // 解析所有Cluster配置


    // cpu的raw_capacity信息是和拓扑信息一起配置,解析完毕后进行一次cpu能力的归一化计算
    topology_normalize_cpu_scale();

    
    // 进行一次基本的配置检查,确保配置中有每一个逻辑cpu的配置
    for_each_possible_cpu(cpu)
        if (cpu_topology[cpu].package_id == -1)
            ret = -EINVAL;
...
}

static int __init parse_cluster(struct device_node *cluster, int depth)
{
    char name[20];
    bool leaf = true;
    bool has_cores = false;
    struct device_node *c;
    static int package_id __initdata;
    int core_id = 0;
    int i, ret;

    // 循环递归的调用parse_cluster()函数可以完成/cpus/cpu-map节点下所有cluster节点的解析
    i = 0;
    do {
        snprintf(name, sizeof(name), "cluster%d", i);
        c = of_get_child_by_name(cluster, name);
        if (c) {
            leaf = false;
            ret = parse_cluster(c, depth + 1);
            of_node_put(c);
            if (ret != 0)
                return ret;
        }
        i++;
    // c为空表示当前节点下没有名为clusterXX的子节点,结束循环,尝试解析cluster下的core节点
    } while (c);

    // 和上面类似,实现解析cluster节点下的所有core节点
    i = 0;
    do {
        snprintf(name, sizeof(name), "core%d", i);
        c = of_get_child_by_name(cluster, name);
        if (c) {
            has_cores = true;
            // core节点只能存在cluster节点下,如果直接出现在cpu-map下属于配置错误
            if (depth == 0) {
                pr_err("%pOF: cpu-map children should be clusters\n", c);
                of_node_put(c);
                return -EINVAL;
            }

            if (leaf) {
                ret = parse_core(c, package_id, core_id++);
            } else {
                pr_err("%pOF: Non-leaf cluster with core %s\n", cluster, name);
                ret = -EINVAL;
            }
    
            of_node_put(c);
            if (ret != 0)
                return ret;
        }
        i++;
    } while (c);

    if (leaf && !has_cores)
        pr_warn("%pOF: empty cluster\n", cluster);
    if (leaf)
        package_id++;
    return 0;
}

static int __init parse_core(struct device_node *core, int package_id,
                 int core_id)
{
    char name[20];
    bool leaf = true;
    int i = 0;
    int cpu;
    struct device_node *t;

    do { // 如果有配置threadXXX,解析它,支持SMT才会配置
        snprintf(name, sizeof(name), "thread%d", i);
        t = of_get_child_by_name(core, name);
        if (t) {
            leaf = false;
            cpu = get_cpu_for_node(t);
            if (cpu >= 0) { // 解析结果填充cpu拓扑信息表
                cpu_topology[cpu].package_id = package_id;
                cpu_topology[cpu].core_id = core_id;
                cpu_topology[cpu].thread_id = i;
            } else if (cpu != -ENODEV) {
                pr_err("%pOF: Can't get CPU for thread\n", t);
                of_node_put(t);
                return -EINVAL;
            }
            of_node_put(t);
        }
        i++;
    } while (t);

    // 根据配置找到对应的CPU ID
    cpu = get_cpu_for_node(core);
    if (cpu >= 0) {
        if (!leaf) {
            pr_err("%pOF: Core has both threads and CPU\n", core);
            return -EINVAL;
        }
        cpu_topology[cpu].package_id = package_id;
        cpu_topology[cpu].core_id = core_id;
    } else if (leaf && cpu != -ENODEV) {
        pr_err("%pOF: Can't get CPU for leaf core\n", core);
        return -EINVAL;
    }
    return 0;
}

static int __init get_cpu_for_node(struct device_node *node)
{
    struct device_node *cpu_node;
    int cpu;

    cpu_node = of_parse_phandle(node, "cpu", 0); // cpu节点配置
    if (!cpu_node)
        return -1;
    cpu = of_cpu_node_to_id(cpu_node);
    if (cpu >= 0) // 解析cpu raw_capacity,见后面cpu能力的分析
        topology_parse_cpu_capacity(cpu_node, cpu);
    else
        pr_info("CPU node for %pOF exist but the possible cpu range is :%*pbl\n",
            cpu_node, cpumask_pr_args(cpu_possible_mask));
    of_node_put(cpu_node);
    return cpu;
}

第一阶段解析会将拓扑表中每个逻辑cpu的package_id、core_id和thread_id(支持超线程时)字段进行填充。第二阶段会调用store_cpu_topology()根据已有信息设置其它字段。

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;

    // DTS未配置的情况下根据硬件的寄存器设置CPU拓扑表,这里我们不关注    
    mpidr = read_cpuid_mpidr();
...
topology_populated:
    update_siblings_masks(cpuid);
}

void update_siblings_masks(unsigned int cpuid)
{
    struct cpu_topology *cpu_topo, *cpuid_topo = &cpu_topology[cpuid];
    int cpu;

    // 更新每一个逻辑cpu的拓扑表信息
    for_each_online_cpu(cpu) {
        cpu_topo = &cpu_topology[cpu];

        // 设置属于同一个NUMA节点的逻辑cpu的llc_sibling掩码
        if (cpuid_topo->llc_id == cpu_topo->llc_id) {
            cpumask_set_cpu(cpu, &cpuid_topo->llc_sibling);
            cpumask_set_cpu(cpuid, &cpu_topo->llc_sibling);
        }

        // 下面属于同一个Cluster的逻辑cpu才需要设置
        if (cpuid_topo->package_id != cpu_topo->package_id)
            continue;
        // 设置属于同一个Cluster的逻辑cpu的core_sibling掩码
        cpumask_set_cpu(cpuid, &cpu_topo->core_sibling);
        cpumask_set_cpu(cpu, &cpuid_topo->core_sibling);

        // 下面属于同一个物理core的逻辑cpu才需要设置
        if (cpuid_topo->core_id != cpu_topo->core_id)
            continue;
        // 设置属于同一个物理core的逻辑cpu的thread_sibling掩码
        cpumask_set_cpu(cpuid, &cpu_topo->thread_sibling);
        cpumask_set_cpu(cpu, &cpuid_topo->thread_sibling);
    }
}

对外接口

对内核其它模块,可以通过如下宏来获取某个逻辑cpu的拓扑信息:

#define topology_physical_package_id(cpu)    (cpu_topology[cpu].package_id)
#define topology_core_id(cpu)        (cpu_topology[cpu].core_id)
#define topology_core_cpumask(cpu)    (&cpu_topology[cpu].core_sibling)
#define topology_sibling_cpumask(cpu)    (&cpu_topology[cpu].thread_sibling)
#define topology_llc_cpumask(cpu)    (&cpu_topology[cpu].llc_sibling)

对于用户态,可以通过sysfs中的/sys/devices/system/cpu/cpuX/topology目录中的各属性文件查看指定逻辑cpu的拓扑信息,驱动代码很简单,这里不再展开,下面是一个2大核+6小核手机上的各cpu拓扑信息值:

cpu

physical_package_id

core_id

core_siblings

core_siblings_list

thread_siblings

thread_siblings_list

0

0

0

3F

0-5

01

0

1

0

1

3F

0-5

02

1

2

0

2

3F

0-5

 

04

2

3

0

3

3F

0-5

08

3

4

0

4

3F

0-5

10

4

5

0

5

3F

0-5

20

5

6

0

0

C0

6-7

40

6

7

0

1

C0

6-7

80

7

CPU能力

CPU能力指的就是cpu的算力,它和两个因素有关:

  1. CPU的硬件架构,通常用DMIPS(每秒百万条指令数)来描述这一能力,这里有个约束是1MHz频率运行的条件。
  2. CPU的运行频率,频率越高,算力越强,当然功耗也更高。

DMIPS是硬件规格,一旦硬件确定后就不会发生改变。CPU的运行频率却受如下几个方面的影响:

  1. 规格最高频:硬件规格决定的cpu能够达到的最大频率,硬件确定后就不会发生改变。
  2. 策略最高频:基于策略(典型的如热、功耗)对cpu当前可以达到的最大频率进行限制,该值必然是小于规格最高频的。这个值是一个动态变化的值。
  3. 当前运行频率:cpufreq使用governor(典型的如schedutil根据cpu利用率)动态的在[0,策略最高频]之间选择一个运行频率,这个值在实际中往往是快速变化的。

基于上面的一些基本概念来看Linux内核对cpu能力实现。首先,在上面解析cpu拓扑信息时,有调用topology_parse_cpu_capacity()从DTS中解析每个逻辑cpu的DMIPS,然后保存到raw_capacity数组中。

static DEFINE_PER_CPU(u32, freq_factor) = 1;
static u32 *raw_capacity;

bool __init topology_parse_cpu_capacity(struct device_node *cpu_node, int cpu)
{
    struct clk *cpu_clk;
    int ret;
    u32 cpu_capacity;

    ret = of_property_read_u32(cpu_node, "capacity-dmips-mhz",
               &cpu_capacity);
    if (!raw_capacity) {
        raw_capacity = kcalloc(num_possible_cpus(),
            sizeof(*raw_capacity), GFP_KERNEL);
    }
    raw_capacity[cpu] = cpu_capacity;
    pr_debug("cpu_capacity: %pOF cpu_capacity=%u (raw)\n",
        cpu_node, raw_capacity[cpu]);

    // 得到启动初期的cpu运行频率保存到Percpu变量中
    cpu_clk = of_clk_get(cpu_node, 0);
    if (!PTR_ERR_OR_ZERO(cpu_clk)) {
        per_cpu(freq_factor, cpu) = clk_get_rate(cpu_clk) / 1000;
        clk_put(cpu_clk);
    }
  
    return !ret;
}

之后,在拓扑信息解析完毕,还调用topology_normalize_cpu_scale()函数对raw_capacity数组中的DMIPS值进行归一化处理,能力最强的cpu的DMIPS值被折算为1024,其它cpu的DMIPS值按照比例折算为小于1024的值。

                     raw_capacity[cpu]
cpu_scale[cpu] = -------------------------- * 1024
                      capacity_scale

void topology_normalize_cpu_scale(void)
{
    u64 capacity;
    u64 capacity_scale;
    int cpu;

    if (!raw_capacity)
        return;

    // 找到系统中能力最强的cpu的能力保存到capacity_scale中,这里考虑了规格最高频
    capacity_scale = 1;
    for_each_possible_cpu(cpu) {
        capacity = raw_capacity[cpu] * per_cpu(freq_factor, cpu);
        capacity_scale = max(capacity, capacity_scale);
    }
    pr_debug("cpu_capacity: capacity_scale=%llu\n", capacity_scale);

    // 对每个逻辑cpu的能力进行折算,折算后的结果保存在Percpu变量cpu_scale中
    for_each_possible_cpu(cpu) {
        capacity = raw_capacity[cpu] * per_cpu(freq_factor, cpu);
        capacity = div64_u64(capacity << SCHED_CAPACITY_SHIFT, capacity_scale);
        topology_set_cpu_scale(cpu, capacity);
        pr_debug("cpu_capacity: CPU%d cpu_capacity=%lu\n",
            cpu, topology_get_cpu_scale(cpu));
    }
}

DEFINE_PER_CPU(unsigned long, cpu_scale) = SCHED_CAPACITY_SCALE;
void topology_set_cpu_scale(unsigned int cpu, unsigned long capacity)
{
    per_cpu(cpu_scale, cpu) = capacity;
}

Linux内核用cpufreq子系统来管理cpu的频率,在初始化时还向cpufreq子系统注册了policy变化通知事件,目的是在cpufreq确定了所有cpu的规格最高频后调用上面的topology_normalize_cpu_scale()函数重新进行一次归一化计算。

static struct notifier_block init_cpu_capacity_notifier = {
    .notifier_call = init_cpu_capacity_callback,
};

static int __init register_cpufreq_notifier(void)
{
    int ret;

    if (!acpi_disabled || !raw_capacity)
        return -EINVAL;

    // cpus_to_visit用来标记是否已经收到了所有逻辑cpu的policy通知
    if (!alloc_cpumask_var(&cpus_to_visit, GFP_KERNEL))
        return -ENOMEM;
    cpumask_copy(cpus_to_visit, cpu_possible_mask);

    // 向cpufreq子系统注册policy变化通知事件
    ret = cpufreq_register_notifier(&init_cpu_capacity_notifier,
            CPUFREQ_POLICY_NOTIFIER);
    return ret;
}
core_initcall(register_cpufreq_notifier);

static int init_cpu_capacity_callback(struct notifier_block *nb,
        unsigned long val, void *data)
{
    struct cpufreq_policy *policy = data;
    int cpu;

    if (val != CPUFREQ_CREATE_POLICY) // 只处理policy的第一个事件
        return 0;
    // 删除已经收到通知的cpu
    cpumask_andnot(cpus_to_visit, cpus_to_visit, policy->related_cpus);

    // 保存cpu的规格最高频率,单位为MHz
    for_each_cpu(cpu, policy->related_cpus)
        per_cpu(freq_factor, cpu) = policy->cpuinfo.max_freq / 1000;
    // 变为空时表示收到了所有逻辑cpu的policy事件
    if (cpumask_empty(cpus_to_visit)) {
        topology_normalize_cpu_scale(); // 重新进行归一化计算
        schedule_work(&update_topology_flags_work); // 去注册policy事件监听
        free_raw_capacity();
        schedule_work(&parsing_done_work);
    }
    return 0;
}

至此,Percpu变量cpu_scale保存了归一化的cpu能力值,取值范围为[0, 1024],系统中能力最大的cpu的值为1024,其它CPU按照和最大能力cpu的比例折算为小于1024的值。该能力值只考虑了DMIPS(硬件规格)规格最高频,所以这个值在确定后就不会再发生变化,它代表了各逻辑cpu的最高算力。用户态可以从/sys/devices/system/cpu/cpuX/cpu_capacity文件获取对应cpu的该能力值。

其余两个影响cpu能力的因素是策略最高频和cpu当前运行频率,这两个值是动态变化的,而且都来自cpufreq子系统。当cpu的运行频率发生变化时,会调用arch_set_freq_scale()函数设置cpu的当前运行频率,以及cpu的规格最高频率。

unsigned int cpufreq_driver_fast_switch(struct cpufreq_policy *policy,
                    unsigned int target_freq)
{
...
    arch_set_freq_scale(policy->related_cpus, freq,
                    policy->cpuinfo.max_freq);
}

#define arch_set_freq_scale topology_set_freq_scale

// 记录每个逻辑cpu的当前运行频率相对于规格最高频折算为1024的值
                       cur_freq
freq_scale[cpu] = -------------------- * 1024
                       max_freq
DEFINE_PER_CPU(unsigned long, freq_scale) = SCHED_CAPACITY_SCALE;

void topology_set_freq_scale(const struct cpumask *cpus, unsigned long cur_freq,
                 unsigned long max_freq)
{
    unsigned long scale;
    int i;

    if (WARN_ON_ONCE(!cur_freq || !max_freq))
        return;

    scale = (cur_freq << SCHED_CAPACITY_SHIFT) / max_freq;
    for_each_cpu(i, cpus)
        per_cpu(freq_scale, i) = scale;
}

说明:在更低一些版本的内核中,还有一个Percpu变量(max_freq_scale)保存了策略最高频相对于规格最高频折算到1024的参数,但是在5.10删除了这部分代码。

对外接口

内核其它模块可以通过如下接口来获取cpu能力信息。

// 获取cpu的归一化能力值,它考虑了DMIPS和规格最高频,可用于在系统中所有cpu之间进行能力比较
#define arch_scale_cpu_capacity topology_get_cpu_scale
static inline unsigned long topology_get_cpu_scale(int cpu)
{
    return per_cpu(cpu_scale, cpu);
}

// 获取cpu的当前运行频率相对于其规格最高频折算到1024的值
#define arch_scale_freq_capacity topology_get_freq_scale
static inline unsigned long topology_get_freq_scale(int cpu)
{
    return per_cpu(freq_scale, cpu);
}

下面是PELT在计算cpu利用率时的代码,它通过将运行时间running(实际时长)结合cpu能力进行折算后统计,这样不同能力的cpu上统计出来的利用率就可以互相进行比较了。

#define cap_scale(v, s) ((v)*(s) >> SCHED_CAPACITY_SHIFT)

static inline void update_rq_clock_pelt(struct rq *rq, s64 delta)
{
...
    delta = cap_scale(delta, arch_scale_cpu_capacity(cpu_of(rq)));
    delta = cap_scale(delta, arch_scale_freq_capacity(cpu_of(rq)));

    rq->clock_pelt += delta;
}

计算公式换种写法更好理解些,后两个因子值在[0,1]之间,相当于对实际运行时长进行打折,能力越差的cpu折扣越多。

                    cpu归一化能力            cpu运行频率归一化
delta = delta * --------------------- * -------------------------
                       1024                       1024

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值