《linux內核設計與實現》第四章

調度程序負責決定哪一個進程投入運行,什麼時候運行以及運行多長時間。只有經過調度程序合理調度,系統資源才能最大限度發揮做用,多進程纔會有併發執行的效果。node

最大限度地利用處理器時間的原則是,只要有能夠執行的進程,那麼就總會有進程正在執行。  linux

1.多任務算法

多任務系統分兩類:非搶佔式多任務(cooperative multitasking)和搶佔式多任務(preemptive multitasking)。由調度器來決定何時中止一個進程運行,這個強制掛起動做叫作搶佔(preemption)。緩存

進程在被搶佔以前能夠運行的時間是預先設定好的,叫作時間片。有效管理時間片能使調度程序從系統全局角度做出調度決定,這樣還能夠避免個別進程獨佔系統資源。服務器

現代操做系統多采用動態時間片計算方法和可配置的計算策略。可是Linux獨一無二的「公平」調度程序自己並無採用時間片來達到公平調度。  併發

相反,cooperative multitasking中除非進程本身主動中止運行,不然會一直執行,主動掛起本身的操做叫讓步。缺點是,調度器沒法管理每一個進程具體執行多少時間,更糟的是,一個毫不讓步的懸掛進程能使系統崩潰。編輯器

 

2.Linux進程調度模塊化

Linux2.4以前的調度程序都至關簡陋,讓人也很容易理解,可是它在衆多可運行進程或多處理器環境下都難以勝任。函數

正由於如此,Linux2.5內核中引入了新的調度程序--O(1),即大O表示法,簡單說,它指無論輸入有多大,調度程序均可以在恆定的時間內完成工做。這主要感謝靜態時間片算法和正對每一處理器的運行隊列。O(1)調度器在擁有數以十計的多處理器的環境下尚能表現近乎完美的性能和可擴展性,可是實踐證實對於調度那些響應時間敏感的程序卻先天不足(好比交互程序),O(1)對於大服務器的工做負載很理想,可是在不少交互程序要運行的桌面系統上則表現不佳。oop

在2.6內核開發初期,開發人員爲了提升對交互程序調度性能,引入了新調度算法。其中最著名的是「反轉樓梯最後期限調度算法」RSDL,該算法吸收了隊列理論,將公平調度概念引入Linux調度程序,而且最終在2.6.23版本中替代了O(1)算法,它此刻被稱爲「徹底公平調度算法」,CFS.

 

3.策略 :決定調度程序在什麼時候讓什麼進程運行,策略每每就決定系統的總體印象,而且還要負責優化使用處理器時間。因此不管從那個方面看,它都是直觀重要的。

(1)I/O消耗型和處理器消耗型的進程

I/O消耗型:指進程的大部分時間用來提交I/O請求或是等待I/O請求,這樣的進程常常處於可運行狀態,但一般只運行很短期,在等待I/O時會阻塞。

處理器消耗型:進程大部分時間都在執行代碼,除非被搶佔,不然一直執行,沒太多I/O需求。調度器不該該常常讓他們執行,應儘可能下降它們的調度頻率,而延長其運行時間。

調度策略就是要在這兩個矛盾中尋找平衡:進程響應迅速(響應時間短)和最大系統利用率(高吞吐量),爲了知足這個需求,調度程序一般採用很是複雜的算法來決定最值得運行的進程投入運行。

 

(2)進程優先級

是一種根據進程的價值和其對處理器時間的需求來對進程進行分級的方法。一般作法是優先級高的先運行,低的後運行,相同優先級輪轉執行,在某些系統中,優先級高的使用時間片也較長。

調度程序老是選擇時間片未用盡並且優先級最高的進程運行。用戶和系統均可以經過設置進程優先級來影響系統的調度。  

Linux採用了兩個不一樣的優先級範圍。第一個是nice值(範圍從-20~19),默認值是0,越大的nice值表示優先級越低,低nice值的進程能夠得到更多的處理器時間。Linux中nice值表明時間片的比例.(ps –el能夠查看系統進程,NI一列就是nice值)

第二種是實時優先級,其值可配,從0~99,越高的值表示優先級越高。

ps -eo state,uid,pid,ppid,rtprio,time,comm

 

(3)時間片

指進程在被搶佔前所能持續運行的時間。時間片過長會致使系統對交互響應表現欠佳,時間片過短會明顯增長進程切換帶來的處理器耗時。IO消耗型不須要長的時間片,而處理器消耗型的進程但願越長越好(提升高速緩存命中率)。

Linux的CFS調度器沒有直接分配時間片到進程,而是分配處理器的使用比。這樣進程所得到的處理器時間實際上是和系統負載密切相關的,這個比例進一步還會受進程nice值影響。具備更小nice值的進程會被賦予高權重,從而有用更多的處理器使用比。

多數系統中,是否將一個進程馬上投入運行,徹底由進程優先級和是否擁有時間片決定的。而在Linux的CFS調度器,其搶佔時機取決於新的可運行程序消耗了多少處理器使用比,若是消耗的使用比比當前進程小,則新進程馬上投入運行,不然推遲運行。

好比系統僅有文字處理和視頻編碼兩個進程,nice值相同,分給的處理器使用比都是50%。文本編輯器大部分時間用於等待用戶輸入,所以確定不會用處處理器的50%,而視頻編碼是有可能用到50%的。咱們關心的是,當IO發生,文本編輯器被喚醒時,CFS發現文本編輯器運行的時間比視頻編碼器短的多,由於文本編輯器沒有消耗掉承諾給它的50%處理器使用比,所以CFS當即讓文本編輯器投入運行。

 

4.Linux調度算法

(1)調度器類

Linux調度器是以模塊的方式提供的,這樣可讓不一樣類型的進程能夠有針對性地選擇調度算法。這種模塊化結構稱爲調度器類。,它容許可動態的添加的調度算法並存,調度屬於本身範疇的進程。

每一個調度器都有一個優先級,系統會按照優先級順序遍歷調度類,選出最高優先級的調度器類,而後選擇下面要執行的進程。

CFS是一個針對普通進程的調度類,SCHED_NORMAL,還有實時調度類。

(2)傳統UNIX進程調度

通常採用絕對的優先級和時間片,這會致使如下問題:

①nice值對應到絕對時間片,致使進程切換沒法最優化

②相對的nice值,把進程的nice值減1,所帶來的效果取決於nice初始值

③時間片會隨定時器節拍改變

④對喚醒進程提高優先級,會留下玩弄調度器的後門(能夠改變影響優先級)。

(3)CFS原理

CFS基於一個簡單的理念:進程調度的效果應如同系統具有一個理想中的多任務處理器。在有n個進程的系統中,每一個進程得到1/n處理器時間。

CFS在全部可運行總數基礎上計算出一個進程應該運行多久。容許每一個進程運行一段時間,循環輪轉,選擇運行最少的進程做爲下一個運行進程。每一個進程都按其權重在所有可運行進程中所佔比例的「時間片」來運行。CFS爲完美多任務中的無限小調度週期設立一個目標—「目標延遲」。每一個進程時間片的最小粒度是1ms.

 

任何處理器進程所得到的處理器時間是由它本身和其餘可運行進程的nice相對值決定的。CFS不是完美的公平,可是在幾百個進程環境中能夠體現出近乎完美的多任務。

 

5.Linux調度的實現

代碼位於kernel/sched/fair.c

(1)時間記帳

①全部調度器都必須對進程運行時間作記帳,CFS再也不有時間片概念,可是爲確保每一個進程只在公平分配給它的處理器時間運行,也會用如下實體機構來作時間記帳,在<linux/sched.h>  

 

struct sched_entity {       struct load_weight    load;        /* for load-balancing */

    struct rb_node        run_node;

    struct list_head    group_node;

    unsigned int        on_rq;

 

    u64            exec_start;

    u64            sum_exec_runtime;

    u64            vruntime;

    u64            prev_sum_exec_runtime;

  …

};

這個結構體做爲se成員,嵌入在進程描述符struct task_struct內

②虛擬運行時間

Vruntime全部可運行進程總數的被加權後的計算時間,單位是ns.其與定時器節拍無關。CFS用vruntime來記錄一個程序到底運行了多長時間以及它還應該再運行多久。

記帳功能在fair.c文件實現

static void update_curr(struct cfs_rq *cfs_rq)   {

    struct sched_entity *curr = cfs_rq->curr;

    u64 now = rq_of(cfs_rq)->clock_task;

    unsigned long delta_exec;

 

    if (unlikely(!curr))

        return;

 

    /*

     * Get the amount of time the current task was running

     * since the last time we changed load (this cannot

     * overflow on 32 bits):

     */

    delta_exec = (unsigned long)(now - curr->exec_start);

    if (!delta_exec)

        return;

 

    __update_curr(cfs_rq, curr, delta_exec);

    curr->exec_start = now;

 

    if (entity_is_task(curr)) {

        struct task_struct *curtask = task_of(curr);

 

        trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);

        cpuacct_charge(curtask, delta_exec);

        account_group_exec_runtime(curtask, delta_exec);

    }

 

    account_cfs_rq_runtime(cfs_rq, delta_exec);

}

update_curr()由系統定時器週期性調用,它計算了當前進程的執行時間(加權計算的)與vruntime相加。Vruntime能夠準確地測量給定進程的運行時間,並且可知道誰應該是下一個被運行的進程。

(2)進程選擇

CFS始終選擇具備最小vruntime的進程來執行。

CFS用紅黑樹來組織可運行進程隊列,vruntime值做爲紅黑樹的鍵值,經過鍵值檢索對應節點的速度與整個樹的節點規模成指數比關係。

①挑選下一個任務

簡單說,CFS運行rbtree樹中最左邊葉子節點所表明的進程,實現函數是

static struct sched_entity *__pick_next_entity(struct sched_entity *se)   {

    struct rb_node *next = rb_next(&se->run_node);

 

    if (!next)

        return NULL;

 

    return rb_entry(next, struct sched_entity, run_node);

}

函數自己並不會遍歷樹找到最左邊葉子節點,儘管有效查找葉子節點是紅黑樹的優點O(logn),更容易的作法是把最左側葉子節點緩存起來。該函數返回值就是下一個運行的進程,若返回NULL,表示沒有可運行進程,CFS調用器選擇idle任務運行。

②向紅黑樹中添加進程

enqueue_entity()函數實現添加進程到rbtree,以及緩存最左邊葉子節點,在進程變爲可運行狀態(被喚醒)或者經過fork()調用第一次建立進程時發生。

static void   enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)

{

    /*

     * Update the normalized vruntime before updating min_vruntime

     * through callig update_curr().

     */

    if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))

        se->vruntime += cfs_rq->min_vruntime;

 

    /*

     * Update run-time statistics of the 'current'.

     */

    update_curr(cfs_rq);

    enqueue_entity_load_avg(cfs_rq, se, flags & ENQUEUE_WAKEUP);

    account_entity_enqueue(cfs_rq, se);

    update_cfs_shares(cfs_rq);

 

    if (flags & ENQUEUE_WAKEUP) {

        place_entity(cfs_rq, se, 0);

        enqueue_sleeper(cfs_rq, se);

    }

 

    update_stats_enqueue(cfs_rq, se);

    check_spread(cfs_rq, se);

    if (se != cfs_rq->curr)

        __enqueue_entity(cfs_rq, se);

    se->on_rq = 1;

 

    if (cfs_rq->nr_running == 1) {

        list_add_leaf_cfs_rq(cfs_rq);

        check_enqueue_throttle(cfs_rq);

    }

}

該函數更新運行時間和其餘一些統計數據,而後調用__enqueue_entity()進行繁重的插入操做,把數據項真正插入到rbtree中。

③從樹中刪除進程

刪除發生在進程堵塞(變爲不可運行狀態)或終止時。

static void   dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)

{

    /*

     * Update run-time statistics of the 'current'.

     */

    update_curr(cfs_rq);

    dequeue_entity_load_avg(cfs_rq, se, flags & DEQUEUE_SLEEP);

 

    update_stats_dequeue(cfs_rq, se);

    if (flags & DEQUEUE_SLEEP) {

#ifdef CONFIG_SCHEDSTATS

        if (entity_is_task(se)) {

            struct task_struct *tsk = task_of(se);

 

            if (tsk->state & TASK_INTERRUPTIBLE)

                se->statistics.sleep_start = rq_of(cfs_rq)->clock;

            if (tsk->state & TASK_UNINTERRUPTIBLE)

                se->statistics.block_start = rq_of(cfs_rq)->clock;

        }

#endif

    }

 

    clear_buddies(cfs_rq, se);

 

    if (se != cfs_rq->curr)

        __dequeue_entity(cfs_rq, se);

    se->on_rq = 0;

    account_entity_dequeue(cfs_rq, se);

 

    /*

     * Normalize the entity after updating the min_vruntime because the

     * update can refer to the ->curr item and we need to reflect this

     * movement in our normalized position.

     */

    if (!(flags & DEQUEUE_SLEEP))

        se->vruntime -= cfs_rq->min_vruntime;

 

    /* return excess runtime on last dequeue */

    return_cfs_rq_runtime(cfs_rq);

 

    update_min_vruntime(cfs_rq);

    update_cfs_shares(cfs_rq);

}

