Skip to main content
 首页 » 操作系统

Linux 调度器之CPU算力 cpu_capacity

2022年07月19日29sxdcgaq8080

一、系统CPU算力

1. 查看系统CPU算力

(1) 设备树中各个CPU原始算力配置

dtb文件位置: 
android/out/target/product/<project_name>/obj/kernel/msm-5.4/arch/arm64/boot/dts/project_name.dtb 
反解析: 
$ dtc -I dtb -O dts -o project_name.dts project_name.dtb 
 
反解析设备树,各个CPU的算力配置: 
capacity-dmips-mhz = <0x400>; //小核,转为10进制为 1024 
capacity-dmips-mhz = <0x6cc>; //大核,转为10进制为 1740

(2) cat cpu_capacity 节点得到的算力

/sys/devices/system # find ./ -name cpu_capacity | xargs cat 
533 
1024 
 
每个cpu最大频点: 
/sys/devices/system # find ./ -name scaling_available_frequencies | xargs cat 
... 1804800 2035200 
... 1708800 1804800

2. cpu_capacity 的设置

/* 
 * cpu_capacity 来自设备树 "capacity-dmips-mhz" 字段,保存在 raw_capacity[cpu]。这个函数最先调用, 
 * 之后每个cluster的cpufreq执行时再唯一通过notifier机制调用一次类似的处理,完成capacity的初始化。 
 * 对每个cluster的每个cpu核都会调用这个函数,这个函数是最先调用的。 
 */ 
bool __init topology_parse_cpu_capacity(struct device_node *cpu_node, int cpu) { //arch_topology.c 
 
    /* cpu_capacity 来自设备树 "capacity-dmips-mhz" 字段,保存在 raw_capacity[cpu] 中*/ 
    ret = of_property_read_u32(cpu_node, "capacity-dmips-mhz", &cpu_capacity); 
    if (!ret) { 
        capacity_scale = max(cpu_capacity, capacity_scale); //此时 capacity_scale 保存的是设备树中指定的算力最大的那个cpu的算力值 
        raw_capacity[cpu] = cpu_capacity; //此时 raw_capacity[cpu] 保存的还是设备树中为每个cpu指定的算力 
        pr_debug("cpu_capacity: %pOF cpu_capacity=%u (raw)\n", cpu_node, raw_capacity[cpu]); 
    } 
}

调用路径:

init_cpu_topology 
    parse_dt_topology 
        parse_cluster 
            parse_core 
                __init get_cpu_for_node(struct device_node *node) 
                    topology_parse_cpu_capacity(cpu_node, cpu); //最先调用(1)

下面看 topology_normalize_cpu_scale 函数,它负责对CPU算力进行归一化到1024:

void topology_normalize_cpu_scale(void) { //arch_topology.c 
    pr_debug("cpu_capacity: capacity_scale=%u\n", capacity_scale); 
    /* 
     * 第二调用(2)时还没没有考虑频点,直接是每个cpu来自设备树"capacity-dmips-mhz"指定的算力与最大值PK的。 
     * 第三调用(3)是在 init_cpu_capacity_callback 下调用的, 
     */ 
    capacity = (raw_capacity[cpu] << SCHED_CAPACITY_SHIFT) / capacity_scale; 
    topology_set_cpu_scale(cpu, capacity) 
        per_cpu(cpu_scale, cpu) = capacity; //写给 per-cpu 变量 cpu_scale 
}

调用路径:

init_cpu_topology //arch_topology.c 
    parse_dt_topology //第二调用(2),调用时所有cpu已经parse完了 
    init_cpu_capacity_callback //后调用,就是上面说的,所有cluster的cpufreq策略online后调用一次 
        topology_normalize_cpu_scale //第三调用(3)

之后就是每个cpufreq策略初始化OK后进行通知更新了:

static struct notifier_block init_cpu_capacity_notifier = { 
    .notifier_call = init_cpu_capacity_callback, 
}; 
 
