Linux內核架構:動態頻率調節系統CPUFreq

概述

在Linux中,內核提供了一套框架模型來完成CPU頻率的動態調節,以達到省電的目的。
這就是所謂的CPUFreq系統。


1. sysfs接口

我們先從CPUFreq提供的sysfs接口入手,直觀地看看它提供了那些功能。以下是我的電腦輸出的結果:

droidphone@990:~$ cd /sys/devices/system/cpu
droidphone@990:/sys/devices/system/cpu$ ls
cpu0  cpu3  cpu6     cpuidle     offline   power    release
cpu1  cpu4  cpu7     kernel_max  online    present  uevent
cpu2  cpu5  cpufreq  modalias    possible  probe

所有與CPUFreq相關的sysfs接口都位於:/sys/devices/system/cpu下面,我們可以看到,8個cpu分別建立了一個自己的目錄,從cpu0到cpu7,我們再看看offline和online以及present的內容:

droidphone@990:/sys/devices/system/cpu$ cat online
0-7
droidphone@990:/sys/devices/system/cpu$ cat offline
8-15
droidphone@990:/sys/devices/system/cpu$ cat present
0-7
droidphone@990:/sys/devices/system/cpu$

online代表目前正在工作的cpu,輸出顯示編號爲0-7這8個cpu在工作,offline代表目前被關掉的cpu,present則表示主板上已經安裝的cpu,由輸出可以看到,我的主板可以安裝16個cpu(因爲intel的超線程技術,其實物理上只是8個),第8-15號cpu處於關閉狀態(實際上不存在,因爲present只有0-7)。

接着往下看:

droidphone@990:/sys/devices/system/cpu/cpu0$ ls
cache    cpuidle      microcode  power      thermal_throttle  uevent
cpufreq  crash_notes  node0      subsystem  topology
droidphone@990:/sys/devices/system/cpu/cpu0$ cd cpufreq/
droidphone@990:/sys/devices/system/cpu/cpu0/cpufreq$ ls
affected_cpus               related_cpus                   scaling_max_freq
bios_limit                  scaling_available_frequencies  scaling_min_freq
cpuinfo_cur_freq            scaling_available_governors    scaling_setspeed
cpuinfo_max_freq            scaling_cur_freq               stats
cpuinfo_min_freq            scaling_driver
cpuinfo_transition_latency  scaling_governor
droidphone@990:/sys/devices/system/cpu/cpu0/cpufreq$ 

在我的電腦上,部分的值如下:
cpuinfo_cur_freq: 1600000

cpuinfo_max_freq: 3401000

cpuinfo_min_freq: 1600000

scaling_cur_freq: 1600000

scaling_max_freq: 3401000

scaling_min_freq: 1600000
所以,我的cpu0的最低運行頻率是1.6GHz,最高是3.4GHz,目前正在運行的頻率是1.6GHz,前綴cpuinfo代表的是cpu硬件上支持的頻率,而scaling前綴代表的是可以通過CPUFreq系統用軟件進行調節時所支持的頻率。cpuinfo_cur_freq代表通過硬件實際上讀到的頻率值,而scaling_cur_freq則是軟件當前的設置值,多數情況下這兩個值是一致的,但是也有可能因爲硬件的原因,有微小的差異。scaling_available_frequencies會輸出當前軟件支持的頻率值,看看我的cpu支持那些頻率:

droidphone@990:/sys/devices/system/cpu/cpu0/cpufreq$ cat scaling_available_frequencies 
3401000 3400000 3000000 2800000 2600000 2400000 2200000 2000000 1800000 1600000 
droidphone@990:/sys/devices/system/cpu/cpu0/cpufreq$ 

Oh,從1.6GHz到3.4GHz,一共支持10擋的頻率可供選擇。scaling_available_governors則會輸出當前可供選擇的頻率調節策略:

conservative ondemand userspace powersave performance

一共有5中策略供我們選擇,那麼當前系統選用那種策略?讓我們看看:

dong@dong-990:/sys/devices/system/cpu/cpu0/cpufreq$ cat scaling_governor
ondemand

OK,我的系統當前選擇ondemand這種策略,這種策略的主要思想是:只要cpu的負載超過某一個閥值,cpu的頻率會立刻提升至最高,然後再根據實際情況降到合適的水平。詳細的情況我們留在後面的章節中討論。scaling_driver則會輸出當前使用哪一個驅動來設置cpu的工作頻率。
當我們選擇userspace作爲我們的調頻governor時,我們可以通過scaling_setspeed手工設置需要的頻率。powersave則簡單地使用最低的工作頻率進行運行,而performance則一直選擇最高的頻率進行運行。


2. 軟件架構

通過上一節的介紹,我們可以大致梳理出CPUFreq系統的構成和工作方式。首先,CPU的硬件特性決定了這個CPU的最高和最低工作頻率,所有的頻率調整數值都必須在這個範圍內,它們用cpuinfo_xxx_freq來表示。然後,我們可以在這個範圍內再次定義出一個軟件的調節範圍,它們用scaling_xxx_freq來表示,同時,根據具體的硬件平臺的不同,我們還需要提供一個頻率表,這個頻率表規定了cpu可以工作的頻率值,當然這些頻率值必須要在cpuinfo_xxx_freq的範圍內。有了這些頻率信息,CPUFreq系統就可以根據當前cpu的負載輕重狀況,合理地從頻率表中選擇一個合適的頻率供cpu使用,已達到節能的目的。至於如何選擇頻率表中的頻率,這個要由不同的governor來實現,目前的內核版本提供了5種governor供我們選擇。選擇好適當的頻率以後,具體的頻率調節工作就交由scaling_driver來完成。CPUFreq系統把一些公共的邏輯和接口代碼抽象出來,這些代碼與平臺無關,也與具體的調頻策略無關,內核的文檔把它稱爲CPUFreq Core(/Documents/cpufreq/core.txt)。另外一部分,與實際的調頻策略相關的部分被稱作cpufreq_policy,cpufreq_policy又是由頻率信息和具體的governor組成,governor纔是具體策略的實現者,當然governor需要我們提供必要的頻率信息,governor的實現最好能做到平臺無關,與平臺相關的代碼用cpufreq_driver表述,它完成實際的頻率調節工作。最後,如果其他內核模塊需要在頻率調節的過程中得到通知消息,則可以通過cpufreq notifiers來完成。由此,我們可以總結出CPUFreq系統的軟件結構如下:

3. cpufreq_policy

一種調頻策略的各種限制條件的組合稱之爲policy,代碼中用cpufreq_policy這一數據結構來表示:

struct cpufreq_policy {

        cpumask_var_t           cpus;   
        cpumask_var_t           related_cpus; 

        unsigned int            shared_type; 

        unsigned int            cpu;    
        unsigned int            last_cpu; 

        struct cpufreq_cpuinfo  cpuinfo;

        unsigned int            min;    /* in kHz */
        unsigned int            max;    /* in kHz */
        unsigned int            cur;    

        unsigned int            policy; 
        struct cpufreq_governor *governor; 
        void                    *governor_data;

        struct work_struct      update; 


        struct cpufreq_real_policy      user_policy;

        struct kobject          kobj;
        struct completion       kobj_unregister;
};

其中的各個字段的解釋如下:

  • cpus和related_cpus 這兩個都是cpumask_var_t變量,cpus表示的是這一policy控制之下的所有還出於online狀態的cpu,而related_cpus則是online和offline兩者的合集。主要是用於多個cpu使用同一種policy的情況,實際上,我們平常見到的大多數系統中都是這種情況:所有的cpu同時使用同一種policy。我們需要related_cpus變量指出這個policy所管理的所有cpu編號。
  • cpu和last_cpu 雖然一種policy可以同時用於多個cpu,但是通常一種policy只會由其中的一個cpu進行管理,cpu變量用於記錄用於管理該policy的cpu編號,而last_cpu則是上一次管理該policy的cpu編號(因爲管理policy的cpu可能會被plug out,這時候就要把管理工作遷移到另一個cpu上)。
  • cpuinfo 保存cpu硬件所能支持的最大和最小的頻率以及切換延遲信息。
  • min/max/cur 該policy下的可使用的最小頻率,最大頻率和當前頻率。
  • policy 該變量可以取以下兩個值:CPUFREQ_POLICY_POWERSAVE和CPUFREQ_POLICY_PERFORMANCE,該變量只有當調頻驅動支持setpolicy回調函數的時候有效,這時候由驅動根據policy變量的值來決定系統的工作頻率或狀態。如果調頻驅動(cpufreq_driver)支持target回調,則頻率由相應的governor來決定。
  • governor和governor_data 指向該policy當前使用的cpufreq_governor結構和它的上下文數據。governor是實現該policy的關鍵所在,調頻策略的邏輯由governor實現。
  • update 有時在中斷上下文中需要更新policy,需要利用該工作隊列把實際的工作移到稍後的進程上下文中執行。
  • user_policy 有時候因爲特殊的原因需要修改policy的參數,比如溫度過高時,最大可允許的運行頻率可能會被降低,爲了在適當的時候恢復原有的運行參數,需要使用user_policy保存原始的參數(min,max,policy,governor)。
  • kobj 該policy在sysfs中對應的kobj的對象。

4. cpufreq_governor

所謂的governor,我把它翻譯成:調節器。governor負責檢測cpu的使用狀況,從而在可用的範圍中選擇一個合適的頻率,代碼中它用cpufreq_governor結構來表示:

struct cpufreq_governor {
        char    name[CPUFREQ_NAME_LEN];
        int     initialized;
        int     (*governor)     (struct cpufreq_policy *policy,
                                 unsigned int event);
        ssize_t (*show_setspeed)        (struct cpufreq_policy *policy,
                                         char *buf);
        int     (*store_setspeed)       (struct cpufreq_policy *policy,
                                         unsigned int freq);
        unsigned int max_transition_latency; /* HW must be able to switch to
                        next freq faster than this value in nano secs or we
                        will fallback to performance governor */
        struct list_head        governor_list;
        struct module           *owner;
};

其中的各個字段的解釋如下:

  • name 該governor的名字。
  • initialized 初始化標誌。
  • governor 指向一個回調函數,CPUFreq Core會在不同的階段調用該回調函數,用於該governor的啓動、停止、初始化、退出動作。
  • list_head 所有註冊的governor都會利用該字段鏈接在一個全局鏈表中,以供系統查詢和使用。

5. cpufreq_driver

上一節提到的gonvernor只是負責計算並提出合適的頻率,但是頻率的設定工作是平臺相關的,這需要cpufreq_driver驅動來完成,cpufreq_driver的結構如下:

struct cpufreq_driver {
        struct module           *owner;
        char                    name[CPUFREQ_NAME_LEN];
        u8                      flags;

        bool                    have_governor_per_policy;

        /* needed by all drivers */
        int     (*init)         (struct cpufreq_policy *policy);
        int     (*verify)       (struct cpufreq_policy *policy);

        /* define one out of two */
        int     (*setpolicy)    (struct cpufreq_policy *policy);
        int     (*target)       (struct cpufreq_policy *policy,
                                 unsigned int target_freq,
                                 unsigned int relation);

        /* should be defined, if possible */
        unsigned int    (*get)  (unsigned int cpu);

        /* optional */
        unsigned int (*getavg)  (struct cpufreq_policy *policy,
                                 unsigned int cpu);
        int     (*bios_limit)   (int cpu, unsigned int *limit);

        int     (*exit)         (struct cpufreq_policy *policy);
        int     (*suspend)      (struct cpufreq_policy *policy);
        int     (*resume)       (struct cpufreq_policy *policy);
        struct freq_attr        **attr;
};

相關的字段的意義解釋如下:

  • name 該頻率驅動的名字。
  • init 回調函數,該回調函數必須實現,CPUFreq Core會通過該回調函數對該驅動進行必要的初始化工作。
  • verify 回調函數,該回調函數必須實現,CPUFreq Core會通過該回調函數檢查policy的參數是否被驅動支持。
  • setpolicy/target 回調函數,驅動必須實現這兩個函數中的其中一個,如果不支持通過governor選擇合適的運行頻率,則實現setpolicy回調函數,這樣系統只能支持CPUFREQ_POLICY_POWERSAVE和CPUFREQ_POLICY_PERFORMANCE這兩種工作策略。反之,實現target回調函數,通過target回調設定 - - governor所需要的頻率。
  • get 回調函數,用於獲取cpu當前的工作頻率。
  • getavg 回調函數,用於獲取cpu當前的平均工作頻率。

6. cpufreq notifiers

CPUFreq的通知系統使用了內核的標準通知接口。它對外提供了兩個通知事件:policy通知和transition通知。

policy通知用於通知其它模塊cpu的policy需要改變,每次policy改變時,該通知鏈上的回調將會用不同的事件參數被調用3次,分別是:

  • CPUFREQ_ADJUST 只要有需要,所有的被通知者可以在此時修改policy的限制信息,比如溫控系統可能會修改在大允許運行的頻率。
  • CPUFREQ_INCOMPATIBLE 只是爲了避免硬件錯誤的情況下,可以在該通知中修改policy的限制信息。
  • CPUFREQ_NOTIFY 真正切換policy前,該通知會發往所有的被通知者。
    transition通知鏈用於在驅動實施調整cpu的頻率時,用於通知相關的註冊者。每次調整頻率時,該通知會發出兩次通知事件:
  • CPUFREQ_PRECHANGE 調整前的通知。
  • CPUFREQ_POSTCHANGE 完成調整後的通知。
    當檢測到因系統進入suspend而造成頻率被改變時,以下通知消息會被髮出:
  • CPUFREQ_RESUMECHANGE

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

圖 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, 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, 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的代碼這裏就不展開了,我只給出它的序列圖:

圖 4.1 設置一個cpufreq_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的更新操作。

governor

在上一篇文章中,介紹了cpufreq的core層,core提供了cpufreq系統的初始化,公共數據結構的建立以及對cpufreq中其它子部件提供註冊功能。core的最核心功能是對policy的管理,一個policy通過cpufreq_policy結構中的governor字段,和某個governor相關聯,本章的內容正是要對governor進行討論。
通過前面兩篇文章的介紹,我們知道,governor的作用是:檢測系統的負載狀況,然後根據當前的負載,選擇出某個可供使用的工作頻率,然後把該工作頻率傳遞給cpufreq_driver,完成頻率的動態調節。內核默認提供了5種governor供我們使用,在之前的內核版本中,每種governor幾乎是獨立的代碼,它們各自用自己的方式實現對系統的負載進行監測,很多時候,檢測的邏輯其實是很相似的,各個governor最大的不同之處其實是根據檢測的結果,選擇合適頻率的策略。所以,爲了減少代碼的重複,在我現在分析的內核版本中(3.10.0),一些公共的邏輯代碼被單獨抽象出來,單獨用一個文件來實現:/drivers/cpufreq/cpufreq_governor.c,而各個具體的governor則分別有自己的代碼文件,如:cpufreq_ondemand.c,cpufreq_performance.c。下面我們先從公共部分討論。


1. 數據結構

cpu_dbs_common_info 該結構把對計算cpu負載需要使用到的一些輔助變量整合在了一起,通常,每個cpu都需要一個cpu_dbs_common_info結構體,該結構體中的成員會在governor的生命週期期間進行傳遞,以用於統計當前cpu的負載,它的定義如下:

/* Per cpu structures */
struct cpu_dbs_common_info {
        int cpu;
        u64 prev_cpu_idle;
        u64 prev_cpu_wall;
        u64 prev_cpu_nice;
        struct cpufreq_policy *cur_policy;
        struct delayed_work work;

        struct mutex timer_mutex;
        ktime_t time_stamp;
};
  • cpu 與該結構體相關聯的cpu編號。
  • prev_cpu_idle 上一次統計時刻該cpu停留在idle狀態的總時間。
  • prev_cpu_wall 上一次統計時刻對應的總工作時間。
  • cur_policy 指向該cpu所使用的cpufreq_policy結構。
  • work 工作隊列,該工作隊列會被定期地觸發,然後定期地進行負載的更新和統計工作。
    dbs縮寫,實際是:demand based switching,通常,因爲cpu_dbs_common_info只包含了經過抽象後的公共部分,所以,各個governor會自己定義的一個包含cpu_dbs_common_info的自定義結構,例如對於ondemand,他會定義:
struct od_cpu_dbs_info_s {
        struct cpu_dbs_common_info cdbs;
        struct cpufreq_frequency_table *freq_table;
        unsigned int freq_lo;
        unsigned int freq_lo_jiffies;
        unsigned int freq_hi_jiffies;
        unsigned int rate_mult;
        unsigned int sample_type:1;
};

而對於Conservative,他的定義如下:

struct cs_cpu_dbs_info_s {
        struct cpu_dbs_common_info cdbs;
        unsigned int down_skip;
        unsigned int requested_freq;
        unsigned int enable:1;
};

把它理解爲類似於C++語言的基類和子類的概念就是了。

common_dbs_data 各個獨立的governor,需要和governor的公共層交互,需要實現一套公共的接口,這個接口由common_dbs_data結構來提供:

struct common_dbs_data {
        /* Common across governors */
        #define GOV_ONDEMAND            0
        #define GOV_CONSERVATIVE        1
        int governor;
        struct attribute_group *attr_group_gov_sys; /* one governor - system */
        struct attribute_group *attr_group_gov_pol; /* one governor - policy */

        /* Common data for platforms that don't set have_governor_per_policy */
        struct dbs_data *gdbs_data;

        struct cpu_dbs_common_info *(*get_cpu_cdbs)(int cpu);
        void *(*get_cpu_dbs_info_s)(int cpu);
        void (*gov_dbs_timer)(struct work_struct *work);
        void (*gov_check_cpu)(int cpu, unsigned int load);
        int (*init)(struct dbs_data *dbs_data);
        void (*exit)(struct dbs_data *dbs_data);

        /* Governor specific ops, see below */
        void *gov_ops;
};

主要的字段意義如下:

  • governor 因爲ondemand和conservative的實現部分有很多相似的地方,用該字段做一區分,可以設置爲GOV_ONDEMAND或GOV_CONSERVATIVE的其中之一。
  • attr_group_gov_sys 該公共的sysfs屬性組。
  • attr_group_gov_pol 各policy使用的屬性組,有時候多個policy會使用同一個governor算法。
  • gdbs_data 通常,當沒有設置have_governor_per_policy時,表示所有的policy使用了同一種 - -
  • governor,該字段指向該governor的dbs_data結構。
  • get_cpu_cdbs 回調函數,公共層用它取得對應cpu的cpu_dbs_common_info結構指針。
  • get_cpu_dbs_info_s 回調函數,公共層用它取得對應cpu的cpu_dbs_common_info_s的派生結構指針,例如:od_cpu_dbs_info_s,cs_cpu_dbs_info_s。
  • gov_dbs_timer 前面說過,cpu_dbs_common_info_s結構中有一個工作隊列,該回調通常用作工作隊列的工作函數。
  • gov_check_cpu 計算cpu負載的回調函數,通常會直接調用公共層提供的dbs_check_cpu函數完成實際的計算工作。
  • init 初始化回調,用於完成該governor的一些額外的初始化工作。
  • exit 回調函數,governor被移除時調用。
  • gov_ops 各個governor可以用該指針定義各自特有的一些操作接口。
    dbs_data 該結構體通常由governor的公共層代碼在governor的初始化階段動態創建,該結構的一個最重要的字段就是cdata:一個common_dbs_data結構指針,另外,該結構還包含一些定義governor工作方式的一些調節參數。該結構的詳細定義如下:
struct dbs_data {
        struct common_dbs_data *cdata;
        unsigned int min_sampling_rate;
        int usage_count;
        void *tuners;

        /* dbs_mutex protects dbs_enable in governor start/stop */
        struct mutex mutex;
};

幾個主要的字段:

  • cdata 一個common_dbs_data結構指針,通常由具體governor的實現部分定義好,然後作爲參數,通過公共層的API:cpufreq_governor_dbs,傳遞到公共層,cpufreq_governor_dbs函數在創建好dbs_data結構後,把該指針賦值給該字段。
  • min_sampling_rate 用於記錄統計cpu負載的採樣週期。
  • usage_count 當沒有設置have_governor_per_policy時,意味着所有的policy採用同一個governor,該字段就是用來統計目前該governor被多少個policy引用。
  • tuners 指向governor的調節參數結構,不同的governor可以定義自己的tuner結構,公共層代碼會在governor的初始化階段調用common_dbs_data結構的init回調函數,governor的實現可以在init回調中初始化tuners字段。
    如果設置了have_governor_per_policy,每個policy擁有各自獨立的governor,也就是說,擁有獨立的dbs_data結構,它會記錄在cpufreq_policy結構的governor_data字段中,否則,如果沒有設置have_governor_per_policy,多個policy共享一個governor,和同一個dbs_data結構關聯,此時,dbs_data被賦值在common_dbs_data結構的gdbs_data字段中。

cpufreq_governor 這個結構在本系列文章的第一篇已經介紹過了,請參看Linux動態頻率調節系統CPUFreq之一:概述。幾個數據結構的關係如下圖所示:

圖 1.1 governor的數據結構關係

下面我們以ondemand這個系統已經實現的governor爲例,說明一下如何實現一個governor。具體的代碼請參看:/drivers/cpufreq/cpufreq_ondemand.c。


2. 定義一個governor

要實現一個governor,首先要定義一個cpufreq_governor結構,對於ondeman來說,它的定義如下:

struct cpufreq_governor cpufreq_gov_ondemand = {
        .name                   = "ondemand",
        .governor               = od_cpufreq_governor_dbs,
        .max_transition_latency = TRANSITION_LATENCY_LIMIT,
        .owner                  = THIS_MODULE,
};

