linux源碼解讀(十五):紅黑樹在內核的應用——CFS調度器


  1、在現代的操作系統中,進程調度是最核心的功能之一;linux 0.11的調度算法簡單粗暴:遍歷task_struct數組,找到時間片counter最大的進程執行;顯然這種策略已經不適合越來越復雜的業務場景需求了,所以后來逐步增加了多種調度策略,目前最廣為人知的調度策略有5種:cfs、idle、deadline、realtime、stop,並且這5種調度策略都是同時存在的,不排除后續增加新的調度策略,怎么才能更方便地統一管理存量和增量的調度策略了?從 2.6.23開始引入了sched_class,如下:

struct sched_class {
    const struct sched_class *next;
    /*
    1、全是成員函數:這里用函數指針來表達;
    2、調度的方式有很多種,比如cfs、rt、idle、deadline,每種方式的實現方法肯定不同,這里提供接口函數讓不同的調度方式各自去實現(類似驅動的struct file_operations *ops結構體)
    */
    void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);/*任務加入隊列;cfs就是在紅黑樹插入節點*/
    void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);/*任務移除隊列;cfs就是在紅黑樹刪除節點*/
    void (*yield_task) (struct rq *rq);/*讓出任務*/
    bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt);/*讓出到任務*/

    void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags);

    /*
     * It is the responsibility of the pick_next_task() method that will
     * return the next task to call put_prev_task() on the @prev task or
     * something equivalent.
     *
     * May return RETRY_TASK when it finds a higher prio class has runnable
     * tasks.
     */
    struct task_struct * (*pick_next_task) (struct rq *rq,
                        struct task_struct *prev,
                        struct pin_cookie cookie);
    void (*put_prev_task) (struct rq *rq, struct task_struct *p);

#ifdef CONFIG_SMP
    int  (*select_task_rq)(struct task_struct *p, int task_cpu, int sd_flag, int flags);
    void (*migrate_task_rq)(struct task_struct *p);

    void (*task_woken) (struct rq *this_rq, struct task_struct *task);

    void (*set_cpus_allowed)(struct task_struct *p,
                 const struct cpumask *newmask);

    void (*rq_online)(struct rq *rq);
    void (*rq_offline)(struct rq *rq);
#endif

    void (*set_curr_task) (struct rq *rq);
    void (*task_tick) (struct rq *rq, struct task_struct *p, int queued);
    void (*task_fork) (struct task_struct *p);
    void (*task_dead) (struct task_struct *p);

    /*
     * The switched_from() call is allowed to drop rq->lock, therefore we
     * cannot assume the switched_from/switched_to pair is serliazed by
     * rq->lock. They are however serialized by p->pi_lock.
     */
    void (*switched_from) (struct rq *this_rq, struct task_struct *task);
    void (*switched_to) (struct rq *this_rq, struct task_struct *task);
    void (*prio_changed) (struct rq *this_rq, struct task_struct *task,
                 int oldprio);

    unsigned int (*get_rr_interval) (struct rq *rq,
                     struct task_struct *task);

    void (*update_curr) (struct rq *rq);

#define TASK_SET_GROUP  0
#define TASK_MOVE_GROUP    1

#ifdef CONFIG_FAIR_GROUP_SCHED
    void (*task_change_group) (struct task_struct *p, int type);
#endif
};

  這個class把調度中涉及到的方法全部抽象出來定義成函數指針,不同的調度算法對於函數的實現肯定不一樣,linux內核直接調用這些函數指針就能達到使用不同調度策略的目的了,是不是很巧妙了?和設備驅動的file_operations結構體思路是一樣的(函數指針的接口分別由各個廠家的驅動實現,但是接口名稱保持一致)!不同調度策略/實例的關系和代碼文件如下:

     

  2、介紹CFS之前,先總結一下linux調度的類型和背景:

   (1)基於時間片輪詢,又稱O(n)調度:每次調度都需要遍歷所有的task_struct,找到時間片最大的執行;如果進程很多,導致task_struct很長,每次光是遍歷就很耗時,時間復雜度是O(n);n是task_struct的個數;除此以外,還有比較明顯的缺陷:

  • SMP系統擴展不好,訪問run queue需要加鎖
  • 實時進程不能立即調度
  • cpu可能空轉
  • 進程在多個cpu之間來回跳轉,降低性能

   (2)上面的調度很耗時,核心因素就是每次都要遍歷所有的task_struct去尋找時間片最大的進程,時間復雜度被抬高到了O(n),並且也沒有優先級的功能,這兩點該怎么改進了?O(1)算法由此誕生,簡單來說,先把所有的任務按照不同的優先級加入不同的隊列,然后先調度優先級高的隊列,由此專門誕生了prio_array結構體來支撐算法,如下:

#define MAX_USER_RT_PRIO    100
#define MAX_RT_PRIO         MAX_USER_RT_PRIO
#define MAX_PRIO            (MAX_RT_PRIO + 40)//140個優先級(0 ~ 139,數值越小優先級越高)

#define BITMAP_SIZE ((((MAX_PRIO+1+7)/8)+sizeof(long)-1)/sizeof(long))

struct prio_array {
    int nr_active;//所有優先級隊列中的總任務數。
    unsigned long bitmap[BITMAP_SIZE];//每個位對應一個優先級的任務隊列,用於記錄哪個任務隊列不為空,能通過 bitmap 夠快速找到不為空的任務隊列
    struct list_head queue[MAX_PRIO];//優先級隊列數組,每個元素維護一個優先級隊列,比如索引為0的元素維護着優先級為0的任務隊列
};

  圖示如下:先掃描bitmap,找到不為空的隊列去調度(比如這里的2、6號隊列不為空);由於bitmap的大小是固定的,所以遍歷的時間也是固定的,時間復雜度自然是O(1)了;因為數值越低、優先級越高,所以從bitmap的0開始遍歷,找到第一個不為空的隊列就可以停止遍歷了,這里又節約了時間,所以整體的效率比簡單粗暴的時間片輪詢高多了!總結一下:O(1)調度算法的本質就是把大量的任務按照優先級分隊列,從優先級高的隊列開始執行,避免了時間片輪詢那種“眉毛胡子一把抓”的混亂,是一種典型的空間換時間的思路

  

    相比時間片輪詢,O(1)算法確實做了比較大的改進,但是自身也不是100%完美無瑕(否則就不會后后續其他的調度算法了),比如:

  •  交互性較強的任務要再次運行,就需要等待當前等待隊列中的所有任務都執行完成:比如進程需要用戶輸入時阻塞,但並不是用戶輸入后馬上喚醒,而是同隊列其他任務都運行完后才繼續執行,可能導致交互不及時,產生卡頓的感覺,影響用戶體驗
  •    不能保證在給定的時間間隔內,為每個任務分配的時間與其優先級是成正比的;這個問題是上面問題引申出來的:比如A進程優先級高,分配了20ms,B進程分配10ms,但是A被阻塞了,cpu轉而執行B;A要等到B執行完成后才會繼續,所以A優先級高完全沒體現出來,名存實亡!

   為了解決上面的問題,CFS誕生了!

  3、(1)不管用哪種調度方式,首先要找到進程的task_struct;由於上層業務應用需求多種多樣,操作系統肯定會不停的創建、運行和銷毀進程,導致進程的狀態時刻都在變化,進程的權重/優先級肯定也要不停地變化,這么多的關鍵因素都在改變,怎么高效、快速地管理這些不斷變化的進程了?以往的調度策略用的最多的就是鏈表了,根據不同的進程狀態、優先級等影響因素加入不同的隊列,但是鏈表有個致命弱點:只能順序遍歷,導致增刪改查效率極低。基於鏈表這種數據結構,又發明了紅黑樹,本質是把原來鏈表“平鋪直敘”式的順序排列改成了按照大小的樹形排列,此時再增刪改查的效率就要高很多了!那么問題又來了:既然紅黑樹需要按照節點某個值的大小排序,選哪個值比較適合了?linux開發人員選擇的是vruntime!計算公式如下:

  vruntime = vruntime + 實際運行時間(time process run) * 1024 / 進程權重(load weight of this process)

  注意:vruntime是累加的!實際運行時間就是進程運行時暫用cpu的時間,權重該怎么計算了?這里有個映射表,根據nice值查找對應的weight!nice值類似於優先級,取值為下面所示的從15到-20,每次遞減5;根據nice值找到weight后就可以帶入公式計算vruntime了!

/*
 * Nice levels are multiplicative, with a gentle 10% change for every
 * nice level changed. I.e. when a CPU-bound task goes from nice 0 to
 * nice 1, it will get ~10% less CPU time than another CPU-bound task
 * that remained on nice 0.
 *
 * The "10% effect" is relative and cumulative: from _any_ nice level,
 * if you go up 1 level, it's -10% CPU usage, if you go down 1 level
 * it's +10% CPU usage. (to achieve that we use a multiplier of 1.25.
 * If a task goes up by ~10% and another task goes down by ~10% then
 * the relative distance between them is ~25%.)
 */
const int sched_prio_to_weight[40] = {
 /* -20 */     88761,     71755,     56483,     46273,     36291,
 /* -15 */     29154,     23254,     18705,     14949,     11916,
 /* -10 */      9548,      7620,      6100,      4904,      3906,
 /*  -5 */      3121,      2501,      1991,      1586,      1277,
 /*   0 */      1024,       820,       655,       526,       423,
 /*   5 */       335,       272,       215,       172,       137,
 /*  10 */       110,        87,        70,        56,        45,
 /*  15 */        36,        29,        23,        18,        15,
};

  vruntime的值越小,說明占用cpu的時間就越少,或者說權重越大,這時就需要優先運行了!所以用紅黑樹是根據所有進程的vruntime來組織的,樹最左下角的節點就是vruntime最小的節點,是需要最先執行的節點;隨着進程的執行,或者說權重的調整,vruntime是不停在變化的,此時就需要動態調整紅黑樹了。由於紅黑樹本身的算法特點,動態調整肯定比鏈表快多了,這是CFS選擇紅黑樹的根本原因!看到這里,CFS算法的特點之一就明顯了:沒有時間片的概念,而是根據實際的運行時間和虛擬運行時間來對任務進行排序,從而選擇調度;

  (2)算法原理介紹完,接着該看看linux內核是怎么實現的了!和其他模塊一樣,CFS的實現少不了結構體的支持,算法相關的核心結構體如下:

  • 第一個肯定是task_struct了!新增了不同調度器的描述符,便於確定本進程使用了哪些調度策略!sched_entity就是構造紅黑樹的關鍵成員變量了!
struct task_struct {
    .......
        int prio, static_prio, normal_prio;
    unsigned int rt_priority;
    const struct sched_class *sched_class;/*調度策略的實例*/
    struct sched_entity se;/*cfs調度策略,包含了rb_node*/
    struct sched_rt_entity rt;/*real time調度策略*/
#ifdef CONFIG_CGROUP_SCHED
    struct task_group *sched_task_group;
#endif
    struct sched_dl_entity dl;/*deadline 調度*/
    .......
}

  組成紅黑樹的關鍵結構體:有個run_node字段,從名字就能看出是正在運行的進程節點!

struct sched_entity {/*cfs調度策略*/
    struct load_weight    load;        /* for load-balancing */
    struct rb_node        run_node;   /*調度實體是由紅黑樹組織起來的*/ 
    struct list_head    group_node;
    unsigned int        on_rq;
    /*構造紅黑樹時,其實下面的每一項都可以用作節點的key;
    1、但是這里選vruntime作為key構造紅黑樹,換句話說用vruntime來排序,小的靠左,大的靠右
    2、如果不同進程的vruntime一樣,可以加很小的數改成不一樣的
*/ u64 exec_start; u64 sum_exec_runtime; u64 vruntime;/*紅黑樹節點排序的變量*/ u64 prev_sum_exec_runtime; u64 nr_migrations; #ifdef CONFIG_SCHEDSTATS struct sched_statistics statistics; #endif #ifdef CONFIG_FAIR_GROUP_SCHED int depth; struct sched_entity *parent; /* rq on which this entity is (to be) queued: */ struct cfs_rq *cfs_rq; /* rq "owned" by this entity/group: */ struct cfs_rq *my_q; #endif #ifdef CONFIG_SMP /* * Per entity load average tracking. * * Put into separate cache line so it does not * collide with read-mostly values above. */ struct sched_avg avg ____cacheline_aligned_in_smp; #endif };

  還有直接描述cfs正在runquene的結構體:包含紅黑樹的根節點、最左邊的節點(也就是vruntime最小的節點)、當前正在使用的調度結構體;

/* CFS-related fields in a runqueue */
struct cfs_rq {
......
    struct rb_root tasks_timeline;/*紅黑樹的root根節點*/
    struct rb_node *rb_leftmost;/*紅黑樹最左邊的節點,也就是vruntime最小的節點*/

    /*
     * 'curr' points to currently running entity on this cfs_rq.
     * It is set to NULL otherwise (i.e when none are currently running).
     */
    struct sched_entity *curr, *next, *last, *skip;
......

}

  上面各種結構體種類繁多,不容易理清關系,看看下面的圖就清晰了:

     

   結構體准備好后,就可以通過各種api建樹了!

     (3)既然紅黑樹排序以vruntime為准,這個值肯定是要不斷調整的,具體的更改函數在update_curr函數(kernel\sched\fair.c),如下:關鍵代碼處加了中文注釋

/*
 * Update the current task's runtime statistics.
 */
static void update_curr(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->curr;
    u64 now = rq_clock_task(rq_of(cfs_rq));
    u64 delta_exec;

    if (unlikely(!curr))
        return;
    /*計算進程已經執行的時間*/
    delta_exec = now - curr->exec_start;
    if (unlikely((s64)delta_exec <= 0))
        return;

    curr->exec_start = now;//更新開始執行的時間

    schedstat_set(curr->statistics.exec_max,
              max(delta_exec, curr->statistics.exec_max));

    curr->sum_exec_runtime += delta_exec;
    schedstat_add(cfs_rq->exec_clock, delta_exec);

    curr->vruntime += calc_delta_fair(delta_exec, curr);//更新vruntime
    update_min_vruntime(cfs_rq);

    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);
}

  把節點加入紅黑樹:

/*
 * Enqueue an entity into the rb-tree:
 */
static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    struct rb_node **link = &cfs_rq->tasks_timeline.rb_node;/*紅黑樹根節點*/
    struct rb_node *parent = NULL;
    struct sched_entity *entry;
    int leftmost = 1;

    /*
     * Find the right place in the rbtree:
     */
    while (*link) {
        parent = *link;
        /*找到節點實例的首地址,就是container_of的宏定義*/
        entry = rb_entry(parent, struct sched_entity, run_node);
        /*
         * We dont care about collisions. Nodes with
         * the same key stay together.
         */
        if (entity_before(se, entry)) {
            link = &parent->rb_left;
        } else {
            link = &parent->rb_right;
            leftmost = 0;
        }
    }

    /*
     * Maintain a cache of leftmost tree entries (it is frequently
     * used):
     */
    if (leftmost)
        cfs_rq->rb_leftmost = &se->run_node;
    /*在紅黑樹中插入節點,即將node節點插入到parent節點的左樹或者右樹,整個過程會動態調整樹結構保持平衡;*/
    rb_link_node(&se->run_node, parent, link);
    /*設置節點的顏色*/
    rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline);
}

  和上面的作用剛好相反:刪除節點

static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    if (cfs_rq->rb_leftmost == &se->run_node) {
        struct rb_node *next_node;

        next_node = rb_next(&se->run_node);
        cfs_rq->rb_leftmost = next_node;
    }

    rb_erase(&se->run_node, &cfs_rq->tasks_timeline);
}

  (4)紅黑樹建好后,最最最最重要的功能就是找出需要調度的進程了,如下:

/*
 * Pick the next process, keeping these things in mind, in this order:
 * 1) keep things fair between processes/task groups
 * 2) pick the "next" process, since someone really wants that to run
 * 3) pick the "last" process, for cache locality
 * 4) do not run the "skip" process, if something else is available
 */
static struct sched_entity *
pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
    struct sched_entity *left = __pick_first_entity(cfs_rq);//樹上最左邊的節點
    struct sched_entity *se;

    /*
     * If curr is set we have to see if its left of the leftmost entity
     * still in the tree, provided there was anything in the tree at all.
       若 second 為空, 或者 curr 的 vruntime 更小 
     */
    if (!left || (curr && entity_before(curr, left)))
        left = curr;

    se = left; /* ideally we run the leftmost entity */

    /*
     * Avoid running the skip buddy, if running something else can
     * be done without getting too unfair.
     */
    if (cfs_rq->skip == se) {
        struct sched_entity *second;

        if (se == curr) {
            second = __pick_first_entity(cfs_rq);/*返回最左邊、也就是vruntime最小的節點*/
        } else {
            second = __pick_next_entity(se);/*找到比se節點大的第一個節點*/
            if (!second || (curr && entity_before(curr, second)))
                second = curr;
        }
        /*判斷是否應該搶占當前進程*/
        if (second && wakeup_preempt_entity(second, left) < 1)
            se = second;
    }

    /*
     * Prefer last buddy, try to return the CPU to a preempted task.
     */
    if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1)
        se = cfs_rq->last;

    /*
     * Someone really wants this to run. If it's not unfair, run it.
     */
    if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1)
        se = cfs_rq->next;

    clear_buddies(cfs_rq, se);

    return se;
}

  這里啰嗦幾句:調度器有多個,都實現了pick_next_entity的方法!

       

 

 

總結:

  1、鏈表這種“憨憨”類的數據結構,能少用就盡量少用;盡量用紅黑樹替代吧,增刪改查的效率高多了!

        2、既然用紅黑樹選擇最小的vruntime節點運行,用小根堆是不是也能達到同樣的效果了?

 

參考:

 1、https://jishuin.proginn.com/p/763bfbd5f8d6 linux進程調度知識點

 2、https://mp.weixin.qq.com/s?__biz=MzA3NzYzODg1OA==&mid=2648464309&idx=1&sn=9fc763d9233fbba6d40b69b1ef54aa8b&chksm=87660610b0118f060a4da0c64417e57e8cb35f4732043106fd1b1d9a3ad6134145e0e47f5a9b&scene=21#wechat_redirect  O(1)調度算法

3、https://blog.csdn.net/longwang155069/article/details/104457109 linux O(1)調度器

4、http://www.wowotech.net/process_management/scheduler-history.html  O(1)、O(n)和CFS調度器

5、https://zhuanlan.zhihu.com/p/372441187 操作系統調度算法CFS


免責聲明!

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



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