一、概述
1. 負載均衡模塊主要分兩個軟件層次:核心負載均衡模塊 和 class-specific均衡模塊。內核對不同的類型的任務有不同的均衡策略,普通的CFS任務和RT、Deadline任務處理方式是不同的。本文主要講述CFS任務的均衡。
二、負載均衡的場景
CFS任務負載均衡主要涉及下面三個場景:
1. 任務放置(task placement)
當阻塞的任務被喚醒的時候,確定該任務應該放置在那個CPU上執行。任務放置主要發生在下面三個場景:
(1) 喚醒一個新fork的線程
SYSCALL_DEFINE0(fork) //fork.c kernel_clone wake_up_new_task //core.c __set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0)); //為新fork的任務選核 activate_task(rq, p, ENQUEUE_NOCLOCK); //將任務queue到rq上 trace_sched_wakeup_new(p); check_preempt_curr(rq, p, WF_FORK); //觸發一次搶占 //其中trace打印p的信息: RxComputationTh-9555 [001] d..2 171682.441405: sched_wakeup_new: comm=RxComputationTh pid=9609 prio=120 target_cpu=002
(2) exec一個線程的時候
SYSCALL_DEFINE3(execve, const char __user *, filename, const char __user *const __user *, argv, const char __user *const __user *, envp) //fs/exec.c do_execve do_execveat_common bprm_execve sched_exec //core.c dest_cpu = current->sched_class->select_task_rq(p, task_cpu(p), SD_BALANCE_EXEC, 0) //此時是正在為正在執行execve系統調用的任務重新選核 stop_one_cpu(task_cpu(p), migration_cpu_stop, &arg); //若是新選的核和正在運行的這個核不是同一個cpu,向任務p正在運行的cpu對應的stop調度類的"migration/X"線程queue一個work,觸發主動遷移 migration_cpu_stop __migrate_task(rq, &rf, p, arg->dest_cpu); //遷移到dst cpu上 exec_binprm trace_sched_process_exec(current, old_pid, bprm);
當執行"cat trace_pipe"命令時,實際上是先fork一個sh的子任務,然后再在子任務中執行系統調用execve裝載"/system/bin/cat"文件,並為正在執行的當前任務重新選核,然后轉為執行cat命令的代碼,也就是誰會為shell命令執行兩次選核。
/sys/kernel/tracing # cat trace_pipe sh-9360 [006] d..2 173370.376830: sched_wakeup_new: comm=sh pid=10752 prio=120 target_cpu=001 cat-10752 [001] .... 173370.379998: sched_process_exec: filename=/system/bin/cat pid=10752 old_pid=10752 //三個pid相等,同一個任務
(3) 喚醒一個阻塞的進程
在上面的三個場景中都會調用 select_task_rq 來為task選擇一個合適的CPU。
wake_up_process //core.c 主要用於各驅動中喚醒任務 wake_up_state //用戶空間鎖、signal、ptrace、swait default_wake_function //waitqueue機制默認喚醒函數、select機制 try_to_wake_up //core.c trace_sched_waking(p) //此時打印的cpu還是任務上次運行的cpu cpu = select_task_rq(p, p->wake_cpu, SD_BALANCE_WAKE, wake_flags) if (task_cpu(p) != cpu) { //新選出的cpu和任務p之前運行的cpu不是同一個cpu wake_flags |= WF_MIGRATED; set_task_cpu(p, cpu); p->sched_class->migrate_task_rq(p, new_cpu); //migrate_task_rq_fair只是主要做一些虛擬時間的修正操作 __set_task_cpu(p, new_cpu); //只是將p->wake_cpu = cpu; p->cpu = cpu; } ttwu_queue(p, cpu, wake_flags); ttwu_queue_wakelist(p, cpu, wake_flags) //若執行喚醒的cpu和目標cpu不在同一個cluster內,走這個分支 __ttwu_queue_wakelist(p, cpu, wake_flags) p->sched_remote_wakeup = !!(wake_flags & WF_MIGRATED); rq->ttwu_pending = 1; __smp_call_single_queue(cpu, &p->wake_entry.llist) //將任務p掛在目標cpu的per-cpu的 call_single_queue 上 send_call_function_single_ipi(cpu) //對目標cpu發生ipi中斷() arch_send_call_function_single_ipi smp_cross_call(cpumask_of(cpu), IPI_CALL_FUNC); //觸發目標cpu的ipi中斷 do_handle_IPI //目標cpu收到ipi中斷 generic_smp_call_function_single_interrupt flush_smp_call_function_queue(true) sched_ttwu_pending //kernel/smp.c 應該會執行這里,待求證 ttwu_do_activate(rq, p, p->sched_remote_wakeup ? WF_MIGRATED : 0, &rf); //sched/core.c 目標cpu上執行的 ttwu_do_activate(rq, p, wake_flags, &rf) //若執行喚醒的cpu和目標cpu在同一個cluster內走這個分支,傳參為目標cpu的rq int en_flags = ENQUEUE_WAKEUP | ENQUEUE_NOCLOCK; if (wake_flags & WF_SYNC) en_flags |= ENQUEUE_WAKEUP_SYNC; if (wake_flags & WF_MIGRATED) en_flags |= ENQUEUE_MIGRATED; activate_task(rq, p, en_flags); enqueue_task(rq, p, flags); p->on_rq = TASK_ON_RQ_QUEUED; ttwu_do_wakeup(rq, p, wake_flags, rf); //傳參為目標cpu的rq check_preempt_curr(rq, p, wake_flags); check_preempt_wakeup //喚醒者和被喚醒者屬於同一調度類,走這個分支,若都是CFS任務就是這個函數(只看CFS) resched_curr(rq) //被喚醒者和curr和buddy PK 虛擬時間看是否需要搶占,需要搶占的話就調用這個函數 resched_curr(rq) //被喚醒者的調度類優先級比喚醒者高,走這個分支 set_tsk_need_resched(curr); //curr是目標cpu上的curr set_preempt_need_resched(); //喚醒者和被喚醒者的目標cpu是同一個cpu,走這個分支,觸發在下一個搶占點到來時重新調度 smp_send_reschedule(cpu); //喚醒者和被喚醒者的目標cpu不是同一個cpu,走這個分支,通過IPI中斷來通知目標cpu smp_cross_call(cpumask_of(cpu), IPI_RESCHEDULE); scheduler_ipi() //目標cpu響應函數 preempt_fold_need_resched(); set_preempt_need_resched(); //若判斷需要調度,觸發在下一個搶占點到來時重新調度,在目標cpu上 p->state = TASK_RUNNING; trace_sched_wakeup(p); //trace打印的時候就已經喚醒了,此時打印出來的cpu就是目標cpu //trace打印: <...>-813 [002] d..3 184883.820266: sched_waking: comm=Binder:1562_C pid=3075 prio=120 target_cpu=007 //上次運行在cpu7 <...>-813 [002] d..4 184883.820277: sched_wakeup: comm=Binder:1562_C pid=3075 prio=120 target_cpu=002 //喚醒后運行在cpu2
總結:喚醒阻塞任務最終都會匯總到 try_to_wake_up() 中。為被喚醒任務新選出的cpu和任務p之前運行的cpu不是同一個cpu的話會置上 WF_MIGRATED 標志。若執行喚醒的cpu和目標cpu不在同一個cluster內,需要觸發ipi IPI_CALL_FUNC 中斷觸發目標cpu執行ttwu_do_activate(),若是在同一個cluster,直接執行ttwu_do_activate()即可。check_preempt_curr() 中判斷若被喚被醒者的調度類優先級比喚醒者高,直接觸發搶占,這個是core里面做的,和具體的調度類沒有關系。若被喚被醒者和喚醒者屬於同一個調度類,則由具體調度類來決定是否觸發搶占。對於CFS任務,若喚醒者和被喚醒者的目標cpu是同一個cpu,判斷需要搶占的話就可以直接觸發搶占,若不在同一個cpu,還要通過ipi中斷向被喚醒者的cpu發IPI_RESCHEDULE 中斷使目標cpu觸發搶占。看來各個cpu只能觸發自己的搶占,不能觸發別的cpu的搶占。
2. 負載均衡(load balance)
通過遷移cpu rq上的任務,讓各個CPU上的負載匹配CPU算力。CFS負載均衡主要有三種:
(1) periodic load balance
在tick中觸發load balance,我們稱之 tick load balance 或者 periodic load balance。具體的代碼執行路徑如下:
scheduler_tick //core.c 硬中斷上下文 rq->idle_balance = idle_cpu(cpu); //表示當前cpu是否idle trigger_load_balance(rq) //fair.c if (time_after_eq(jiffies, rq->next_balance)) raise_softirq(SCHED_SOFTIRQ); //軟中斷響應函數后執行。喚醒對應的cpu的ksoftirqd/X線程來執行 run_rebalance_domains enum cpu_idle_type idle = this_rq->idle_balance ? CPU_IDLE : CPU_NOT_IDLE; nohz_idle_balance(this_rq, idle) //若 nohz_idle_balance 過了,就直接退出了,也先不看這里 rebalance_domains(this_rq, idle) //只有當前jieeies > sd->last_balance + interval 才執行 load_balance(cpu, rq, sd, idle, &continue_balancing) //執行負載均衡,嘗試拉負載到參數cpu上 nohz_balancer_kick(rq); //這個是中斷上下文,先執行,主要是觸發一個ipi中斷。只有系統中有處於nohz的idle cpu才可能起作用,這里先不看它。
(2) new idle load balance
調度器在pick next task的時候,發現當前cfs rq中沒有runnable任務,只能執行idle線程,讓CPU進入idle狀態的時候觸發的負載均衡,我們稱之new idle load balance。具體的代碼執行路徑如下:
__schedule(bool preempt) //core.c pick_next_task(rq, prev, &rf) pick_next_task_fair //只看CFS調度類 if (!sched_fair_runnable(rq)) //rq->cfs.nr_running=0, rq上一個runnable的任務都沒有才調用 new_tasks = newidle_balance(rq, rf); if (new_tasks > 0) goto again; //若是均衡到任務了,重新觸發CFS任務選核。 return NULL; //若是沒有均衡到任務,哪就選idle調度類了。
只有CFS調度類,均衡也沒有均衡到cfs任務,才會執行idle調度類的任務。
(3) idle load banlance
當其他的cpu已經進入idle,但本CPU任務太重,需要通過ipi中斷將其它idle的cpu喚醒來分攤負載而觸發的負載均衡,我們稱之idle load banlance。具體的代碼執行路徑如下:
scheduler_tick //core.c 硬中斷上下文 rq->idle_balance = idle_cpu(cpu); //表示當前cpu是否idle trigger_load_balance(rq) //fair.c nohz_balancer_kick(rq); //主要看這里 kick_ilb(flags) ilb_cpu = find_new_ilb(); //只找nohz idle狀態中的首個idle cpu smp_call_function_single_async(ilb_cpu, &cpu_rq(ilb_cpu)->nohz_csd); generic_exec_single(cpu, csd) //參數cpu為首個處於no-hz idle狀態的cpu __smp_call_single_queue(cpu, &csd->llist) //將首個idle cpu 的 rq->nohz_csd 添加到其cpu對應的per-cpu的單鏈表頭 call_single_queue 中 send_call_function_single_ipi(cpu) arch_send_call_function_single_ipi(cpu) smp_cross_call(cpumask_of(cpu), IPI_CALL_FUNC) do_handle_IPI //目標cpu被ipi中斷喚醒開始執行 generic_smp_call_function_interrupt nohz_csd_func //就是 rq->nohz_csd.func() rq->nohz_idle_balance = flags; raise_softirq_irqoff(SCHED_SOFTIRQ); //之后就是和 "periodic load balance"中的邏輯相同了。
其實 "idle load banlance" 是和 "periodic load balance" 交織在一起的,擋在tick中周期觸發 "periodic load balance" 的時候,就會判斷是有處於 no-hz idle 狀態的cpus,若是有又需要均衡的話就使用ipi中斷喚醒首個處於no-hz idle 狀態的cpu,然后在它上面觸發負載均衡,讓其去拉取繁忙cpu上的負載。
注:如果沒有dynamic tick特性,那么就不需要進行idle load balance,因為tick會喚醒處於idle的cpu,從而周期性tick就可以覆蓋這個場景。
3. 主動均衡(active upmigration)
把當前正在運行的 misfit task 向上遷移到算力更高的CPU上去。當一個低算力CPU的rq中出現misfit task的時候,如果該任務持續執行,那么遷移runnable任務負載均衡無能為力,需要主動均衡。
主動遷移是 Load balance 的一種特殊場景。在負載均衡中,只要運用適當的同步機制(持有一個或者多個rq lock),runnable的任務可以在各個CPU runqueue之間移動,然而running的任務是例外,它不掛在CPU rq中(雖然正在running的任務的se->on_rq=1,dequeue se時沒有置0),load balance無法覆蓋。為了能夠遷移running狀態的任務,內核提供了active upmigration 的方法(利用stop machine調度類的 migration/X 線程,就是先搶占它,被搶占后在put_prev_entity()中將其返回rq中,然后再遷移它,見《load_balance函數分析》)。
三、補充
1. nohz.idle_cpus_mask 的更新邏輯
scheduler_tick //core.c trigger_load_balance //fair.c nohz_balancer_kick(struct rq *rq) //fair.c tick中觸發均衡的cpu此時是非idle的才調用 nohz_balance_enter_idle(int cpu) //fair.c cpu非active的才調用,非主要邏輯 sched_cpu_dying //core.c cpu hotplug 相關功能的 nohz_balance_exit_idle(struct rq *rq) //fair.c cpumask_clear_cpu(rq->cpu, nohz.idle_cpus_mask) //fair.c atomic_dec(&nohz.nr_cpus); do_idle //idle.c cpuidle_idle_call //idle.c do_idle //idle.c 若cpu是offline的才執行 tick_nohz_idle_stop_tick //tick-sched.c __tick_nohz_idle_stop_tick //tick-sched.c nohz_balance_enter_idle(int cpu) //fair.c cpumask_set_cpu(cpu, nohz.idle_cpus_mask) //fair.c atomic_inc(&nohz.nr_cpus);
cpu 進入idle時才會設置到 nohz.idle_cpus_mask,scheduler_tick()中發現cpu不是idle的就取消設置。nohz.nr_cpus 表示 nohz.idle_cpus_mask 中idle cpu的個數。