static int __init register_cpufreq_notifier(void) { 
    /* 所有的 possible cpu 都拷贝到 cpus_to_visit*/ 
    cpumask_copy(cpus_to_visit, cpu_possible_mask); 
    /* 此 notifier 的回调函数为 init_cpu_capacity_callback */ 
    ret = cpufreq_register_notifier(&init_cpu_capacity_notifier, CPUFREQ_POLICY_NOTIFIER); 
} 
core_initcall(register_cpufreq_notifier); //注册的还挺早 
 
/* 这个函数是每个cluster的cpufreq策略ok后都会调用 */ 
static int init_cpu_capacity_callback(struct notifier_block *nb, unsigned long val, void *data) //arch_topology.c 
{ 
    struct cpufreq_policy *policy = data; 
    if (val != CPUFREQ_CREATE_POLICY) 
        return 0; 
 
    pr_debug("cpu_capacity: init cpu capacity for CPUs [%*pbl] (to_visit=%*pbl)\n", 
         cpumask_pr_args(policy->related_cpus), 
         cpumask_pr_args(cpus_to_visit)); 
 
    /* 将此policy中的cpu从mask中清空 */ 
    cpumask_andnot(cpus_to_visit, cpus_to_visit, policy->related_cpus); 
 
    /* 
     * 这里是每个cluster都执行一次的。先读取之前不考虑频点对比的scale后的cpu算力值,然后乘以最大频点。 
     * 最终在 topology_normalize_cpu_scale 中的计算结果就是: 
     *         之前算的 cpu_capacity * own(policy->cpuinfo.max_freq) / max(policy->cpuinfo.max_freq) 
     * 相当于按最大频点又scale了一次,最终cat cpu_capacity节点出来的值就是它了。 
     */ 
    for_each_cpu(cpu, policy->related_cpus) { 
        raw_capacity[cpu] = topology_get_cpu_scale(cpu) * policy->cpuinfo.max_freq / 1000UL; 
        capacity_scale = max(raw_capacity[cpu], capacity_scale); 
    } 
 
    /* 完全为空才调用,也就是说所有cluster的policy都调用过后才能进入 */ 
    if (cpumask_empty(cpus_to_visit)) { 
        topology_normalize_cpu_scale(); //考虑最大频点再次scale,也就是说设备树中配置算力时是不用考虑频点的 
        walt_update_cluster_topology(); //只会调用一次 
        schedule_work(&update_topology_flags_work); 
        free_raw_capacity(); 
        pr_debug("cpu_capacity: parsing done\n"); 
        /* 然后触发调用 parsing_done_workfn,取消这个notifier的通知,也就是说这个if语句体只会进来一次 */ 
        schedule_work(&parsing_done_work); 
    } 
    return 0; 
} 
 
static void parsing_done_workfn(struct work_struct *work) 
{ 
    /*parse done 后就取消注册了*/ 
    cpufreq_unregister_notifier(&init_cpu_capacity_notifier, CPUFREQ_POLICY_NOTIFIER); 
}

接下来就是在 drivers/cpufreq/cpufreq.c 中,只是在 cpufreq_online 中 notifier 通知一次:

static int cpufreq_online(unsigned int cpu) { 
    ... 
    blocking_notifier_call_chain(&cpufreq_policy_notifier_list, CPUFREQ_CREATE_POLICY, policy); 
    ... 
}

cpufreq_online 调用路径:

cpufreq_interface.add_dev //回调 
    cpufreq_add_dev //cpufreq.c 
cpufreq_register_driver 
    cpuhp_cpufreq_online //cpufreq.c 
        cpufreq_online

由上,cat /sys/devices/system/cpu/cpuX/cpu_capacity 获取到的CPU的算力为:own(capacity-dmips-mhz)/max(capacity-dmips-mhz) * (own(max_freq)/max(max_freq)) * 1024