其中,governor是這個結構的核心字段,cpufreq_governor註冊後,cpufreq的核心層通過該字段操縱這個governor的行爲,包括:初始化、啓動、退出等工作。現在,該字段被設置爲od_cpufreq_governor_dbs,我們看看它的實現:

static int od_cpufreq_governor_dbs(struct cpufreq_policy *policy,
                unsigned int event)
{
        return cpufreq_governor_dbs(policy, &od_dbs_cdata, event);
}

只是簡單地調用了governor的公共層提供的API:cpufreq_governor_dbs,關於這個API,我們在後面會逐一進行展開,這裏我們注意到參數:&od_dbs_cdata,正是我們前面討論過得common_dbs_data結構,作爲和governor公共層的接口,在這裏它的定義如下:

static struct common_dbs_data od_dbs_cdata = {
        .governor = GOV_ONDEMAND,
        .attr_group_gov_sys = &od_attr_group_gov_sys,
        .attr_group_gov_pol = &od_attr_group_gov_pol,
        .get_cpu_cdbs = get_cpu_cdbs,
        .get_cpu_dbs_info_s = get_cpu_dbs_info_s,
        .gov_dbs_timer = od_dbs_timer,
        .gov_check_cpu = od_check_cpu,
        .gov_ops = &od_ops,
        .init = od_init,
        .exit = od_exit,
};

這裏先介紹一下get_cpu_cdbs和get_cpu_dbs_info_s這兩個回調,前面介紹cpu_dbs_common_info_s結構的時候已經說過,各個governor需要定義一個cpu_dbs_common_info_s結構的派生結構,對於ondemand來說,這個派生結構是:od_cpu_dbs_info_s。兩個回調函數分別用來獲得基類和派生類這兩個結構的指針。我們先看看od_cpu_dbs_info_s是如何定義的:

static DEFINE_PER_CPU(struct od_cpu_dbs_info_s, od_cpu_dbs_info);

沒錯,它被定義爲了一個per_cpu變量,也就是說,每個cpu擁有各自獨立的od_cpu_dbs_info_s,這很正常,因爲每個cpu需要的實時負載是不一樣的,需要獨立的上下文變量來進行負載的統計。前面也已經列出了od_cpu_dbs_info_s的聲明,他的第一個字段cdbs就是一個cpu_dbs_common_info_s結構。內核爲我們提供了一個輔助宏來幫助我們定義get_cpu_cdbs和get_cpu_dbs_info_s這兩個回調函數:

#define define_get_cpu_dbs_routines(_dbs_info)                          \
static struct cpu_dbs_common_info *get_cpu_cdbs(int cpu)                \
{                                                                       \
        return &per_cpu(_dbs_info, cpu).cdbs;                           \
}                                                                       \
                                                                        \
static void *get_cpu_dbs_info_s(int cpu)                                \
{                                                                       \
        return &per_cpu(_dbs_info, cpu);                                \
}      

所以,在cpufreq_ondemand.c中,我們只要簡單地使用上述的宏即可定義這兩個回調:

define_get_cpu_dbs_routines(od_cpu_dbs_info);  

經過上述這一系列的定義以後,governor的公共層即可通過這兩個回調獲取各個cpu所對應的cpu_dbs_common_info_s和od_cpu_dbs_info_s的結構指針,用來記錄本次統計週期的一些上下文參數(idle時間和運行時間等等)。


3. 初始化一個governor

當一個governor被policy選定後,核心層會通過__cpufreq_set_policy函數對該cpu的policy進行設定,參看 Linux動態頻率調節系統CPUFreq之二:核心(core)架構與API中的第4節和圖4.1。如果policy認爲這是一個新的governor(和原來使用的舊的governor不相同),policy會通過__cpufreq_governor函數,並傳遞CPUFREQ_GOV_POLICY_INIT參數,而__cpufreq_governor函數實際上是調用cpufreq_governor結構中的governor回調函數,在第2節中我們已經知道,這個回調最後會進入governor公共API:cpufreq_governor_dbs,下面是它收到CPUFREQ_GOV_POLICY_INIT參數時,經過簡化後的代碼片段:

        case CPUFREQ_GOV_POLICY_INIT:
                ......

                dbs_data = kzalloc(sizeof(*dbs_data), GFP_KERNEL);
                ......

                dbs_data->cdata = cdata;
                dbs_data->usage_count = 1;
                rc = cdata->init(dbs_data);
                ......

                rc = sysfs_create_group(get_governor_parent_kobj(policy),
                                get_sysfs_attr(dbs_data));
                ......

                policy->governor_data = dbs_data;

                ......
                /* Bring kernel and HW constraints together */
                dbs_data->min_sampling_rate = max(dbs_data->min_sampling_rate,
                                MIN_LATENCY_MULTIPLIER * latency);
                set_sampling_rate(dbs_data, max(dbs_data->min_sampling_rate,
                                        latency * LATENCY_MULTIPLIER));
                if ((cdata->governor == GOV_CONSERVATIVE) &&
                                (!policy->governor->initialized)) {
                        struct cs_ops *cs_ops = dbs_data->cdata->gov_ops;

                        cpufreq_register_notifier(cs_ops->notifier_block,
                                        CPUFREQ_TRANSITION_NOTIFIER);
                }

                if (!have_governor_per_policy())
                        cdata->gdbs_data = dbs_data;

                return 0;