rb_erase(),而後更新rb_leftmost緩存,若是刪除的是最作左節點,要從新找到新的最左節點。

(3)調度器入口

調度器入口點是schedule()函數,調用pick_next_task(),以優先級爲序,從最高優先級類開始,每一個調度器類都實現了pick_next_task(),從第一個返回的非NULL值的類中選擇下一個可運行進程。

(4)睡眠和喚醒

當休眠時,進程把本身標記成休眠狀態,從可執行紅黑樹中移出,當如等待隊列,而後調用schedule()選擇了一個進程執行。

喚醒過程恰好相反:進程被設置爲可執行狀態,而後從等待隊列中移到可執行紅黑樹中。

休眠分TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE  ,兩種狀態的進程位於同一個等待隊列上,等待某些事件,不可以運行。

①等待隊列

休眠經過等待隊列處理,等待隊列是由等待某些事件發生的進程組成的簡單鏈表.

DEFINE_WAIT(wait);   Add_wait_queue(wait);

While(!condition){

    Prepare_to_wait(&q,&wait,TASK_INTERRUPTIBLE);

    If(signal_pending(current))

        Schedule();

}

Finish_wait(&q,&wait);

②喚醒

經過wake_up()進行,喚醒指定等待隊列的全部進程,把喚醒進程狀態設置爲TASK_RUNNING,調用enqueue_task()將此進程放入紅黑樹中,若是被喚醒進程優先級比當前正執行的優先級高,還要設置need_resched標誌。

6.搶佔和上下文切換

(1)上下文切換,也就是從一個執行進程切換到另外一個可執行進程,在schedule()調用context_swtich()函數完成。

context_swtich()一是調用switch_mm(),把虛擬內存從上一個進程映射切換到新進程中。

二是調用swtich_to()把上一個進程的處理器狀態新進程的處理器狀態,包括保存、恢復棧信息和寄存器信息,還有其餘任何與體系結構相關的信息。

內核提供了一個need_resched標誌來代表是否須要從新調度,每一個進程都包含一個need_resched。

(2)搶佔

用戶搶佔:內核即將返回用戶空間的時候,檢查need_resched標誌被設置,就會調用schedule()。包括:①從系統調用返回用戶空間時,②從中斷處理程序返回用戶空間時③用戶調用sleep()等主動讓出。

內核搶佔:只要沒有持有鎖,內核就能夠進行搶佔(thread_info裏的preempt_count=0說明不持有鎖)

發生時間點:①中斷處理程序正在執行,且返回內核空間以前②內核代碼再一次具備可搶佔性的時候③內核進程顯示的調用schedule()④內核進程阻塞。  

7.實時調度策略

Linux提供了兩種實時調度策略:SCHED_FIFO和SCHED_RR,普通的,非實時的調度策略是SCHED_NORMAL。實時策略不被CFS管理,由一個特殊的實時調度器管理。

SCHED_FIFO:實現了一個簡單的,先入先出的算法,它不使用時間片,處於可運行狀態的SCHED_FIFO會比任何SCHED_NORMAL的進程先獲得調度,而且它會一直執行,直到執行完,可是有高優先級的SCHED_FIFO或SCHED_RR會馬上搶佔。

SCHED_RR與SCHED_FIFO大致相同,是帶有時間片的SCHED_FIFO—實時輪流調度算法。

Linux的實時調度算法提供了一種軟實時工做方式:內核調度進程,盡力使進程在它的限定時間到來前運行,但內核不保證總能知足這些進程需求。而硬實時系統保證在必定條件下能夠保證任何調度的需求。

 

實時優先級從0~MAX_RT_PRIO-1.默認是0~99.SCHED_NORMAL級進程的nice值共享這個取值空間,MAX_RT_PRIO~MAX_RT_PRIO+40.默認狀況下nice從-20~19對應100~139的實時優先級範圍。

 

8.與調度相關的系統調用

Linux提供了一個系統調用族,用於管理與調度程序相關的參數。

 

(1) 與調度策略有關的

sched_setscheduler()和sched_getscheduler()分別用於設置和獲取進程的調度策略和實時優先級。

對於一個普通進程來講,nice()函數能夠將給定進程的靜態優先級增長一個給定的量。只有超級用戶才能使用負值。

(2)與處理器綁定

sched_setaffinity()設置綁定處理器,

(3)放棄處理器

sched_yield()顯示的將處理器時間讓給其餘進程,把本身移到過時隊列,這樣確保一段時間內它不會再被執行,因爲實時進程不會過時,因此屬於例外。

相關文章
相關標籤/搜索