調度器21—負載均衡—框架分析


一、概述

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的個數。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM