当前位置 博文首页 > 想要去旅行:Linux动态频率调节系统CPUFreq之二:核心(core)架

    想要去旅行:Linux动态频率调节系统CPUFreq之二:核心(core)架

    作者:[db:作者] 时间:2021-08-31 15:57

    上一节中,我们大致地讲解了一下CPUFreq在用户空间的sysfs接口和它的几个重要的数据结构,同时也提到,CPUFreq子系统把一些公共的代码逻辑组织在一起,构成了CPUFreq的核心部分,这些公共逻辑向CPUFreq和其它内核模块提供了必要的API,像cpufreq_governor、cpufreq_driver等模块通过这些API来完成一个完整的CPUFreq体系。这一节我们就来讨论一下核心架构的代码架构以及如何使用这些公共的API接口。

    /*****************************************************************************************************/

    原文链接:https://blog.csdn.net/DroidPhone/article/details/9385745

    /*****************************************************************************************************/

    核心部分的代码都在:/drivers/cpufreq/cpufreq.c中,本系列文章我使用的内核版本是3.10.0.

    1. CPUFreq子系统的初始化


    先看看具体的代码:

    static int __init cpufreq_core_init(void)
    {
            int cpu;
     
            if (cpufreq_disabled())
                    return -ENODEV;
     
            for_each_possible_cpu(cpu) {
                    per_cpu(cpufreq_policy_cpu, cpu) = -1;
                    init_rwsem(&per_cpu(cpu_policy_rwsem, cpu));
            }
     
            cpufreq_global_kobject = kobject_create_and_add("cpufreq", &cpu_subsys.dev_root->kobj);
            BUG_ON(!cpufreq_global_kobject);
            register_syscore_ops(&cpufreq_syscore_ops);
     
            return 0;
    }
    core_initcall(cpufreq_core_init);
    

    可见,在系统的启动阶段,经由initcall机制,cpufreq_core_init被调用,由它来完成核心部分的初始化工作,其中:

    **cpufreq_policy_cpu **是一个per_cpu变量,在smp的系统下,每个cpu可以有自己独立的调频policy,也可以所有的cpu都是用一种policy,这时候就有可能出现其中一个cpu管理着某个policy,而其它cpu因为也使用同一个policy,这些cpu的policy的就交由那个管理cpu代管,这个per_cpu变量就是用来记录各个cpu的policy实际上是由那个cpu进行管理的。初始化时都被初始化为-1了,代表现在还没有开始进行policy的管理。

    接下来的kobject_create_and_add函数在/sys/devices/system/cpu这个节点下建立了一个cpufreq节点,该节点的下面以后会用来放置当前governor的一些配置参数。参数cpu_subsys是内核的一个全局变量,是由更早期的初始化时初始化的,代码在drivers/base/cpu.c中:

    struct bus_type cpu_subsys = {
            .name = "cpu",
            .dev_name = "cpu",
    };
    EXPORT_SYMBOL_GPL(cpu_subsys);
     
     
    void __init cpu_dev_init(void)
    {
            if (subsys_system_register(&cpu_subsys, cpu_root_attr_groups))
                    panic("Failed to register CPU subsystem");
     
            cpu_dev_register_generic();
    }
    

    这将会建立一根cpu总线,总线下挂着系统中所有的cpu,cpu总线设备的根目录就位于:/sys/devices/system/cpu,同时,/sys/bus下也会出现一个cpu的总线节点。cpu总线设备的根目录下会依次出现cpu0,cpu1,… cpux节点,每个cpu对应其中的一个设备节点。CPUFreq子系统利用这个cpu_subsys来获取系统中的cpu设备,并在这些cpu设备下面建立相应的cpufreq对象,这个我们在后面再讨论。

    这样看来,cpufreq子系统的初始化其实没有做什么重要的事情,只是初始化了几个per_cpu变量和建立了一个cpufreq文件节点。下图是初始化过程的序列图:

    图 1.1  核心层初始化

    图 1.1 核心层初始化

    2. 注册cpufreq_governor


    系统中可以同时存在多个governor策略,一个policy通过cpufreq_policy结构中的governor指针和某个governor相关联。要想一个governor被policy使用,首先要把该governor注册到cpufreq的核心中,我们可以通过核心层提供的API来完成注册:

    int cpufreq_register_governor(struct cpufreq_governor *governor)
    {
            int err;
            ......
     
            governor->initialized = 0;
            err = -EBUSY;
            if (__find_governor(governor->name) == NULL) {
                    err = 0;
                    list_add(&governor->governor_list, &cpufreq_governor_list);
            }
     
            ......
            return err;
    }
    

    核心层定义了一个全局链表变量:cpufreq_governor_list,注册函数首先根据governor的名称,通过__find_governor()函数查找该governor是否已經被注册过,如果没有被注册过,则把代表该governor的结构体添加到cpufreq_governor_list链表中。在上一篇中我们提到,目前的内核版本提供了5种governor供我们使用,我们可以通过内核的配置項来选择需要编译的governor,同时需要指定一个默认的governor。在cpufreq.h中,将会根据配置項的选择,把CPUFREQ_DEFAULT_GOVERNOR宏指向默认governor结构体变量的地址,在注册cpufreq_driver的阶段需要使用这个宏来设定系统默认使用的governor。

    3. 注册一个cpufreq_driver驱动


    与governor不同,系统中只会存在一个cpufreq_driver驱动,根据上一篇Linux动态频率调节系统CPUFreq之一:概述的介绍,cpufreq_driver是平台相关的,负责最终实施频率的调整动作,而选择工作频率的策略是由governor完成的。所以,系统中只需要注册一个cpufreq_driver即可,它只负责知道如何控制该平台的时钟系统,从而设定由governor确定的工作频率。注册cpufreq_driver驱动会触发cpufreq核心的一系列额外的初始化动作,第一节所说的核心初始化工作非常简单,实际上,更多的初始化动作在注册cpufreq_driver阶段完成。核心提供了一个API:cpufreq_register_driver来完成注册工作。下面我们分析一下这个函数的工作过程:

    int cpufreq_register_driver(struct cpufreq_driver *driver_data)
    {
            ......
     
            if (cpufreq_disabled())
                    return -ENODEV;
     
            if (!driver_data || !driver_data->verify || !driver_data->init ||
                ((!driver_data->setpolicy) && (!driver_data->target)))
                    return -EINVAL;
    

    该API只有一个参数:一个cpufreq_driver指针,driver_data,该结构事先在驱动的代码中定义,调用该API时作为参数传入。函数先判断系统目前是否禁止了调频功能,然后检查cpufreq_driver的几个回调函数是否被实现,由代码可以看出,verify和init回调函数必须要实现,而setpolicy和target回调则至少要被实现其中的一个。这几个回调的作用请参考本系列的第一篇文章。接下来:

     write_lock_irqsave(&cpufreq_driver_lock, flags);
            if (cpufreq_driver) {
                    write_unlock_irqrestore(&cpufreq_driver_lock, flags);
                    return -EBUSY;
            }
            cpufreq_driver = driver_data;
            write_unlock_irqrestore(&cpufreq_driver_lock, flags);
    

    检查全局变量cpufreq_driver是否已经被赋值,如果没有,则传入的参数被赋值给全局变量cpufreq_driver,从而保证了系统中只会注册一个cpufreq_driver驱动。然后:

            ret = subsys_interface_register(&cpufreq_interface);
            
            ......
            ...... 
     
            register_hotcpu_notifier(&cpufreq_cpu_notifier);
    

    通过subsys_interface_register给每一个cpu建立一个cpufreq_policy,最后注册cpu hot plug通知,以便在cpu hot plug的时候,能够动态地处理各个cpu policy之间的关系(比如迁移负责管理的cpu等等)。这里要重点讨论一下subsys_interface_register的过程,回到第一节的内容,我们知道初始化阶段,cpu_subsys被建立,从而每个cpu都会在cpu总线设备下建立一个属于自己的设备:sys/devices/system/cpu/cpux。subsys_interface_register负责在cpu_subsys子系统的子设备下面注册公共的接口。我们看看参数cpufreq_interface的定义:

    static struct subsys_interface cpufreq_interface = {
            .name           = "cpufreq",
            .subsys         = &cpu_subsys,
            .add_dev        = cpufreq_add_dev,
            .remove_dev     = cpufreq_remove_dev,
    };
    

    subsys_interface_register函数的代码我就不再展开了,它的大致作用就是:遍历子系统下面的每一个子设备,然后用这个子设备作为参数,调用cpufrq_interface结构的add_dev回调函数,这里的回调函数被指向了cpufreq_add_dev,它的具体工作方式我们在下一节中讨论。

    driver注册完成后,驱动被保存在全局变量cpufreq_driver中,供核心层使用,同时,每个cpu也会建立自己的policy策略,governor也开始工作,实时地监控着cpu的负载并计算合适的工作频率,然后通过driver调整真正的工作频率。下图是cpufreq_driver注册过程的序列图:

     图 3.1   cpufreq_driver的注册过程

    图 3.1 cpufreq_driver的注册过程

    4. 为每个cpu建立频率调整策略(policy)


    为每个cpu建立频率调整策略实在注册cpufreq_driver阶段的subsys_interface_registe函数中完成的,上一节已经提到,该函数最终会调用cpufreq_add_dev回调函数,现在展开这个函数分析一下:

    因为subsys_interface_registe会枚举各个cpu设备,不管该cpu处于offline还是online状态,cpufreq_add_dev都会被调用,所以函数的一开始,判断如果cpu处于offline状态,直接返回。

    static int cpufreq_add_dev(struct device *dev, struct subsys_interface *sif)
    {
            ......
     
            if (cpu_is_offline(cpu))
                    return 0;
    

    如果是smp系统,本cpu的policy可能和其他cpu共同使用同一个policy,并委托另一个叫做管理cpu的cpu进行管理,下面的代码判断这种情况,如果已经委托别的cpu管理,则直接返回,核心层定义了另一个per_cpu变量:cpufreq_cpu_data,用来保存各个cpu所使用的cpufreq_policy结构的指针,cpufreq_cpu_get函数实际上就是通过这个per_cpu变量,获取该指针,如果该指针非0,代表该cpu已经建立好了它自身的policy(可能是在他之前的管理cpu建立policy期间一并建立的)。

            policy = cpufreq_cpu_get(cpu);
            if (unlikely(policy)) {
                    cpufreq_cpu_put(policy);
                    return 0;
            }
    

    因为cpu hot plug期间,cpufreq_add_dev也会被调用,下面的代码片段检测该cpu之前是否被hot-unpluged过,如果是,找到其中一个相关的cpu(这些相关的cpu都委托给同一个托管它cpu进行管理,调用cpufreq_add_policy_cpu函数,该函数只是简单地建立一个cpufreq链接,链接到管理cpu的cpufreq节点。

           for_each_online_cpu(sibling) {
                    struct cpufreq_policy *cp = per_cpu(cpufreq_cpu_data, sibling);
                    if (cp && cpumask_test_cpu(cpu, cp->related_cpus)) {
                            read_unlock_irqrestore(&cpufreq_driver_lock, flags);
                            return cpufreq_add_policy_cpu(cpu, sibling, dev);
                    }
            }
    

    当这是系统初始化阶段第一次调用cpufreq_add_dev时(subsys_interface_register枚举到的第一个cpu,通常就是cpu0),cpufreq_cpu_data应该为NULL,所以我们要为这样的cpu分配一个cpufreq_policy结构,并初始化该policy所管理的cpu,包括online的cpus字段和online+offline的cpu_related字段,并把自己设置为这个policy的管理cpu,使用默认governor初始化policy->governor字段,同时吧自己加入到online的cpus字段中:

            policy = kzalloc(sizeof(struct cpufreq_policy), GFP_KERNEL);
            if (!policy)
                    goto nomem_out;
     
            if (!alloc_cpumask_var(&policy->cpus, GFP_KERNEL))
                    goto err_free_policy;
     
            if (!zalloc_cpumask_var(&policy->related_cpus, GFP_KERNEL))
                    goto err_free_cpumask;
     
            policy->cpu = cpu;
            policy->governor = CPUFREQ_DEFAULT_GOVERNOR;
            cpumask_copy(policy->cpus, cpumask_of(cpu));
     
            /* Initially set CPU itself as the policy_cpu */
            per_cpu(cpufreq_policy_cpu, cpu) = cpu;
    

    接下来初始化一个供kobject系统注销时使用的同步变量,初始化一个workqueue,某些时候不能马上执行对该policy的更新操作,可以使用该workqueue来延迟执行。

            init_completion(&policy->kobj_unregister);
            INIT_WORK(&policy->update, handle_update);
    

    接着,调用cpufreq_driver的init回调,进一步初始化该policy:

           ret = cpufreq_driver->init(policy);
            if (ret) {
                    pr_debug("initialization failed\n");
                    goto err_set_policy_cpu;
            }
    

    在上述驱动的初始化内部,应该完成以下工作:

    • 设定该cpu的最大和最小工作频率
    • 设定该policy的最大和最小工作频率
    • 设定该policy可供调节的频率档位
    • 设定cpu调节频率时的延迟时间特性
    • 该policy可以管理的cpu个数(policy->cpus)

    继续:

            /* related cpus should atleast have policy->cpus */
            cpumask_or(policy->related_cpus, policy->related_cpus,