在上一篇文章中,介紹了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,他會定義:app
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結構來提供:spa
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; };
主要的字段意義以下:3d
- 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結構後,把該指針賦值給該字段。code
-
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之一:概述。幾個數據結構的關係以下圖所示:
下面咱們以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的初始化工做完成,下面是整個過程的序列圖:
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的過程:
工做隊列被調度執行後,會在工做隊列的執行函數中進行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系統的最終目的,整個流程能夠參考下圖: