轉自:http://oenhan.com/cpu-load-balance
還是神奇的進程調度問題引發的,參看Linux進程組調度機制分析,組調度機制是看清楚了,發現在重啟過程中,很多內核調用棧阻塞在了double_rq_lock函數上,而double_rq_lock則是load_balance觸發的,懷疑當時的核間調度出現了問題,在某個負責場景下產生了多核互鎖,后面看了一下CPU負載平衡下的代碼實現,寫一下總結。
內核代碼版本:kernel-3.0.13-0.27。
內核代碼函數起自load_balance函數,從load_balance函數看引用它的函數可以一直找到schedule函數這里,便從這里開始往下看,在__schedule中有下面一句話。
1
2
|
if (unlikely(!rq->nr_running))
idle_balance(cpu, rq);
|
從上面可以看出什么時候內核會嘗試進行CPU負載平衡:即當前CPU運行隊列為NULL的時候。
CPU負載平衡有兩種方式:pull和push,即空閑CPU從其他忙的CPU隊列中拉一個進程到當前CPU隊列;或者忙的CPU隊列將一個進程推送到空閑的CPU隊列中。idle_balance干的則是pull的事情,具體push下面會提到。
在idle_balance里面,有一個proc閥門控制當前CPU是否pull:
1
2
|
if (this_rq->avg_idle < sysctl_sched_migration_cost)
return;
|
sysctl_sched_migration_cost對應proc控制文件是/proc/sys/kernel/sched_migration_cost,開關代表如果CPU隊列空閑了500us(sysctl_sched_migration_cost默認值)以上,則進行pull,否則則返回。
for_each_domain(this_cpu, sd) 則是遍歷當前CPU所在的調度域,可以直觀的理解成一個CPU組,類似task_group,核間平衡指組內的平衡。負載平衡有一個矛盾就是:負載平衡的頻度和CPU cache的命中率是矛盾的,CPU調度域就是將各個CPU分成層次不同的組,低層次搞定的平衡就絕不上升到高層次處理,避免影響cache的命中率。
最終通過load_balance進入正題。
首先通過find_busiest_group獲取當前調度域中的最忙的調度組,首先update_sd_lb_stats更新sd的狀態,也就是遍歷對應的sd,將sds里面的結構體數據填滿,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
struct sd_lb_stats {
struct sched_group *busiest; /* Busiest group in this sd */
struct sched_group *this; /* Local group in this sd */
unsigned long total_load; /* Total load of all groups in sd */
unsigned long total_pwr; /* Total power of all groups in sd */
unsigned long avg_load; /* Average load across all groups in sd */
/** Statistics of this group */
unsigned long this_load; //當前調度組的負載
unsigned long this_load_per_task; //當前調度組的平均負載
unsigned long this_nr_running; //當前調度組內運行隊列中進程的總數
unsigned long this_has_capacity;
unsigned int this_idle_cpus;
/* Statistics of the busiest group */
unsigned int busiest_idle_cpus;
unsigned long max_load; //最忙的組的負載量
unsigned long busiest_load_per_task; //最忙的組中平均每個任務的負載量
unsigned long busiest_nr_running; //最忙的組中所有運行隊列中進程的個數
unsigned long busiest_group_capacity;
unsigned long busiest_has_capacity;
unsigned int busiest_group_weight;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
do
{
local_group = cpumask_test_cpu(this_cpu, sched_group_cpus(sg));
if (local_group) {
//如果是當前CPU上的group,則進行賦值
sds->this_load = sgs.avg_load;
sds->this = sg;
sds->this_nr_running = sgs.sum_nr_running;
sds->this_load_per_task = sgs.sum_weighted_load;
sds->this_has_capacity = sgs.group_has_capacity;
sds->this_idle_cpus = sgs.idle_cpus;
} else if (update_sd_pick_busiest(sd, sds, sg, &sgs, this_cpu)) {
//在update_sd_pick_busiest判斷當前sgs的是否超過了之前的最大值,如果是
//則將sgs值賦給sds
sds->max_load = sgs.avg_load;
sds->busiest = sg;
sds->busiest_nr_running = sgs.sum_nr_running;
sds->busiest_idle_cpus = sgs.idle_cpus;
sds->busiest_group_capacity = sgs.group_capacity;
sds->busiest_load_per_task = sgs.sum_weighted_load;
sds->busiest_has_capacity = sgs.group_has_capacity;
sds->busiest_group_weight = sgs.group_weight;
sds->group_imb = sgs.group_imb;
}
sg = sg->next;
} while (sg != sd->groups);
|
決定選擇調度域中最忙的組的參照標准是該組內所有 CPU上負載(load) 的和, 找到組中找到忙的運行隊列的參照標准是該CPU運行隊列的長度, 即負載,並且 load 值越大就表示越忙。在平衡的過程中,通過比較當前隊列與以前記錄的busiest 的負載情況,及時更新這些變量,讓 busiest 始終指向域內最忙的一組,以便於查找。
調度域的平均負載計算
1
2
3
|
sds.avg_load = (SCHED_POWER_SCALE * sds.total_load) / sds.total_pwr;
if (sds.this_load >= sds.avg_load)
goto out_balanced;
|
在比較負載大小的過程中, 當發現當前運行的CPU所在的組中busiest為空時,或者當前正在運行的 CPU隊列就是最忙的時, 或者當前 CPU隊列的負載不小於本組內的平均負載時,或者不平衡的額度不大時,都會返回 NULL 值,即組組之間不需要進行平衡;當最忙的組的負載小於該調度域的平均負載時,只需要進行小范圍的負載平衡;當要轉移的任務量小於每個進程的平均負載時,如此便拿到了最忙的調度組。
然后find_busiest_queue中找到最忙的調度隊列,遍歷該組中的所有 CPU 隊列,經過依次比較各個隊列的負載,找到最忙的那個隊列。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
for_each_cpu(i, sched_group_cpus(group)) {
/*rq->cpu_power表示所在處理器的計算能力,在函式sched_init初始化時,會把這值設定為SCHED_LOAD_SCALE (=Nice 0的Load Weight=1024).並可透過函式update_cpu_power (in kernel/sched_fair.c)更新這個值.*/
unsigned long power = power_of(i);
unsigned long capacity = DIV_ROUND_CLOSEST(power,SCHED_POWER_SCALE);
unsigned long wl;
if (!cpumask_test_cpu(i, cpus))
continue;
rq = cpu_rq(i);
/*獲取隊列負載cpu_rq(cpu)->load.weight;*/
wl = weighted_cpuload(i);
/*
* When comparing with imbalance, use weighted_cpuload()
* which is not scaled with the cpu power.
*/
if (capacity && rq->nr_running == 1 && wl > imbalance)
continue;
/*
* For the load comparisons with the other cpu's, consider
* the weighted_cpuload() scaled with the cpu power, so that
* the load can be moved away from the cpu that is potentially
* running at a lower capacity.
*/
wl = (wl * SCHED_POWER_SCALE) / power;
if (wl > max_load) {
max_load = wl;
busiest = rq;
}
|
通過上面的計算,便拿到了最忙隊列。
當busiest->nr_running運行數大於1的時候,進行pull操作,pull前對move_tasks,先進行double_rq_lock加鎖處理。
1
2
3
4
|
double_rq_lock(this_rq, busiest);
ld_moved = move_tasks(this_rq, this_cpu, busiest,
imbalance, sd, idle, &all_pinned);
double_rq_unlock(this_rq, busiest);
|
move_tasks進程pull task是允許失敗的,即move_tasks->balance_tasks,在此處,有sysctl_sched_nr_migrate開關控制進程遷移個數,對應proc的是/proc/sys/kernel/sched_nr_migrate。
下面有can_migrate_task函數檢查選定的進程是否可以進行遷移,遷移失敗的原因有3個,1.遷移的進程處於運行狀態;2.進程被綁核了,不能遷移到目標CPU上;3.進程的cache仍然是hot,此處也是為了保證cache命中率。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/*關於cache cold的情況下,如果遷移失敗的個數太多,仍然進行遷移
* Aggressive migration if:
* 1) task is cache cold, or
* 2) too many balance attempts have failed.
*/
tsk_cache_hot = task_hot(p, rq->clock_task, sd);
if (!tsk_cache_hot ||
sd->nr_balance_failed > sd->cache_nice_tries) {
#ifdef CONFIG_SCHEDSTATS
if (tsk_cache_hot) {
schedstat_inc(sd, lb_hot_gained[idle]);
schedstat_inc(p, se.statistics.nr_forced_migrations);
}
#endif
return 1;
}
|
判斷進程cache是否有效,判斷條件,進程的運行的時間大於proc控制開關sysctl_sched_migration_cost,對應目錄/proc/sys/kernel/sched_migration_cost_ns
1
2
3
4
5
6
7
|
static int
task_hot(struct task_struct *p, u64 now, struct sched_domain *sd)
{
s64 delta;
delta = now - p->se.exec_start;
return delta < (s64)sysctl_sched_migration_cost;
}
|
在load_balance中,move_tasks返回失敗也就是ld_moved==0,其中sd->nr_balance_failed++對應can_migrate_task中的"too many balance attempts have failed",然后busiest->active_balance = 1設置,active_balance = 1。
1
2
3
4
5
|
if (active_balance)
//如果pull失敗了,開始觸發push操作
stop_one_cpu_nowait(cpu_of(busiest),
active_load_balance_cpu_stop, busiest,
&busiest->active_balance_work);
|
push整個觸發操作代碼機制比較繞,stop_one_cpu_nowait把active_load_balance_cpu_stop添加到cpu_stopper每CPU變量的任務隊列里面,如下:
1
2
3
4
5
6
|
void stop_one_cpu_nowait(unsigned int cpu, cpu_stop_fn_t fn, void *arg,
struct cpu_stop_work *work_buf)
{
*work_buf = (struct cpu_stop_work){ .fn = fn, .arg = arg, };
cpu_stop_queue_work(&per_cpu(cpu_stopper, cpu), work_buf);
}
|
而cpu_stopper則是cpu_stop_init函數通過cpu_stop_cpu_callback創建的migration內核線程,觸發任務隊列調度。因為migration內核線程是綁定每個核心上的,進程遷移失敗的1和3問題就可以通過push解決。active_load_balance_cpu_stop則調用move_one_task函數遷移指定的進程。
上面描述的則是整個pull和push的過程,需要補充的pull觸發除了schedule后觸發,還有scheduler_tick通過觸發中斷,調用run_rebalance_domains再調用rebalance_domains觸發,不再細數。
1
2
3
4
|
void __init sched_init(void)
{
open_softirq(SCHED_SOFTIRQ, run_rebalance_domains);
}
|
鏈接為:http://oenhan.com/cpu-load-balance