每一個調度器類sched_class都必須提供一個pick_next_task函數用以在就緒隊列中選擇一個最優的進程來等待調度, 而咱們的CFS調度器類中, 選擇下一個將要運行的進程由pick_next_task_fair函數來完成linux
以前咱們在將主調度器的時候, 主調度器schedule函數在進程調度搶佔時, 會經過__schedule函數調用全局pick_next_task選擇一個最優的進程, 在pick_next_task
中咱們就按照優先級依次調用不一樣調度器類提供的pick_next_task
方法c#
今天就讓咱們窺探一下徹底公平調度器類CFS的pick_next_task方法pick_next_fair緩存
pick_next_task_fairapp
選擇下一個將要運行的進程pick_next_task_fair
執行. 其代碼執行流程以下負載均衡
對於pick_next_task_fair函數的講解, 咱們從simple標籤開始, 這個是常規狀態下pick_next的思路, 簡單的來講pick_next_task_fair的函數框架以下框架
again: 控制循環來讀取最優進程 #ifdef CONFIG_FAIR_GROUP_SCHED 完成組調度下的pick_next選擇 返回被選擇的調度時實體的指針 #endif simple: 最基礎的pick_next函數 返回被選擇的調度時實體的指針 idle : 若是系統中沒有可運行的進行, 則須要調度idle進程
可見咱們會發現,less
在不支持組調度狀況下(選項CONFIG_FAIR_GROUP_SCHED), CFS的pick_next_task_fair函數會直接執行simple標籤, 優選下一個函數, 這個流程清晰並且簡單, 可是已經足夠咱們理解cfs的pick_next了electron
pick_next_task_fair函數的simple標籤訂義在kernel/sched/fair.c, line 5526), 代碼以下所示ide
simple: cfs_rq = &rq->cfs; #endif /* 若是nr_running計數器爲0, * 當前隊列上沒有可運行進程, * 則須要調度idle進程 */ if (!cfs_rq->nr_running) goto idle; /* 將當前進程放入運行隊列的合適位置 */ put_prev_task(rq, prev); do { /* 選出下一個可執行調度實體(進程) */ se = pick_next_entity(cfs_rq, NULL); /* 把選中的進程從紅黑樹移除,更新紅黑樹 * set_next_entity會調用__dequeue_entity完成此工做 */ set_next_entity(cfs_rq, se); /* group_cfs_rq return NULL when !CONFIG_FAIR_GROUP_SCHED * 在非組調度狀況下, group_cfs_rq返回了NULL */ cfs_rq = group_cfs_rq(se); } while (cfs_rq); /* 在沒有配置組調度選項(CONFIG_FAIR_GROUP_SCHED)的狀況下.group_cfs_rq()返回NULL.所以,上函數中的循環只會循環一次 */ /* 獲取到調度實體指代的進程信息 */ p = task_of(se); if (hrtick_enabled(rq)) hrtick_start_fair(rq, p); return p;
其基本流程以下函數
流程 | 描述 |
---|---|
!cfs_rq->nr_running -=> goto idle; | 若是nr_running計數器爲0, 當前隊列上沒有可運行進程, 則須要調度idle進程 |
put_prev_task(rq, prev); | 將當前進程放入運行隊列的合適位置, 每次當進程被調度後都會使用set_next_entity從紅黑樹中移除, 所以被搶佔時須要從新加如紅黑樹中等待被調度 |
se = pick_next_entity(cfs_rq, NULL); | 選出下一個可執行調度實體 |
set_next_entity(cfs_rq, se); | set_next_entity會調用__dequeue_entity把選中的進程從紅黑樹移除,並更新紅黑樹 |
put_prev_task
用來將前一個進程prev放回到就緒隊列中, 這是一個全局的函數, 而每一個調度器類也必須實現一個本身的put_prev_task函數(好比CFS的put_prev_task_fair),
因爲CFS調度的時候, prev進程不必定是一個CFS調度的進程, 所以必須調用全局的put_prev_task來調用prev進程所屬調度器類sched_class的對應put_prev_task方法, 完成將進程放回到就緒隊列中
全局的put_prev_task函數定義在kernel/sched/sched.h, line 1245, 代碼以下所示
static inline void put_prev_task(struct rq *rq, struct task_struct *prev) { prev->sched_class->put_prev_task(rq, prev); }
而後咱們來分析一下CFS的put_prev_task_fair函數, 其定義在kernel/sched/fair.c, line 5572
在選中了下一個將被調度執行的進程以後,回到pick_next_task_fair
中,執行set_next_entity
/* * Account for a descheduled task: */ static void put_prev_task_fair(struct rq *rq, struct task_struct *prev) { struct sched_entity *se = &prev->se; struct cfs_rq *cfs_rq; for_each_sched_entity(se) { cfs_rq = cfs_rq_of(se); put_prev_entity(cfs_rq, se); } }
前面咱們說到過函數在組策略狀況下, 調度實體之間存在父子的層次, for_each_sched_entity會從當前調度實體開始, 而後循環向其父調度實體進行更新, 非組調度狀況下則只執行一次
而put_prev_task_fair
函數最終會調用put_prev_entity函數將prev的調度時提se放回到就緒隊列中等待下次調度
put_prev_entity函數定義在kernel/sched/fair.c, line 3443, 他在更新了虛擬運行時間等信息後, 最終經過__enqueue_entity函數將prev進程(即current進程)放回就緒隊列rq上
/* * 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 * * 1. 首先要確保任務組之間的公平, 這也是設置組的緣由之一 * 2. 其次, 挑選下一個合適的(優先級比較高的)進程 * 由於它確實須要立刻運行 * 3. 若是沒有找到條件2中的進程 * 那麼爲了保持良好的局部性 * 則選中上一次執行的進程 * 4. 只要有任務存在, 就不要讓CPU空轉, * 只有在沒有進程的狀況下才會讓CPU運行idle進程 */ 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. * * 若是 * left == NULL 或者 * curr != NULL curr進程比left進程更優(即curr的虛擬運行時間更小) * 說明curr進程是自動放棄運行權利, 且其比最左進程更優 * 所以將left指向了curr, 即curr是最優的進程 */ if (!left || (curr && entity_before(curr, left))) { left = curr; } /* se = left存儲了cfs_rq隊列中最優的那個進程 * 若是進程curr是一個自願放棄CPU的進程(其比最左進程更優), 則取se = curr * 不然進程se就取紅黑樹中最左的進程left, 它必然是當前就緒隊列上最優的 */ se = left; /* ideally we run the leftmost entity */ /* * Avoid running the skip buddy, if running something else can * be done without getting too unfair. * * cfs_rq->skip存儲了須要調過不參與調度的進程調度實體 * 若是咱們挑選出來的最優調度實體se正好是skip * 那麼咱們須要選擇次優的調度實體se來進行調度 * 因爲以前的se = left = (curr before left) curr left * 則若是 se == curr == skip, 則選擇left = __pick_first_entity進行便可 * 不然則se == left == skip, 則選擇次優的那個調度實體second */ if (cfs_rq->skip == se) { struct sched_entity *second; if (se == curr) /* se == curr == skip選擇最左的那個調度實體left */ { second = __pick_first_entity(cfs_rq); } else /* 不然se == left == skip, 選擇次優的調度實體second */ { /* 摘取紅黑樹上第二左的進程節點 */ second = __pick_next_entity(se); /* 同時與left進程同樣, * 若是 * second == NULL 沒有次優的進程 或者 * curr != NULL curr進程比left進程更優(即curr的虛擬運行時間更小) * 說明curr進程比最second進程更優 * 所以將second指向了curr, 即curr是最優的進程*/ if (!second || (curr && entity_before(curr, second))) second = curr; } /* 判斷left和second的vruntime的差距是否小於sysctl_sched_wakeup_granularity * 即若是second能搶佔left */ 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; /* 用過一次任何一個next或者last * 都須要清除掉這個指針 * 以避免影響到下次pick next sched_entity */ clear_buddies(cfs_rq, se); return se; }
pick_next_entity則從CFS的紅黑樹中摘取一個最優的進程, 這個進程每每在紅黑樹的最左端, 即vruntime最小, 可是也有例外, 可是不外乎這幾個進程
調度實體 | 描述 |
---|---|
left = __pick_first_entity(cfs_rq) | 紅黑樹的最左節點, 這個節點擁有當前隊列中vruntime最小的特性, 即應該優先被調度 |
second = __pick_first_entity(left) | 紅黑樹的次左節點, 爲何這個節點也可能呢, 由於內核支持skip跳過某個進程的搶佔權力的, 若是left被標記爲skip(由cfs_rq->skip域指定), 那麼可能就須要找到次優的那個進程 |
curr結點 | curr節點的vruntime可能比left和second更小, 可是因爲它正在運行, 所以它不在紅黑樹中(進程搶佔物理機的時候對應節點同時會從紅黑樹中刪除), 可是若是其vruntime足夠小, 意味着cfs調度器應該儘量的補償curr進程, 讓它再次被調度 |
其中__pick_first_entity會返回cfs_rq紅黑樹中的最左節點rb_leftmost所屬的調度實體信息, 該函數定義在kernel/sched/fair.c, line 543
而__pick_next_entity(se)函數則返回se在紅黑樹中中序遍歷的下一個節點信息, 該函數定義在kernel/sched/fair.c, line 544, 獲取下一個節點的工做能夠經過內核紅黑樹的標準操做rb_next完成
在pick_next_entity的最後, 要把紅黑樹最左下角的進程和另外兩個進程(即next和last)作比較, next是搶佔失敗的進程, 而last則是搶佔成功後被搶佔的進程, 這三個進程到底哪個是最優的next進程呢?
Linux CFS實現的判決條件是:
cfs_rq的last和next指針,last表示最後一個執行wakeup的sched_entity,next表示最後一個被wakeup的sched_entity。他們在進程wakeup的時候會賦值,在pick新sched_entity的時候,會優先選擇這些last或者next指針的sched_entity,有利於提升緩存的命中率
所以咱們優選出來的進程必須同last和next指針域進行對比, 其實就是檢查就緒隊列中的最優進程, 即紅黑樹中最左節點last是否能夠搶佔last和next指針域, 檢查是否能夠搶佔是經過wakeup_preempt_entity
函數來完成的.
// http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L5317 /* * Should 'se' preempt 'curr'. * * |s1 * |s2 * |s3 * g * |<--->|c * * w(c, s1) = -1 * w(c, s2) = 0 * w(c, s3) = 1 * */ static int wakeup_preempt_entity(struct sched_entity *curr, struct sched_entity *se) { /* vdiff爲curr和se vruntime的差值*/ s64 gran, vdiff = curr->vruntime - se->vruntime; /* cfs_rq的vruntime是單調遞增的,也就是一個基準 * 各個進程的vruntime追趕競爭cfsq的vruntime * 若是curr的vruntime比較小, 說明curr更加須要補償, * 即se沒法搶佔curr */ if (vdiff <= 0) return -1; /* 計算curr的最小搶佔期限粒度 */ gran = wakeup_gran(curr, se); /* 當差值大於這個最小粒度的時候才搶佔,這能夠避免頻繁搶佔 */ if (vdiff > gran) return 1; return 0; } // http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L5282 static unsigned long wakeup_gran(struct sched_entity *curr, struct sched_entity *se) { /* NICE_0_LOAD的基準最小運行期限 */ unsigned long gran = sysctl_sched_wakeup_granularity; /* * Since its curr running now, convert the gran from real-time * to virtual-time in his units. * * By using 'se' instead of 'curr' we penalize light tasks, so * they get preempted easier. That is, if 'se' < 'curr' then * the resulting gran will be larger, therefore penalizing the * lighter, if otoh 'se' > 'curr' then the resulting gran will * be smaller, again penalizing the lighter task. * * This is especially important for buddies when the leftmost * task is higher priority than the buddy. * * 計算進程運行的期限,即搶佔的粒度 */ return calc_delta_fair(gran, se); }
到底能不能選擇last和next兩個進程, 則是wakeup_preempt_entity函數來決定的, 看下面的圖解便可:
若是S3是left,curr是next或者last,left的vruntime值小於curr和next, 函數wakeup_preempt_entity確定返回1,那麼就說明next和last指針的vruntime和left差距過大,這個時候沒有必要選擇這個last或者next指針,而是應該優先補償left
若是next或者last是S2,S1,那麼vruntime和left差距並不大,並無超過sysctl_sched_wakeup_granularity ,那麼這個next或者last就能夠被優先選擇,而代替了left
而清除last和next這兩個指針的時機有這麼幾個:
如今咱們已經經過pick_next_task_fair選擇了進程, 可是還須要完成一些工做, 才能將其標記爲運行進程. 這是經過set_next_entity來處理的. 該函數定義在kernel/sched/fair.c, line 3348
當前執行進程(咱們選擇出來的進程立刻要搶佔處理器開始執行)不該該再保存在就緒隊列上, 所以set_next_entity()函數會調用__dequeue_entity(cfs_rq, se)把選中的下一個進程移出紅黑樹. 若是當前進程是最左節點, __dequeue_entity會將leftmost指針設置到次左進程
/* 'current' is not kept within the tree. */ if (se->on_rq) /* 若是se尚在rq隊列上 */ { /* ...... */ /* 將se從cfs_rq的紅黑樹中刪除 */ __dequeue_entity(cfs_rq, se); /* ...... */ }
儘管該進程再也不包含在紅黑樹中, 可是進程和就緒隊列之間的關聯並無丟失, 由於curr標記了當前進程cfs_rq->curr = se;
cfs_rq->curr = se;
而後接下來是一些統計信息的處理, 若是內核開啓了調度統計CONFIG_SCHEDSTATS標識, 則會完成調度統計的計算和更新
#ifdef CONFIG_SCHEDSTATS /* * Track our maximum slice length, if the CPU's load is at * least twice that of our own weight (i.e. dont track it * when there are only lesser-weight tasks around): */ if (schedstat_enabled() && rq_of(cfs_rq)->load.weight >= 2*se->load.weight) { se->statistics.slice_max = max(se->statistics.slice_max, se->sum_exec_runtime - se->prev_sum_exec_runtime); } #endif
在set_next_entity的最後, 將選擇出的調度實體se的sum_exec_runtime保存在了prev_sum_exec_runtime中, 由於該調度實體指向的進程, 立刻將搶佔處理器成爲當前活動進程, 在CPU上花費的實際時間將記入sum_exec_runtime, 所以內核會在prev_sum_exec_runtime保存此前的設置. 要注意進程中的sum_exec_runtime沒有重置. 所以差值sum_exec_runtime - prev_sum_runtime確實標識了在CPU上執行花費的實際時間.
最後咱們附上set_next_entity函數的完整註釋信息
static void set_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) { /* 'current' is not kept within the tree. */ if (se->on_rq) /* 若是se尚在rq隊列上 */ { /* * Any task has to be enqueued before it get to execute on * a CPU. So account for the time it spent waiting on the * runqueue. */ if (schedstat_enabled()) update_stats_wait_end(cfs_rq, se); /* 將se從cfs_rq的紅黑樹中刪除 */ __dequeue_entity(cfs_rq, se); update_load_avg(se, 1); } /* 新sched_entity中的exec_start字段爲當前clock_task */ update_stats_curr_start(cfs_rq, se); /* 將se設置爲curr進程 */ cfs_rq->curr = se; #ifdef CONFIG_SCHEDSTATS /* * Track our maximum slice length, if the CPU's load is at * least twice that of our own weight (i.e. dont track it * when there are only lesser-weight tasks around): */ if (schedstat_enabled() && rq_of(cfs_rq)->load.weight >= 2*se->load.weight) { se->statistics.slice_max = max(se->statistics.slice_max, se->sum_exec_runtime - se->prev_sum_exec_runtime); } #endif /* 更新task上一次投入運行的從時間 */ se->prev_sum_exec_runtime = se->sum_exec_runtime; }
/* 若是nr_running計數器爲0, * 當前隊列上沒有可運行進程, * 則須要調度idle進程 */ if (!cfs_rq->nr_running) goto idle;
若是系統中當前運行隊列上沒有可調度的進程, 那麼會調到idle標籤去調度idle進程.
idle標籤以下所示
idle: /* * This is OK, because current is on_cpu, which avoids it being picked * for load-balance and preemption/IRQs are still disabled avoiding * further scheduler activity on it and we're being very careful to * re-start the picking loop. */ lockdep_unpin_lock(&rq->lock); new_tasks = idle_balance(rq); lockdep_pin_lock(&rq->lock); /* * Because idle_balance() releases (and re-acquires) rq->lock, it is * possible for any higher priority task to appear. In that case we * must re-start the pick_next_entity() loop. */ if (new_tasks < 0) return RETRY_TASK; if (new_tasks > 0) goto again; return NULL;
其關鍵就是調用idle_balance進行任務的遷移
每一個cpu都有本身的運行隊列, 若是當前cpu上運行的任務都已經dequeue出運行隊列,並且idle_balance也沒有移動到當前運行隊列的任務,那麼schedule函數中,按照stop > idle > rt > cfs > idle這三種調度方式順序,尋找各自的運行任務,那麼若是rt和cfs都未找到運行任務,那麼最後會調用idle schedule的idle進程,做爲schedule函數調度的下一個任務
若是某個cpu空閒, 而其餘CPU不空閒, 即當前CPU運行隊列爲NULL, 而其餘CPU運行隊列有進程等待調度的時候, 則內核會對CPU嘗試負載平衡, CPU負載均衡有兩種方式: pull和push, 即空閒CPU從其餘忙的CPU隊列中pull拉一個進程複製到當前空閒CPU上, 或者忙的CPU隊列將一個進程push推送到空閒的CPU隊列中.
idle_balance其實就是pull的工做.
組調度的情形下, 調度實體之間存在明顯的層次關係, 所以在跟新子調度實體的時候, 須要更新父調度實體的信息, 同時咱們爲了保證同一組內的進程不能長時間佔用處理機, 必須補償其餘組內的進程, 保證公平性
#ifdef CONFIG_FAIR_GROUP_SCHED /* 若是nr_running計數器爲0, 即當前隊列上沒有可運行進程, * 則須要調度idle進程 */ if (!cfs_rq->nr_running) goto idle; /* 若是當前運行進程prev不是被fair調度的普通非實時進程 */ if (prev->sched_class != &fair_sched_class) goto simple; /* * Because of the set_next_buddy() in dequeue_task_fair() it is rather * likely that a next task is from the same cgroup as the current. * * Therefore attempt to avoid putting and setting the entire cgroup * hierarchy, only change the part that actually changes. */ do { struct sched_entity *curr = cfs_rq->curr; /* * Since we got here without doing put_prev_entity() we also * have to consider cfs_rq->curr. If it is still a runnable * entity, update_curr() will update its vruntime, otherwise * forget we've ever seen it. */ if (curr) { /* 若是當前進程curr在隊列上, * 則須要更新起統計量和虛擬運行時間 * 不然設置curr爲空 */ if (curr->on_rq) update_curr(cfs_rq); else curr = NULL; /* * This call to check_cfs_rq_runtime() will do the * throttle and dequeue its entity in the parent(s). * Therefore the 'simple' nr_running test will indeed * be correct. */ if (unlikely(check_cfs_rq_runtime(cfs_rq))) goto simple; } /* 選擇一個最優的調度實體 */ se = pick_next_entity(cfs_rq, curr); cfs_rq = group_cfs_rq(se); } while (cfs_rq); /* 若是被調度的進程仍屬於當前組,那麼選取下一個可能被調度的任務,以保證組間調度的公平性 */ /* 獲取調度實體se的進程實體信息 */ p = task_of(se); /* * Since we haven't yet done put_prev_entity and if the selected task * is a different task than we started out with, try and touch the * least amount of cfs_rqs. */ if (prev != p) { struct sched_entity *pse = &prev->se; while (!(cfs_rq = is_same_group(se, pse))) { int se_depth = se->depth; int pse_depth = pse->depth; if (se_depth <= pse_depth) { put_prev_entity(cfs_rq_of(pse), pse); pse = parent_entity(pse); } if (se_depth >= pse_depth) { set_next_entity(cfs_rq_of(se), se); se = parent_entity(se); } } put_prev_entity(cfs_rq, pse); set_next_entity(cfs_rq, se); } if (hrtick_enabled(rq)) hrtick_start_fair(rq, p); return p;
咱們在以前講解主調度器的時候就提到過, 主調度器函數schedule會調用__schedule來完成搶佔, 而主調度器的主要功能就是選擇一個新的進程來搶佔到當前的處理器. 所以其中必然不能缺乏pick_next_task工做
參見主調度器schedule)中調用全局的pick_next_task選擇搶佔的進程一節的內容
__schedule調用全局的pick_next_task函數選擇一個最優的進程, 內核代碼參見kernel/sched/core.c, line 3142
static void __sched notrace __schedule(bool preempt) { /* ...... */ next = pick_next_task(rq); /* ...... */ }
全局的pick_next_task函數會從按照優先級遍歷全部調度器類的pick_next_task函數, 去查找最優的那個進程, 固然由於大多數狀況下, 系統中全是CFS調度的非實時進程, 於是linux內核也有一些優化的策略
其執行流程以下