大核:1740/1740 * (2035200/2035200) * 1024 = 1024 
小核:1024/1740 * (1804800/2035200) * 1024 = 533

cat cpu_capacity 得到的算力是每个CPU核在最大频点上对应的算力,注意是最大频点的。若是想获取当前频点的算力或cpufreq policy内的最大算力,还要拿freq再进行scale才行。

3. cpu_capacity 的获取

cat /sys/devices/system/cpu/cpuX/cpu_capacity 
    cpu_capacity_show //arch_topology.c 
        topology_get_cpu_scale(cpu->dev.id) 
            per_cpu(cpu_scale, cpu);

注:这是不考虑限频的原始算力。

二、rq->cpu_capacity_orig 和 rq->cpu_capacity

1. rq->cpu_capacity_orig 和 rq->cpu_capacity 的赋值

(1) 更新函数

static void update_cpu_capacity(struct sched_domain *sd, int cpu) 
{ 
    /* 使用的是topology_get_cpu_scale(int cpu),返回的是 per_cpu(cpu_scale, cpu),就是cat cpu_capacity文件得到的那个值 */ 
    unsigned long capacity = arch_scale_cpu_capacity(cpu); //不是使用返回的是常量值的那个,使用的是宏定义的 
    struct sched_group *sdg = sd->groups; 
 
    /* 
     * 使用的是 topology_get_max_freq_scale(), return per_cpu(max_freq_scale, cpu); max_freq_scale 是当前policy的最大频 
     * 点相对于此cpu能达到的最大频点的scale,见 arch_set_max_freq_scale() 
     */ 
    capacity *= arch_scale_max_freq_capacity(sd, cpu);//1024 
    capacity >>= SCHED_CAPACITY_SHIFT; //除以1024, 就相当于 capacity * (cur_freq/max_freq), 得到的是当前policy的算力 
 
    /*thermal降低它有什么影响?*/ 
    capacity = min(capacity, thermal_cap(cpu)); //受温控影响,取最小值,return thermal_cap_cpu[cpu] ?: SCHED_CAPACITY_SCALE; 
    cpu_rq(cpu)->cpu_capacity_orig = capacity; //这个是取受频点和温控影响后的 
 
    /*就理解为返回的是 capacity- rt/dl/irq 后的吧*/ 
    capacity = scale_rt_capacity(cpu, capacity); 
    if (!capacity) 
        capacity = 1; 
 
    /*赋的是当前cpu最大算力按freq scale、受温度影响、然后再减去rt/dl/irq的贡献后的*/ 
    cpu_rq(cpu)->cpu_capacity = capacity; 
    sdg->sgc->capacity = capacity; 
    sdg->sgc->min_capacity = capacity; 
    sdg->sgc->max_capacity = capacity; 
}

调用路径:

    run_rebalance_domains //SCHED_SOFTIRQ 软中断中处理 
        nohz_idle_balance 
    newidle_balance     
        nohz_newidle_balance 
            _nohz_idle_balance 
scheduler_tick //每个tick中都会触发负载均衡 
    trigger_load_balance 
        raise_softirq(SCHED_SOFTIRQ) //open_softirq(SCHED_SOFTIRQ, run_rebalance_domains); 
            run_rebalance_domains 
                rebalance_domains 
        fair_sched_class.balance //回调    (各个调度类的回调没见到有什么位置调用!) 
            balance_fair 
            pick_next_task_fair //没有任务可pick时调用 
                newidle_balance 
                    load_balance 
                        find_busiest_group 
                            update_sd_lb_stats 
                    sched_isolate_cpu 
                    sched_unisolate_cpu_unlocked 
                        sched_update_group_capacities 
                    sched_init_domains 
                    partition_sched_domains_locked 
                        build_sched_domains 
                            init_sched_groups_capacity 
                                update_group_capacity //sd->child domain不为null的时候调用 
                                    update_cpu_capacity

(2) 下面看 per_cpu 的 max_freq_scale 的设置:

更新函数:

void arch_set_max_freq_scale(struct cpumask *cpus, unsigned long policy_max_freq) { //arch_topology.c 
    int cpu = cpumask_first(cpus); 
    /*获取当前cpu的最大频点*/ 
    max_freq = per_cpu(max_cpu_freq, cpu); 
    /*当policy的最大频点乘以1024除以此CPU的最大频点得到scale值*/ 
    scale = (policy_max_freq << SCHED_CAPACITY_SHIFT) / max_freq; 
    for_each_cpu(cpu, cpus) { 
        /*将scale存储在per-cpu变量max_freq_scale中*/ 
        per_cpu(max_freq_scale, cpu) = scale; 
    } 
}

调用路径:

cpufreq_add_dev 
cpuhp_cpufreq_online  
    cpufreq_online 
        cpufreq_init_policy 
        store_scaling_governor //用户空间更改governor的设置 
cpufreq_notifier_min //schedule_work(&policy->update); policy->nb_min.notifier_call = cpufreq_notifier_min; 
cpufreq_notifier_max //schedule_work(&policy->update); policy->nb_max.notifier_call = cpufreq_notifier_max; 
cpufreq_verify_current_freq //schedule_work(&policy->update); 
    handle_update //cpufreq.c INIT_WORK(&policy->update, handle_update); 
    cpufreq_update_policy 
        refresh_frequency_limits 
            cpufreq_set_policy 
                arch_set_max_freq_scale(policy->cpus, policy->max) //从传参可以看出是per-cluster的cpu

rq->cpu_capacity_orig 是一个policy的最大算力,是使用cpu的最大算力乘以此policy的最大频点与cpu最大频点的比值得到的,然后还考虑了thermal的限频影响。cpu_rq(cpu)->cpu_capacity 是 rq->cpu_capacity_orig 中去除 rt、dl、irq 的贡献后的。
也就是说设置 cpu的 freq limit 会改变其 rq->cpu_capacity_orig 表示的算力。CPU onlline的时候会触发重新计算。max_freq_scale 是用来计算某个policy的最大算力的。

(3) 下面看一下 thermal_cap() 这个温度如何影响CPU算力的:

设置位置:

unsigned long thermal_cap(int cpu) { //walt.c 
    return thermal_cap_cpu[cpu] ?: SCHED_CAPACITY_SCALE; //不为0返回自己 
} 
 
void sched_update_cpu_freq_min_max(const cpumask_t *cpus, u32 fmin, u32 fmax) { 
    for_each_cpu(i, &cpus) 
        thermal_cap_cpu[i] = do_thermal_cap(i, fmax); 
} 
 
static inline unsigned long do_thermal_cap(int cpu, unsigned long thermal_max_freq) 
{ 
    if (unlikely(!walt_clusters_parsed)) 
        return capacity_orig_of(cpu); 
 
    /* 
     * arg1: return per_cpu(cpu_scale, cpu) 就是 cat cpu_capacity 文件得到的值。 
     * arg3: rq->wrq.cluster->max_possible_freq 
     * cpu_capacity * thermal_max_freq / max_possible_freq 若结果有小数向上圆整 
     */ 
    return mult_frac(arch_scale_cpu_capacity(cpu), thermal_max_freq, cpu_max_possible_freq(cpu)); 
}

调用路径:

    limits_mitigation_notify(struct limits_dcvs_hw *hw) //thermal/qcom/msm_lmh_dcvs.c 
limits_dcvsh_poll     
dcvsh_handle_isr //qcom-cpufreq-hw.c 中断线程,对 dcvsh-irq-<cluster_first_cpu_id>进行响应的 
    limits_mitigation_notify(struct cpufreq_qcom *c, bool limit) 
        sched_update_cpu_freq_min_max

