Linux程序排程CFS演算法實現分析
網上講CFS的文章很多,可能版本不一,理解不盡相同。我以問題追溯方式,跟蹤原始碼寫下我對CFS的理解,有的問題我也還沒理解透,歡迎對核心有興趣的朋友一起交流學習,原始碼版本是與LKD3配套的Linux2.6.34
背景知識:
(1) Linux的排程器類主要實現兩類程序排程演算法:實時排程演算法和完全公平排程演算法(CFS),實時排程演算法SCHED_FIFO和SCHED_RR,按優先順序執行,一般不會被搶佔。直到實時程序執行完,才會執行普通程序。而大多數的普通程序,用的就是CFS演算法。
(2) 程序排程的時機:
①程序狀態轉換時刻:程序終止、程序睡眠;
②當前程序的”時間片”用完;
③主動讓出處理器,使用者呼叫sleep()或者核心呼叫schedule();
④從中斷,系統呼叫或異常返回時;
(3) 每個程序task_struct中都有一個struct
sched_entity se成員,這就是排程器的實體結構,程序排程演算法實際上就是管理所有程序的這個se。
點選(此處)摺疊或開啟
-
struct task_struct {
-
volatile long state; /* -1 unrunnable, 0 runnable, >
-
void *stack;
-
atomic_t usage;
-
unsigned int flags; /* per process flags, defined below */
-
unsigned int ptrace;
-
int lock_depth; /* BKL lock depth */
-
#ifdef CONFIG_SMP
-
#ifdef __ARCH_WANT_UNLOCKED_CTXSW
-
int
-
#endif
-
#endif
-
int prio, static_prio, normal_prio;
-
unsigned int rt_priority;
-
const struct sched_class *sched_class;
- struct sched_entity se; //程序排程實體
-
struct sched_rt_entity rt;
-
…
- }
CFS基於一個簡單的理念:所有任務都應該公平的分配處理器。理想情況下,n個程序的排程系統中,每個程序獲得1/n處理器時間,所有程序的vruntime也是相同的。
CFS完全拋棄了時間片的概念,而是分配一個處理器使用比來度量。
1.理解CFS的首先要理解vruntime的含義
簡單說vruntime就是該程序的執行時間,但這個時間是通過優先順序和系統負載等加權過的時間,而非物理時鐘時間,按字面理解為虛擬執行時間,也很恰當。
每個程序的排程實體se都儲存著本程序的虛擬執行時間。
點選(此處)摺疊或開啟
-
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;
-
…
- }
而程序相關的排程方法如下
點選(此處)摺疊或開啟
-
static const struct sched_class fair_sched_class = {
-
.next = &idle_sched_class,
-
.enqueue_task = enqueue_task_fair,
-
.dequeue_task = dequeue_task_fair,
-
.yield_task = yield_task_fair,
-
.check_preempt_curr = check_preempt_wakeup,
-
.pick_next_task = pick_next_task_fair,
-
.put_prev_task = put_prev_task_fair,
-
#ifdef CONFIG_SMP
-
.select_task_rq = select_task_rq_fair,
-
.rq_online = rq_online_fair,
-
.rq_offline = rq_offline_fair,
-
.task_waking = task_waking_fair,
-
#endif
-
.set_curr_task = set_curr_task_fair,
-
.task_tick = task_tick_fair,
-
.task_fork = task_fork_fair,
-
.prio_changed = prio_changed_fair,
-
.switched_to = switched_to_fair,
-
.get_rr_interval = get_rr_interval_fair,
-
#ifdef CONFIG_FAIR_GROUP_SCHED
-
.task_move_group = task_move_group_fair,
-
#endif
- };
2.vruntime的值如何跟新?
時鐘中斷產生時,會依次呼叫tick_periodic()->
update_process_times()->scheduler_tick()
點選(此處)摺疊或開啟
-
void scheduler_tick(void)
-
{
-
…
-
raw_spin_lock(&rq->lock);
-
update_rq_clock(rq);
-
update_cpu_load(rq);
-
curr->sched_class->task_tick(rq, curr, 0); //執行排程器tick,更新程序vruntime
-
raw_spin_unlock(&rq->lock);
-
…
-
}
-
task_tick_fair ()呼叫entity_tick()如下:
-
static void entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
-
{
-
update_curr(cfs_rq);
-
…
-
if (cfs_rq->nr_running > 1 || !sched_feat(WAKEUP_PREEMPT))
-
check_preempt_tick(cfs_rq, curr); //檢查當前程序是否需要排程
- }
這裡分析兩個重要函式update_curr()和check_preempt_tick()
點選(此處)摺疊或開啟
-
static void update_curr(struct cfs_rq *cfs_rq)
-
{
-
struct sched_entity *curr = cfs_rq->curr;
-
u64 now = rq_of(cfs_rq)->clock;
-
unsigned long delta_exec;
-
if (unlikely(!curr))
-
return;
-
// delta_exec獲得最後一次修改後,當前程序所執行的實際時間
-
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);
-
}
- }
主要關心__update_curr()函式
點選(此處)摺疊或開啟
-
static inline void __update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr, unsigned long delta_exec)
-
{
-
unsigned long delta_exec_weighted;
-
schedstat_set(curr->exec_max, max((u64)delta_exec, curr->exec_max));
-
curr->sum_exec_runtime += delta_exec;//累計實際執行時間
-
schedstat_add(cfs_rq, exec_clock, delta_exec);
-
delta_exec_weighted = calc_delta_fair(delta_exec, curr);//對delta_exec加權
-
curr->vruntime += delta_exec_weighted;//累計入vruntime
-
update_min_vruntime(cfs_rq); //更新cfs_rq最小vruntime(儲存所有程序中的最小vruntime)
- }
關注calc_delta_fair()加權函式如何實現
點選(此處)摺疊或開啟
-
/*
-
* delta /= w
-
*/
-
static inline unsigned long
-
calc_delta_fair(unsigned long delta, struct sched_entity *se)
-
{
-
if (unlikely(se->load.weight != NICE_0_LOAD))
-
delta = calc_delta_mine(delta, NICE_0_LOAD, &se->load);
-
return delta;
- }
若當前程序nice為0,直接返回實際執行時間,其他所有nice值的加權都是以0nice值為參考增加或減少的。
點選(此處)摺疊或開啟
-
/*
-
* delta *= weight / lw
-
*/
-
static unsigned long
-
calc_delta_mine(unsigned long delta_exec, unsigned long weight,
-
struct load_weight *lw)
-
{
-
u64 tmp;
-
if (!lw->inv_weight) {
-
if (BITS_PER_LONG > 32 && unlikely(lw->weight >= WMULT_CONST))
-
lw->inv_weight = 1;
-
else
-
lw->inv_weight = 1 + (WMULT_CONST-lw->weight/2)
-
/ (lw->weight+1);//這公式沒弄明白
-
}
-
tmp = (u64)delta_exec * weight;
-
/*
-
* Check whether we'd overflow the 64-bit multiplication:
-
*/
-
if (unlikely(tmp > WMULT_CONST))
-
tmp = SRR(SRR(tmp, WMULT_SHIFT/2) * lw->inv_weight,
-
WMULT_SHIFT/2);
-
else
-
tmp = SRR(tmp * lw->inv_weight, WMULT_SHIFT);//做四捨五入
-
return (unsigned long)min(tmp, (u64)(unsigned long)LONG_MAX);
- }
當nice!=0時,實際是按公式delta *= weight / lw來計算的weight=1024是nice0的權重,lw是當前程序的權重,該lw和nice值的換算後面介紹,上面還書的lw計算公式沒弄明白,總之這個函式就是把實際執行時間加權為程序排程裡的虛擬執行時間,從而更新vruntime。
更新完vruntime之後,會檢查是否需要程序排程
點選(此處)摺疊或開啟
-
回到static void entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
-
{
-
update_curr(cfs_rq);
-
…
-
if (cfs_rq->nr_running > 1 || !sched_feat(WAKEUP_PREEMPT))
-
check_preempt_tick(cfs_rq, curr); //檢查當前程序是否需要排程
- }
更新完cfs_rq之後,會檢查當前程序是否已經用完自己的“時間片”
點選(此處)摺疊或開啟
-
/*
-
* Preempt the current task with a newly woken task if needed:
-
*/
-
static void
-
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
-
{
-
unsigned long ideal_runtime, delta_exec;
- ideal_runtime = sched_slice(cfs_rq, curr);//ideal_runtime是理論上的處理器執行時間片
-
delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_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;
-
}
-
/*
-
* Ensure that a task that missed wakeup preemption by a
-
* narrow margin doesn't have to wait for a full slice.
-
* This also mitigates buddy induced latencies under load.
-
*/
-
if (!sched_feat(WAKEUP_PREEMPT))
-
return;
-
if (delta_exec < sysctl_sched_min_granularity)
-
return;
-
if (cfs_rq->nr_running > 1) {
-
struct sched_entity *se = __pick_next_entity(cfs_rq);
-
s64 delta = curr->vruntime - se->vruntime;
-
if (delta > ideal_runtime)
-
resched_task(rq_of(cfs_rq)->curr);
-
}
- }
當該程序執行時間超過實際分配的“時間片”,就標記排程標誌resched_task(rq_of();,否則本程序繼續執行。中斷退出,排程函式schedule()會檢查此標記,以選取新的程序來搶佔當前程序
3.如何選擇下一個可執行程序
CFS選擇具有最小vruntime值的程序作為下一個可執行程序,CFS用紅黑樹來組織排程實體,而鍵值就是vruntime。那麼CFS只要查詢選擇最左葉子節點作為下一個可執行程序即可。實際上CFS快取了最左葉子,可以直接選取left_most葉子。
上面程式碼跟蹤到timer tick中斷退出,若“ideal_runtime”已經用完,就會呼叫schedule()函式選中新程序並且完成切換。
點選(此處)摺疊或開啟
-
asmlinkage void __sched schedule(void)
-
{
-
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
-
if (unlikely(signal_pending_state(prev->state, prev)))
-
prev->state = TASK_RUNNING;
-
else
-
deactivate_task(rq, prev, 1);//如果狀態已經不可執行,將其移除執行佇列
-
switch_count = &prev->nvcsw;
-
}
-
pre_schedule(rq, prev);
-
if (unlikely(!rq->nr_running))
-
idle_balance(cpu, rq);
-
put_prev_task(rq, prev); //處理上一個程序
-
next = pick_next_task(rq);//選出下一個程序
-
…
-
context_switch(rq, prev, next); /* unlocks the rq *///完成程序切換
-
…
- }
如果程序狀態已經不是可執行,那麼會將該程序移出可執行佇列,如果繼續可執行
put_prev_task()會依次呼叫put_prev_task_fair()->put_prev_entity()
點選(此處)摺疊或開啟
-
static void put_prev_entity(struct cfs_rq *cfs_rq, struct sched_entity *prev)
-
{
-
/*
-
* If still on the runqueue then deactivate_task()
-
* was not called and update_curr() has to be done:
-
*/
-
if (prev->on_rq)
-
update_curr(cfs_rq);
-
check_spread(cfs_rq, prev);
-
if (prev->on_rq) {
-
update_stats_wait_start(cfs_rq, prev);
-
/* Put 'current' back into the tree. */
-
__enqueue_entity(cfs_rq, prev);
-
}
-
cfs_rq->curr = NULL;
- }
__enqueue_entity(cfs_rq, prev) 將上一個程序重新插入紅黑樹(注意,當前執行程序是不在紅黑樹中的)
pick_next_task()會依次呼叫pick_next_task_fair()
點選(此處)摺疊或開啟
-
static struct task_struct *pick_next_task_fair(struct rq *rq)
-
{
-
struct task_struct *p;
-
struct cfs_rq *cfs_rq = &rq->cfs;
-
struct sched_entity *se;
-
if (!cfs_rq->nr_running)
-
return NULL;
-
do {
-
se = pick_next_entity(cfs_rq);//選出下一個可執行程序
-
set_next_entity(cfs_rq, se); //把選中的程序(left_most葉子)從紅黑樹移除,更新紅黑樹
-
cfs_rq = group_cfs_rq(se);
-
} while (cfs_rq);
-
p = task_of(se);
-
hrtick_start_fair(rq, p);
-
return p;
- }
set_next_entity()函式會呼叫__dequeue_entity(cfs_rq, se)把選中的下一個程序即最左葉子移出紅黑樹。
最後context_switch()完成程序的切換。
4.何時更新rbtree
①上一個程序執行完ideal_time,還可繼續執行時,會插入紅黑樹;
②下一個程序被選中移出rbtree紅黑樹時;
③新建程序
④程序由睡眠態被啟用,變為可執行態時
⑤調整優先順序時也會更新rbtree
5.新建程序如何加入紅黑樹
新建程序會做一系列複雜的工作,這裡我們只關心與紅黑樹有關部分
Linux使用fork,clone或者vfork等系統呼叫建立程序,最終都會到do_fork函式實現,如果沒有設定CLONE_STOPPED,do_fork會執行兩個與紅黑樹相關的函式: copy_process()和wake_up_new_task()
(1) copy_process()->sched_fork()->task_fork()
點選(此處)摺疊或開啟
-
static void place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
-
{
-
u64 vruntime = cfs_rq->min_vruntime;//以cfs的最小vruntime為基準
- /