1. 程式人生 > WINDOWS開發 >Linux動態頻率調節系統CPUFreq之二:核心(core)架構與API【轉】

Linux動態頻率調節系統CPUFreq之二:核心(core)架構與API【轉】

Linux動態頻率調節系統CPUFreq之二:核心(core)架構與API

上一節中,我們大致地講解了一下CPUFreq在使用者空間的sysfs介面和它的幾個重要的資料結構,同時也提到,CPUFreq子系統把一些公共的程式碼邏輯組織在一起,構成了CPUFreq的核心部分,這些公共邏輯向CPUFreq和其它核心模組提供了必要的API,像cpufreq_governor、cpufreq_driver等模組通過這些API來完成一個完整的CPUFreq體系。這一節我們就來討論一下核心架構的程式碼架構以及如何使用這些公共的API介面。

核心部分的程式碼都在:/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 核心層初始化

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註冊過程的序列圖:

技術分享圖片

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,policy->cpus);

註釋已經寫的很清楚了,把online的cpu加到代表online+offline的related欄位中。接著,剔除offline的cpu:

        cpumask_and(policy->cpus,policy->cpus,cpu_online_mask);

然後,發出CPUFREQ_START通知:

        blocking_notifier_call_chain(&cpufreq_policy_notifier_list,CPUFREQ_START,policy);

如果是hot-plug加入的cpu,找出它上次使用的governor:

#ifdef CONFIG_HOTPLUG_CPU
        gov = __find_governor(per_cpu(cpufreq_cpu_governor,cpu));
        if (gov) {
                policy->governor = gov;
                pr_debug("Restoring governor %s for cpu %d\n",policy->governor->name,cpu);
        }
#endif

最後,建立cpu裝置下的sysfs檔案節點:cpufreq,它的完整路徑是:/sys/devices/system/cpu/cpux/cpufreq,同時,在他的下面,相應的sysfs節點也同時被建立,節點的內容請參考本系列的第一篇文章:Linux動態頻率調節系統CPUFreq之一:概述:

       ret = cpufreq_add_dev_interface(cpu,policy,dev);

至此,一個cpu的policy建立完成,它的頻率限制條件、使用的governor策略,sysfs檔案節點都已經建立完成。需要注意點是,系統中有多少個cpu,cpufreq_add_dev函式就會被呼叫多少次,最後,每個cpu都會建立自己的policy,當然,也有可能只有部分cpu建立了真正的policy,而其它cpu則委託這些cpu進行policy的管理,關於這一點,一開始讀程式碼的時候可能有點困擾,為了搞清楚他們之間的關係,我們再跟入cpufreq_add_dev_interface函式看看:

static int cpufreq_add_dev_interface(unsigned int cpu,struct cpufreq_policy *policy,struct device *dev)
{
        ......
 
        /* prepare interface data */
        ret = kobject_init_and_add(&policy->kobj,&ktype_cpufreq,&dev->kobj,"cpufreq");
        ......
 
        /* set up files for this cpu device */
        drv_attr = cpufreq_driver->attr;
        while ((drv_attr) && (*drv_attr)) {
                ret = sysfs_create_file(&policy->kobj,&((*drv_attr)->attr));
                if (ret)
                        goto err_out_kobj_put;
                drv_attr++;
        }

函式的一開始,建立cpufreq檔案節點,然後在它的下面再建立一系列節點,使用者可以通過這些檔案節點控制該policy的一些引數。這不是我們的重點,我們看下面這一段程式碼:

        for_each_cpu(j,policy->cpus) {
                per_cpu(cpufreq_cpu_data,j) = policy;
                per_cpu(cpufreq_policy_cpu,j) = policy->cpu;
        }

前面的程式碼已經設定了該policy所管理的online cpu:policy->cpus,通過兩個per_cpu變數,這裡把每個online cpu的policy都設定為了本cpu(管理cpu)的policy,並且把所有online的cpu的管理cpu也指定為了本cpu。接下來,cpufreq_add_dev_symlink被呼叫,所有policy->cpus指定的cpu會建立一個cpufreq連結,指向本cpu(管理cpu)的真實cpufreq節點:

        ret = cpufreq_add_dev_symlink(cpu,policy);

注意,假如這時的cpu是cpu0,也就是說,其它cpu的cpufreq_add_dev還沒有被呼叫,但是在cpufreq_cpu_data中,與之對應的policy指標已經被賦值為cpu0所對應的policy,這樣,回到cpufreq_add_dev函式的開頭部分,當接下其它被認為使用cpu0託管他們的policy的cpu也會進入cpufreq_add_dev函式,但是,因為cpufreq_cpu_data中對應的policy已經在cpu0的建立階段被賦值,所以這些cpu他們不會走完所有的流程,在函式的開頭的判斷部分,判斷cpufreq_cpu_data中cpu對應的policy已經被賦值,就直接返回了。

接著往下看cpufreq_add_dev_interface的程式碼:

        memcpy(&new_policy,sizeof(struct cpufreq_policy));
        /* assure that the starting sequence is run in __cpufreq_set_policy */
        policy->governor = NULL;
 
        /* set default policy */
        ret = __cpufreq_set_policy(policy,&new_policy);
        policy->user_policy.policy = policy->policy;
        policy->user_policy.governor = policy->governor;

通過__cpufreq_set_policy函式,最終使得該policy正式生效。到這裡,每個cpu的policy已經建立完畢,並正式開始工作。關於__cpufreq_set_policy的程式碼這裡就不展開了,我只給出它的序列圖:
技術分享圖片

5. 其它API

cpufreq的核心層除了提供上面幾節討論的註冊governor,註冊cpufreq_driver等API外,還提供了其他一些輔助的API,以方便其它模組的使用。

  • int cpufreq_register_notifier(struct notifier_block *nb,unsigned int list);
  • int cpufreq_unregister_notifier(struct notifier_block *nb,unsigned int list);

以上兩個API用於註冊和登出cpufreq系統的通知訊息,第二個引數可以選擇通知的型別,可以有以下兩種型別:

  • CPUFREQ_TRANSITION_NOTIFIER 收到頻率變更通知

  • CPUFREQ_POLICY_NOTIFIER 收到policy更新通知

  • int cpufreq_driver_target(struct cpufreq_policy *policy,
    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?unsigned int target_freq,
    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?unsigned int relation);

  • int __cpufreq_driver_target(struct cpufreq_policy *policy,
    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?unsigned int target_freq,
    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?unsigned int relation);

以上兩個API用來設定cpu的工作頻率,區別在於cpufreq_driver_target是帶鎖的版本,而__cpufreq_driver_target是不帶鎖的版本,如果確定是在governor的上下文中,使用不帶鎖的版本,否則需要使用帶鎖的版本。

  • void cpufreq_verify_within_limits(struct cpufreq_policy *policy,unsigned int min,unsigned int max);

這個API用來檢查並重新設定policy的最大和最小頻率。

  • int cpufreq_update_policy(unsigned int cpu);

這個API用來觸發cpufreq核心進行policy的更新操作。