也就是说 thermal 是通过限制 CPU 最大频率来限制 CPU 算力的。然后 rq->cpu_capacity_orig 取的是 freq policy 和 thermal 两者限制最很的。也就是说 rq->cpu_capacity_orig 是设备树中指定的cpu的算力与超大核的算力连带两者的最大频点scale后的算力然后再被 freq policy 和 thermal 再次 scale 后的算力。

2. rq->cpu_capacity_orig 的使用

限制CPU的算力有什么用呢,那就看对 rq->cpu_capacity_orig 的使用

(1) 使用位置1 —— capacity_curr_of

unsigned long capacity_curr_of(int cpu) 
{ 
    unsigned long max_cap = cpu_rq(cpu)->cpu_capacity_orig; 
    /* return per_cpu(freq_scale, cpu) => freq_scale = freq_cur * 1024 / freq_max */ 
    unsigned long scale_freq = arch_scale_freq_capacity(cpu); 
 
    /* max_cap * (freq_cur * 1024 / freq_max) == max_cap * (freq_cur/freq_max)  */ 
    return cap_scale(max_cap, scale_freq); 
}

capacity_curr_of 的调用路径:

try_to_wake_up //core.c 若 do_pl_notif() 返回true时才触发调频 
    do_pl_notif //walt.c 若cpu的当前(频点)的算力已经是此freq policy下的最大算力了,就返回false 
walt_find_best_target //fair.c 为任务选核调用路径 
    walt_adjust_cpus_for_packing //fair.c 若是评估的算力比这个cpu的当前算力大,就舍弃这个cpu。 
        capacity_curr_of

由 capacity_curr_of() 的调用路径可知,在唤醒任务时会有一个提频点通过它来判断是否触发提频。在为任务选核时,通过它来判断是否舍弃算力不足的cpu核.

capacity_curr_of(cpu) 返回的是这个 cpu 此时的算力,也就是当前频点下的此 cpu 的算力。有一个trace sched_cpu_util 会打印这个 cpu 的当前算力值。算力与频点挂钩的,若判断某个 cpu 已经达到某个cpu的最大算力了,那就迁核吧,已经是最大频点了,没有必要再调频。

freq_scale 这个per-cpu变量的赋值逻辑:

是下面这个函数中赋值,由上分析可知,参数中的这个 max_freq 需要是 freq pollicy 内的最大频点逻辑才是正常的。

void arch_set_freq_scale(struct cpumask *cpus, unsigned long cur_freq, unsigned long max_freq) //arch_topology.c 
{ 
    scale = (cur_freq << SCHED_CAPACITY_SHIFT) / max_freq; 
 
    for_each_cpu(i, cpus){ 
        per_cpu(freq_scale, i) = scale; 
        per_cpu(max_cpu_freq, i) = max_freq; 
    } 
}

arch_set_freq_scale 的调用路径:

cpufreq_qcom_hw_driver.fast_switch //回调 
    qcom_cpufreq_hw_fast_switch 
    cpufreq_qcom_hw_driver.target_index //回调 
        qcom_cpufreq_hw_target_index //qcom-cpufreq-hw.c 
            arch_set_freq_scale //arch_topology.c

在设置CPU频点时 arch_set_freq_scale 会被调用,更新这个 scale 值。freq_scale 表示当前正在使用的频点对(此freq policy内的)最大频点的scale值。

(2) 使用位置2 —— check_cpu_capacity

/* 
 * 检查rq的算力是否因副业活动而明显减少。imbalance_pct表示阈值。返回true是容量减少了。 
 * sd->imbalance_pct 的注释:直到触发这个水线才进行均衡 
 */ 
static inline int check_cpu_capacity(struct rq *rq, struct sched_domain *sd) //fair.c 
{ 
    /* 
     * 整理后:rq->cpu_capacity * (sd->imbalance_pct/100) < rq->cpu_capacity_orig 
     * 明明是 cpu_capacity_orig要大一些, imbalance_pct 会取大于100的值吗? 
     */ 
    return ((rq->cpu_capacity * sd->imbalance_pct) < (rq->cpu_capacity_orig * 100)); 
}