首先,它會給這個policy分配一個dbs_data實例,然後把通過參數cdata傳入的common_dbs_data指針,賦值給它的cdata字段,這樣,policy就可以通過該字段獲得governor的操作接口(通過cdata的一系列回調函數)。然後,調用cdata的init回調函數,對這個governor做進一步的初始化工作,對於ondemand來說,init回調的實際執行函數是:od_init,主要是完成和governor相關的一些調節參數的初始化,然後把初始化好的od_dbs_tuners結構指針賦值到dbs_data的tuners字段中,它的詳細代碼這裏就不貼出了。接着,通過sysfs_create_group函數,建立該governor在sysfs中的節點,以後我們就可以通過這些節點對該governor的算法邏輯進行微調,ondemand在我的電腦中,建立了以下這些節點(sys/devices/system/cpu/cpufreq/ondemand):
sampling_rate;
io_is_busy;
up_threshold;
sampling_down_factor;
ignore_nice;
powersave_bias;
sampling_rate_min;

繼續,把初始化好的dbs_data結構賦值給policy的governor_data字段,以方便以後的訪問。最後是通過set_sampling_rate設置governor的採樣週期,如果還有設置have_governor_per_policy,把dbs_data結構指針賦值給cdata結構的gdbs_data字段,至此,governor的初始化工作完成,下面是整個過程的序列圖:

圖 3.1 governor的初始化


4. 啓動一個governor

核心層會通過__cpufreq_set_policy函數,通過CPUFREQ_GOV_POLICY_INIT參數,在公共層的API:cpufreq_governor_dbs中,完成了對governor的初始化工作,緊接着,__cpufreq_set_policy會通過CPUFREQ_GOV_START參數,和初始化governor的流程一樣,最終會到達cpufreq_governor_dbs函數中,我們看看它是如何啓動一個governor的:

        case CPUFREQ_GOV_START:
                if (!policy->cur)
                        return -EINVAL;

                mutex_lock(&dbs_data->mutex);

                for_each_cpu(j, policy->cpus) {
                        struct cpu_dbs_common_info *j_cdbs =
                                dbs_data->cdata->get_cpu_cdbs(j);

                        j_cdbs->cpu = j;
                        j_cdbs->cur_policy = policy;
                        j_cdbs->prev_cpu_idle = get_cpu_idle_time(j,
                                               &j_cdbs->prev_cpu_wall, io_busy);
                        if (ignore_nice)
                                j_cdbs->prev_cpu_nice =
                                        kcpustat_cpu(j).cpustat[CPUTIME_NICE];

                        mutex_init(&j_cdbs->timer_mutex);
                        INIT_DEFERRABLE_WORK(&j_cdbs->work,
                                             dbs_data->cdata->gov_dbs_timer);
                }

首先,遍歷使用該policy的所有的處於online狀態的cpu,針對每一個cpu,做以下動作:

  • 取出該cpu相關聯的cpu_dbs_common_info結構指針,之前已經討論過,governor定義了一個per_cpu變量來定義各個cpu所對應的cpu_dbs_common_info結構,通過common_dbs_data結構的回調函數可以獲取該結構的指針。

  • 初始化cpu_dbs_common_info結構的cpu,cur_policy,prev_cpu_idle,prev_cpu_wall,prev_cpu_nice字段,其中,prev_cpu_idle,prev_cpu_wall這兩個字段會被以後的負載計算所使用。

  • 爲每個cpu初始化一個工作隊列,工作隊列的執行函數是common_dbs_data結構中的gov_dbs_timer字段所指向的回調函數,對於ondemand來說,該函數是:od_dbs_timer。這個工作隊列會被按照設定好的採樣率定期地被喚醒,進行cpu負載的統計工作。

然後,記錄目前的時間戳,調度初始化好的工作隊列在稍後某個時間點運行:

               /* Initiate timer time stamp */
                cpu_cdbs->time_stamp = ktime_get();

                gov_queue_work(dbs_data, policy,
                                delay_for_sampling_rate(sampling_rate), true);

下圖表達了啓動一個governor的過程:

圖 4.1 啓動一個governor

工作隊列被調度執行後,會在工作隊列的執行函數中進行cpu負載的統計工作,這個我們在下一節中討論。


5. 系統負載的檢測

上一節我們提到,核心層啓動一個governor後,會在每個使用該governor的cpu上建立一個工作隊列,工作隊列的執行函數是在common_dbs_data中gov_dbs_timer字段所指向的函數,理所當然,該函數由各個governor的具體代碼來實現,對於ondemand governor,它的實現函數是od_dbs_timer。governor的公共層代碼爲我們提供了一個API:dbs_check_cpu,該API用來計算兩個統計週期期間某個cpu的負載情況,我們先分析一下dbs_check_cpu:

void dbs_check_cpu(struct dbs_data *dbs_data, int cpu)
{
        struct cpu_dbs_common_info *cdbs = dbs_data->cdata->get_cpu_cdbs(cpu);
        ......
        policy = cdbs->cur_policy;

        /* Get Absolute Load (in terms of freq for ondemand gov) */
        for_each_cpu(j, policy->cpus) {
                struct cpu_dbs_common_info *j_cdbs;
                ......

                j_cdbs = dbs_data->cdata->get_cpu_cdbs(j);

                ......
                cur_idle_time = get_cpu_idle_time(j, &cur_wall_time, io_busy);

                wall_time = (unsigned int)
                        (cur_wall_time - j_cdbs->prev_cpu_wall);
                j_cdbs->prev_cpu_wall = cur_wall_time;

                idle_time = (unsigned int)
                        (cur_idle_time - j_cdbs->prev_cpu_idle);
                j_cdbs->prev_cpu_idle = cur_idle_time;
                ......

                load = 100 * (wall_time - idle_time) / wall_time;
                ......
                load *= cur_freq;    /* 實際的代碼不是這樣,爲了簡化討論,精簡爲實際的計算邏輯*/

                if (load > max_load)
                        max_load = load;
        }

        dbs_data->cdata->gov_check_cpu(cpu, max_load);
}

由代碼可以看出,遍歷該policy下每個online的cpu,取出該cpu對應的cpu_dbs_common_info結構,該結構中的prev_cpu_idle和prev_cpu_wall保存有上一次採樣週期時記錄的idle時間和運行時間,負載的計算其實很簡單:

  • idle_time = 本次idle時間 - 上次idle時間;
  • wall_time = 本次總運行時間 - 上次總運行時間;
  • 負載load = 100 * (wall_time - idle_time)/ wall_time;
  • 把所有cpu中,負載最大值記入max_load中,作爲選擇頻率的依據;

計算出最大負載max_load後,調用具體governor實現的gov_check_cpu回調函數,對於ondemand來說,該回調函數是:od_check_cpu,我們跟進去看看:

static void od_check_cpu(int cpu, unsigned int load_freq)
{
        struct od_cpu_dbs_info_s *dbs_info = &per_cpu(od_cpu_dbs_info, cpu);
        struct cpufreq_policy *policy = dbs_info->cdbs.cur_policy;
        struct dbs_data *dbs_data = policy->governor_data;
        struct od_dbs_tuners *od_tuners = dbs_data->tuners;

        dbs_info->freq_lo = 0;

        /* Check for frequency increase */
        if (load_freq > od_tuners->up_threshold * policy->cur) {
                /* If switching to max speed, apply sampling_down_factor */
                if (policy->cur < policy->max)
                        dbs_info->rate_mult =
                                od_tuners->sampling_down_factor;
                dbs_freq_increase(policy, policy->max);
                return;
        }

當負載比預設的閥值高時(od_tuners->up_threshold,默認值是95%),立刻選擇該policy最大的工作頻率作爲接下來的工作頻率。如果負載沒有達到預設的閥值,但是當前頻率已經是最低頻率了,則什麼都不做,直接返回:

        if (policy->cur == policy->min)
                return;

運行到這裏,cpu的頻率可能已經在上面的過程中被設置爲最大頻率,實際上我們可能並不需要這麼高的頻率,所以接着判斷,當負載低於另一個預設值時,這時需要計算一個合適於該負載的新頻率:

        if (load_freq < od_tuners->adj_up_threshold
                        * policy->cur) {
                unsigned int freq_next;
                freq_next = load_freq / od_tuners->adj_up_threshold;

                /* No longer fully busy, reset rate_mult */
                dbs_info->rate_mult = 1;

                if (freq_next < policy->min)
                        freq_next = policy->min;

                if (!od_tuners->powersave_bias) {
                        __cpufreq_driver_target(policy, freq_next,
                                        CPUFREQ_RELATION_L);
                        return;
                }

                freq_next = od_ops.powersave_bias_target(policy, freq_next,
                                        CPUFREQ_RELATION_L);
                __cpufreq_driver_target(policy, freq_next, CPUFREQ_RELATION_L);
        }
}

對於ondemand來說,因爲傳入的負載是乘上了當前頻率後的歸一化值,所以計算新頻率時,直接用load_freq除以想要的負載即可。本來計算出來的頻率直接通過__cpufreq_driver_target函數,交給cpufreq_driver調節頻率即可,但是這裏的處理考慮了powersave_bias的設置情況,當設置了powersave_bias時,表明我們爲了進一步節省電力,我們希望在計算出來的新頻率的基礎上,再乘以一個powersave_bias設定的百分比,作爲真正的運行頻率,powersave_bias的值從0-1000,每一步代表0.1%。實際的情況比想象中稍微複雜一點,考慮到乘以一個powersave_bias後的新頻率可能不在cpu所支持的頻率表中,ondemand算法會在頻率表中查找,分別找出最接近新頻率的一個區間,由高低兩個頻率組成,低的頻率記入od_cpu_dbs_info_s結構的freq_lo字段中,高的頻率通過od_ops.powersave_bias_target回調返回。同時,od_ops.powersave_bias_target回調函數還計算出高低兩個頻率應該運行的時間,分別記入od_cpu_dbs_info_s結構的freq_hi_jiffies和freq_low_jiffies字段中。原則是,通過兩個不同頻率的運行時間的組合,使得綜合結果接近我們想要的目標頻率。詳細的計算邏輯請參考函數:generic_powersave_bias_target。
討論完上面兩個函數,讓我們回到本節的開頭,負載的計算工作是在一個工作隊列中發起的,前面說過,ondemand對應的工作隊列的工作函數是od_dbs_timer,我們看看他的實現代碼:

