Linux內核進程調度

計算機方面總是這樣你以爲你都知道其實可能還有很多不知道,慫了慫了

學操作系統的時候老師經常會講先進先出呀,優先級調度呀,多級隊列呀等等策略,他們都有自己的適用場景,因爲Linux內核裏有不同類型的進程,所以就要結合多個策略,其內部進程大體可分爲實時進程,非實時進程,非實時進程又可分爲批處理進程,交互式進程等,對於實時進程就主要採用先進先出輪轉調度;所有非實時進程在Linux裏都使用CFS調度,即完全公平調度算法,他是將所有普通進程都統一對待,用一個紅黑樹來管理所有進程,鍵值是每個進程對應的虛擬時間,他是由優先級計算而來,每個pick都是選擇紅黑樹最左邊的進程作爲下一個運行的進程。
對於進程都會有一個調度器類,一個調度器類負責調度某一類進程,他們都有自己管理的運行隊列,每次就通過調度器類上的函數來選擇搶佔。
主要藉助兩個調度器,一個是主調度器,一個是週期調度器,每個調度器對應相應的函數操作,週期調度器有一個調度定時器,每隔一段時間就會執行一箇中斷,進入中斷會對當前運行進程運行時間進行更新,如果進程需要被調度,就會在調度定時器中斷中會設置一個調度標誌位,之後從定時器中斷返回,中斷返回處理都必須去判斷調度標誌位是否設置,如設置則執行schedule()用主調度器進行調度,主調度器就主要是pick下一個運行的進程,然後進行上下文切換
下面詳細說說

週期調度器

內核調用scheduler_tick()函數來更新進程一些計數值以及檢查進程執行的時間是否超過了它對應的ideal_runtime
,真正的操作是內核先找到當前正在運行進程然後調用它的調度器上的調度方法函數它會來對該進程相關數據進行更新,檢查它運行時間是否超出,該進程是FIFO調度進程的話scheduler_tick()就什麼都不做,因爲它不可能被比其優先級低的進程搶佔,如果是時間輪轉調度進程就遞減它的時間片計數器並檢查是否已經減爲0,如果是cfs調度進程,就根據該進程在隊列中的總權重佔的比例來計算它的執行時間。若會被重新調度, 那麼調度類方法會設置特定標誌, 中斷返回時會檢查該標誌

void scheduler_tick(void)
{
    //獲得當前cpu號
    int cpu = smp_processor_id();
    //cpu就緒隊列
    struct rq *rq = cpu_rq(cpu);
    //當前正在運行的進程
    struct task_struct *curr = rq->curr;
   //定時器
    sched_clock_tick();
    //自旋鎖,更新進程相關數據要加鎖
    raw_spin_lock(&rq->lock);
    //更新時間戳
    update_rq_clock(rq);
    //更新進程其他數據
    update_cpu_load_active(rq);
    //進程對應調度器類上的函數來判斷調度
    curr->sched_class->task_tick(rq, curr, 0);
    
    raw_spin_unlock(&rq->lock);

    perf_event_task_tick();

#ifdef CONFIG_SMP
    rq->idle_at_tick = idle_cpu(cpu);
	trigger_load_balance(rq, cpu);
#endif
}

注:代碼均來自linux-5.0.1

可以看到調度了進程的task_tick函數;這裏面就是檢查當前進程是否已經超過設定一個時間片,對於cfs調度進程是按照該進程在隊列中的總權重中佔得比例,算出它該執行的時間,如果該進程實際執行時間超過了,則激發延遲調度

 ideal_runtime = sched_slice(cfs_rq, curr);
    delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
    //如果時間超過ideal_runtime就激發延遲調度
    if (delta_exec > ideal_runtime) {
        resched_task(rq_of(cfs_rq)->curr);
        /*
         * The current task ran long enough, ensure it doesn't get
         * re-elected due to buddy favours.
         */
        clear_buddies(cfs_rq, curr);
        return;
    }
主調度器

主調度器主要就是schedule函數來操控,如果會發生調度就是調用它來完成具體的工作,流程大概是檢查死鎖,關閉內核搶佔,檢查進程是否需要再加入就緒隊列進行相應操作,然後從cpu的就緒隊列中選擇一個最合適的進程來運行

static void __sched notrace __schedule(bool preempt)
{
    struct task_struct *prev, *next;
    unsigned long *switch_count;
    struct rq_flags rf;
    struct rq *rq;
    int cpu;

    cpu = smp_processor_id();
    rq = cpu_rq(cpu);
    prev = rq->curr;

    schedule_debug(prev);

    if (sched_feat(HRTICK))
        hrtick_clear(rq);

    local_irq_disable();
    rcu_note_context_switch(preempt);

    /*
     * Make sure that signal_pending_state()->signal_pending() below
     * can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)
     * done by the caller to avoid the race with signal_wake_up().
     *
     * The membarrier system call requires a full memory barrier
     * after coming from user-space, before storing to rq->curr.
     */
    //
    rq_lock(rq, &rf);
    smp_mb__after_spinlock();

    /* Promote REQ to ACT */
    rq->clock_update_flags <<= 1;
    update_rq_clock(rq);

    switch_count = &prev->nivcsw;
    if (!preempt && prev->state) {
        if (signal_pending_state(prev->state, prev)) {
            prev->state = TASK_RUNNING;
        } else {
            deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);
            prev->on_rq = 0;

            if (prev->in_iowait) {
                atomic_inc(&rq->nr_iowait);
                delayacct_blkio_start();
            }

            /*
             * If a worker went to sleep, notify and ask workqueue
             * whether it wants to wake up a task to maintain
             * concurrency.
             */
            if (prev->flags & PF_WQ_WORKER) {
                struct task_struct *to_wakeup;

                to_wakeup = wq_worker_sleeping(prev);
                if (to_wakeup)
                    try_to_wake_up_local(to_wakeup, &rf);
            }
        }
        switch_count = &prev->nvcsw;
    }

    next = pick_next_task(rq, prev, &rf);
    clear_tsk_need_resched(prev);
    clear_preempt_need_resched();

    if (likely(prev != next)) {
        rq->nr_switches++;
        rq->curr = next;
        
        ++*switch_count;

        trace_sched_switch(preempt, prev, next);

        /* Also unlocks the rq: */
        rq = context_switch(rq, prev, next, &rf);
    } else {
        rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
        rq_unlock_irq(rq, &rf);
    }
    balance_callback(rq);
}

其中調用pick_next_task函數來選擇下一個運行的進程

static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
    const struct sched_class *class;
    struct task_struct *p;

    //如果都是cfs調度進程直接選擇紅紅黑樹最左邊的那個進程
    if (likely((prev->sched_class == &idle_sched_class ||
                prev->sched_class == &fair_sched_class) &&
               rq->nr_running == rq->cfs.h_nr_running)) {

        p = fair_sched_class.pick_next_task(rq, prev, rf);
        if (unlikely(p == RETRY_TASK))
            goto again;

        /* Assumes fair_sched_class->next == idle_sched_class */
        if (unlikely(!p))
            p = idle_sched_class.pick_next_task(rq, prev, rf);

        return p;
    }

    again:
    for_each_class(class) {
        p = class->pick_next_task(rq, prev, rf);
        if (p) {
            if (unlikely(p == RETRY_TASK))
                goto again;
            return p;
        }
    }
    /* The idle class should always have a runnable task: */
    BUG();
}

然後就是調用context_switch進行上下文切換

多核運行隊列平衡

可以看到schedule函數每次都是從當前cpu就緒隊列裏選擇進程來運行,在整個過程中一個進程可能始終就在一個cpu上運行,但是很容易出現某個cpu超負荷的情況,於是就設計內核會週期性地檢查運行隊列是否平衡,如果不平衡就把一些進程移動另一cpu.

很多地方還是隻寫了皮毛,後面再補充…睡覺

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章