调用路径:

    trigger_load_balance //schedule_tick中触发SCHED_SOFTIRQ软中断调用 
        nohz_balancer_kick //fair.c 
load_balance //共路径 
    need_active_balance 
    load_balance //共路径 
        voluntary_active_balance //fair.c 
rebalance_domains //schedule_tick中触发SCHED_SOFTIRQ软中断调用 
newidle_balance //CPU进入空闲时均衡 
    load_balance 
        find_busiest_queue //fair.c 
    nohz_balancer_kick //共路径 
        check_misfit_status //fair.c 此cpu上有misfit load的任务,且此cpu不是这个调度域中算力最大的cpu或这个函数返回true就返回真 
            check_cpu_capacity

由调用路径来看,主要是在负载均衡的流程中调用。

imbalance_pct 的取值逻辑:

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) { //sched/topology.c 
 
    struct sched_domain *sd = *per_cpu_ptr(sdd->sd, cpu); 
     
    *sd = (struct sched_domain){ 
        ... 
        .imbalance_pct = 125, 
        ... 
}

调用路径:

sched_init_domains // 
partition_sched_domains_locked 
    build_sched_domains 
        build_sched_domain //sched/topology.c 
            sd_init

imbalance_pct 的赋值果真是大于100的,在调度域的初始化中进行赋值。也就是说 rt/dl/irq 的负载占到cpu算力的 25% 时,就要倾向于进行负载均衡了。

(3) 使用位置3 —— check_misfit_status

static inline int check_misfit_status(struct rq *rq, struct sched_domain *sd) 
{ 
    return rq->misfit_task_load && (rq->cpu_capacity_orig < rq->rd->max_cpu_capacity.val || check_cpu_capacity(rq, sd)); 
}

调用路径见 check_cpu_capacity() 的调用路径,也主要是在负载均衡中调用。

rq->misfit_task_load 的赋值路径:

static inline void update_misfit_status(struct task_struct *p, struct rq *rq) 
{ 
    if (!p) { 
        rq->misfit_task_load = 0; 
        return; 
    } 
 
    if (task_fits_max(p, cpu_of(rq))) { 
        rq->misfit_task_load = 0; 
        return; 
    } 
 
    /* load Qcom没有改,只改的是util */ 
    rq->misfit_task_load = max_t(unsigned long, task_h_load(p), 1); //return p->se.avg.load_avg; 
}

对于任务 p 来说算力不足对 rq->misfit_task_load 赋 p->se.avg.load_avg,若一个任务一直跑,PELT算法计算出的 load_avg 就无限接近其权重。rq->misfit_task_load 在负载均衡上使用判断的比较多。

(4) 使用位置4 —— task_fits_capacity

/* 
 * p->wts.demand_scaled 要大于 XX% * capacity 才返回真,使用的是rq->cpu_capacity_orig, 
 * 这个是cpu的最大算力(cat cpu_capacity文件得到)然后按频点scale和受到thermal影响后的 
 * rq->cpu_capacity_orig 来判断是选up还是down margin的。 
 * 
 * 有时调用 capacity 传的是 rq->cpu_capacity , 有时候传的是 rq->cpu_capacity_orig 
 */ 
static inline bool task_fits_capacity(struct task_struct *p, long capacity, int cpu) { //fair.c 
    /*return p->wts.demand_scaled 要大于 XX% * capacity 才返回true*/ 
    return capacity * 1024 > uclamp_task_util(p) * margin; 
}

实测,/sys/devices/system/cpu/cpuX/cpu_capacity 的值不会随 cpufreq policy 对最大频点的限制而变化,就算是限制最大频点然后睡眠唤醒后也不变,而 capacity_curr_of(cpu) 获取到的算力会随着限频而变化。


本文参考链接:https://www.cnblogs.com/hellokitty2/p/15429760.html