static void od_dbs_timer(struct work_struct *work)
{
        ......

        /* Common NORMAL_SAMPLE setup */
        core_dbs_info->sample_type = OD_NORMAL_SAMPLE;
        if (sample_type == OD_SUB_SAMPLE) {
                delay = core_dbs_info->freq_lo_jiffies;
                __cpufreq_driver_target(core_dbs_info->cdbs.cur_policy,
                                core_dbs_info->freq_lo, CPUFREQ_RELATION_H);
        } else {
                dbs_check_cpu(dbs_data, cpu);
                if (core_dbs_info->freq_lo) {
                        /* Setup timer for SUB_SAMPLE */
                        core_dbs_info->sample_type = OD_SUB_SAMPLE;
                        delay = core_dbs_info->freq_hi_jiffies;
                }
        }

max_delay:
        if (!delay)
                delay = delay_for_sampling_rate(od_tuners->sampling_rate
                                * core_dbs_info->rate_mult);

        gov_queue_work(dbs_data, dbs_info->cdbs.cur_policy, delay, modify_all);
        mutex_unlock(&core_dbs_info->cdbs.timer_mutex);
}

如果sample_type是OD_SUB_SAMPLE時,表明上一次採樣時,需要用高低兩個頻率來模擬實際的目標頻率中的第二步:需要運行freq_lo,並且持續時間爲freq_lo_jiffies。否則,調用公共層計算負載的API:dbs_check_cpu,開始一次新的採樣,當powersave_bias沒有設置時,該函數返回前,所需要的新的目標頻率會被設置,考慮到powersave_bias的設置情況,判斷一下如果freq_lo被設置,說明需要用高低兩個頻率來模擬實際的目標頻率,高頻率已經在dbs_check_cpu返回前被設置(實際的設置工作是在od_check_cpu中),所以把sample_type設置爲OD_SUB_SAMPLE,以便下一次運行工作函數進行採樣時可以設置低頻率運行。最後,調度工作隊列在下一個採樣時刻再次運行,這樣,cpu的工作頻率實現了在每個採樣週期,根據實際的負載情況,動態地設定合適的工作頻率進行運行,既滿足了性能的需求,也降低了系統的功耗,達到了cpufreq系統的最終目的,整個流程可以參考下

圖 5.1 負載計算和頻率選擇

5. 各種Governors說明

1.Performance governor: Highest frequency

  • 設置當前處理的頻率到最高頻率,後一直保持在這個頻率
  • 這個Governors目標就是讓系統輸出最高performance

2.Powersave Governor:Lowest Frequency

  • 設置當前處理器頻率在最低的頻率,然後保持在這一頻率
  • 目標就是設置當設置當前處理器的頻率在最低的頻率,不管系統有多忙都不提高處理器頻率。
  • 這個Governor看着會省電,但實際並不會省。因爲performance降低,處理task時間變長,所以相比其他,其進入睡眠的時間也最慢
    3.Userspace governor: Manual frequencies
  • 允許用戶手動選擇一組頻率
  • 這種模式通常與運行在userspace的daemon進程配合,去控制處理器頻率

4.Ondemand governor: Frequency change based on processor use

  • 在2.6.10內核開始使用,這也是第一個根據cpu使用狀況來動態調整cpu頻率的governor
  • Ondemand governor會檢查當前cpu使用情況,如果其load超過某一閾值(threshold),就會把當前cpu頻率設定到最高的頻率。然後發現load低於閾值就會逐步降低cpu頻率。

5.Conservative governor: A more gradual ondemand

  • 這個governor基於ondemand governor,從2.6.12內核開始導入
  • 它會檢查cpu load高於或者低於某一閾值,如果cpu load比閾值高,則逐步提高cpu頻率,如果發現cpu load比閾值低,則逐步減小cpu頻率

    6.interactive governor : 看後面具體的介紹

6.詳細介紹每種governor

CPU Load計算方法:

load_freq = load * current frequency
if (load_freq > (up_threshold * current frequency)) 
        next_freq = max_frequency (CPUFREQ_RELATION_H)
if (load_freq < ((up_threshold - down_differential) * current frequency))
        next_freq = load_freq / (up_threshold - down_differential) (CPUFREQ_RELATION_L)

Ex) if load=30, current frequency=800MHz
next_freq= (30 * 800MHz) / (80 - 10) = 342MHz
CPUFREQ_RELATION_L means higher than or equal target frequency
 Select 500MHz

6.1 Ondemand governor

  • 如果cpu load超過某一閾值(up_threashould),ondemand governor會把cpu頻率提高到最高(scaling_max_freq)。up_threshold的默認值爲80.
  • 如果cpu load低於up_threshold,ondemand governor會把cpu頻率降低一檔。最低可以到scaling_min_freq。
  • 每個sampling_rate(默認40ms),每個cpu load都會被重新計算
  • ignore_nice: 有正數nice值的cpu的load,將不會被計入到整個cpu load當中去。
  • powersave_bias: 有些更強調省電的場景下,設置這個值(0.1~100%)可以減小cpu頻率以達到省電的目的。

6.2 Interactive governor

  • Designed for latency-sensitive, interactive workloads
  • It is more aggressive about scaling the CPU speed up in response to CPU-intensive activity.
    • Instead of sampling the CPU at a specified rate, the interactive governor will check whether to scale the CPU frequency up soon after coming out of idle.
  • min_sample_time: The minimum amount of time to spend at the current frequency before ramping down. This is to ensure that the governor has seen enough historic CPU load data to determine the appropriate workload. Default is 80000 us.
  • go_maxspeed_load: The CPU load at which to ramp to max speed. Default is 85.

The relation between NO_HZ and timer used on CPUFREQ

  • If NO_HZ is enabled, PERIODIC Timer(periodic tick, tick_sched_timer()) isn’t happened during IDLE state. But, If user registers ONESHOT Timer on each device, ONESHOT Timer is happened during IDLE state.
  • If CPUFREQ governor uses the work-queue to check CPU load periodically, work-queue is executed during IDLE with NO_Hz. Because work-queue is not Timer.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章