天天看點

CFS排程器中新程序建立之後的事情---任何事情都是一種權衡(續)

linux中的程序是個很重要的概念,這個就不必多說了,linux中程序建立的fork機制繼承了unix的基因,是作業系統中最重要的東西,fork中的寫時複制機制是fork的精髓,是程序機制的精髓,它不僅僅代表了這些,它的實作還幫了另一個忙,這就是一般說來,linux在fork之後一般讓新程序先運作,這是為了避免不必要的寫時複制操作,因為新程序往往不再操作父程序的位址空間而是馬上進行新的邏輯或者進行exec調用,但是卻複制了父程序的位址空間,如果父程序優先運作,那麼父程序的每一步運作隻要是寫操作都會導緻寫時複制,這是個根本沒有必要的操作,系統的機制雖然要求寫時複制,但是政策上卻是很少會有子程序操作父程序位址空間的情況,父程序操作其位址空間卻是一定的,因為它們共享一個位址空間,是以會導緻沒有用的寫時複制,是以解決的辦法就是讓子程序先運作,最起碼一旦子程序進行了exec,寫時複制就再也麼有必要了,而這是大多數的情況,在O(1)排程器時期,在fork中有以下邏輯:

if (!(clone_flags & CLONE_STOPPED))

wake_up_new_task(p, clone_flags);

就是說喚醒子程序,喚醒過程:

if (likely(cpu == this_cpu)) {

if (!(clone_flags & CLONE_VM)) {

if (unlikely(!current->array))

__activate_task(p, rq);

else {

p->prio = current->prio;

p->normal_prio = current->normal_prio;

list_add_tail(&p->run_list, ¤t->run_list); //注意,排隊位置和正常的入隊不一樣

p->array = current->array;

p->array->nr_active++;

inc_nr_running(p, rq);

}

set_need_resched(); //為了讓子程序先運作,設定目前程序的排程标志

} else //如果不在這個cpu上,就由smp權衡吧!個邏輯很簡單,就是讓子程序先運作。

事情在O(1)排程器時期是如此簡單,但是在cfs中呢?還是這麼顯然嗎?不是了,在cfs中,讓子程序先運作這件事不再是機制導緻的了,而是使用者配置的,使用者完全可以不讓子程序先運作,這進一步讓核心脫離了政策的設定,但是無論如何,核心預設讓子程序先運作,使用者可以通過配置設定是否讓子程序先運作,在cfs中wake_up_new_task為:

void wake_up_new_task(struct task_struct *p, unsigned long clone_flags)

{

unsigned long flags;

struct rq *rq;

rq = task_rq_lock(p, &flags);

BUG_ON(p->state != TASK_RUNNING);

update_rq_clock(rq);

p->prio = effective_prio(p);

if (!p->sched_class->task_new || !current->se.on_rq) {

activate_task(rq, p, 0); //按照傳統的機制進行

} else {

p->sched_class->task_new(rq, p); //OO性質進一步展現

inc_nr_running(rq);

trace_sched_wakeup_new(rq, p);

check_preempt_curr(rq, p, 0); //這裡是一次機會,因為有新人入隊了,是以在重大事件前,目前程序必須被權衡,是這麼回事嗎?一般而言隻要update_curr之後就會權衡一下目前程序是否應該會繼續運作,是這樣嗎?看看cfs中task_new中的update_curr調用

...

task_rq_unlock(rq, &flags);

在cfs中,其task_new是什麼呢?

static void task_new_fair(struct rq *rq, struct task_struct *p)

struct cfs_rq *cfs_rq = task_cfs_rq(p);

struct sched_entity *se = &p->se, *curr = cfs_rq->curr;

int this_cpu = smp_processor_id();

sched_info_queued(p);

update_curr(cfs_rq);

place_entity(cfs_rq, se, 1); //這個是本函數中最重要的

//sysctl_sched_child_runs_first為1是子程序先運作的必要條件

if (sysctl_sched_child_runs_first && this_cpu == task_cpu(p) && curr && curr->vruntime < se->vruntime) {

swap(curr->vruntime, se->vruntime); //交換子程序和父程序,就是讓子程序先運作

resched_task(rq->curr); //這個再說我就無語了

enqueue_task_fair(rq, p, 0);

我可以用不能再重要來形容place_entity函數,因為它對新程序進行了零歲教育,嬰兒的活力十足是有目共睹的。注意,這個函數中隻是盡量讓子程序最早運作,fork的寫時複制避免機制卻是沒有在這裡展現,因為在cfs中,其實是在排程類出現以後,子程序先運作這件事這個政策就從核心中脫離了,核心不再關注政策。進一步,子程序應該何時運作呢?這裡要明白,place_entity的代碼最壞的打算就是使用者沒有設定子程序優先運作的政策,但是它還是想讓子程序雖然不是優先但最起碼要盡早運作,另一方面,如果使用者設定了子程序優先運作并且符合别的條件,以下馬上要交換目前程序(父程序)和子程序,cfs排程器保證目前程序雖然被新程序搶占了,但是不能因為這次搶占延誤了太多,是以place_entity中存在了太多的政策,我們先看一下最簡單的,就是沒有START_DEBIT的情況下,新程序的vruntime就是運作隊列的min_vrntime,如果目前程序的vrntime小于min_vruntime,那麼排程器會在place_entity之前的update_curr中将目前程序的vruntime設定為min_vruntime,是以此時新程序的vruntime将是和父程序的vruntime相等,在後面的if (sysctl_sched_child_runs_first && this_cpu == task_cpu(p) && curr && curr->vruntime < se->vruntime)判定中不會通過,然後在再後面的入隊操作中會排到目前程序的右邊,這樣就不會搶占目前程序,結果就是會有更多的寫時複制;如果目前程序的vrantime大于min_vruntime,那麼新程序的vruntime肯定小于目前程序的vruntime,搶占是肯定的嗎?不,因為如果目前程序如果還沒有完成自己的一個虛拟時間的推進,那麼還是無法搶占,但是如果恰好完成了一次推進,也就是,那麼搶占就是必然的了,是以,寫時複制是有限的,畢竟公平是需要代價的,不能說為了不進行一次無用的寫時複制而破壞了cfs的公平規則。現在分析另外一部分,在設定了START_DEBIT的情況下,這種情況看似複雜其實不然,debit的意思就是記賬,就是将欠款計入欠款的借方,這個很好了解,看下面的代碼:

if (initial && sched_feat(START_DEBIT))

vruntime += sched_vslice(cfs_rq, se);

這裡的vrntime就是min_vruntime,意思是說,假設新程序已經運作了min_vruntime的虛拟時間了,在此基礎上又運作了sched_vslice(cfs_rq, se)虛拟時間,但是新程序并沒有運作,這筆帳記到誰身上呢?當然是其父程序身上,這裡的思想就是目前的紅黑樹的最最左下的程序已經承諾給一個程序了,不應該給新的程序,除非最左下的程序就是新程序的父程序,我們不考慮别的程序,僅考慮父子程序,結果就是如果沒有設定子程序先運作,那麼将不考慮額外的什麼,而是完全按照cfs的機制進行排程,新程序假設已經運作了min_vruntime個虛拟時間了,另外有運作了sched_vslice(cfs_rq, se)個虛拟時間,它已經完成了它的最後一次導緻不公平的運作,将要入隊,這很簡單,是嗎?是的!結果是目前程序或者别的程序繼續運作而不管新程序的死活,但是一旦使用者設定了新程序優先運作,那麼新程序既然被cfs排程器認為已經在min_vruntime上運作了sched_vslice(cfs_rq, se)個虛拟時間,那麼其vruntime肯定比目前程序的vruntime要大(證明很簡單),如此一來的結果就是導緻目前程序和新程序的vruntime的交換,新程序的優先運作!cfs中都是承諾預先運作sched_vslice(cfs_rq, se)個虛拟時間的,因為隻有這樣才會導緻不公平,就好像小時候玩的賽跑遊戲,你先跑我才能追你,是以必須一個程序先引起不公平才可以程序cfs排程。在設定了START_DEBIT的情況下,并且使用者設定了子程序優先運作的情況下,即使父程序的目前的動時間片還沒有完,也就是說當期的虛拟時間還沒有推進完一次,那麼還是會經過另一個更高層次的權衡,就是排程器的check_preempt_curr函數,這個函數也是排程類相關的,在cfs中,它的邏輯中含有:

if (wakeup_preempt_entity(se, pse) == 1) {

resched_task(curr);

這個wakeup_preempt_entity很有意思,它就是判定搶占的,即使沒有設定debit特性當有新程序插入的時候,即使新程序的vruntime即使min_vrntime的情況下,父程序的vruntime如果也是min_vruntime的時候,在排程器的check_preempt_curr回調函數中也會有嚴格的檢查來盡量搶占目前的程序,也就是印證了一句話,cfs排程器盡量保證公平,盡自己最大的努力來保證vruntime最小的程序來運作。以上是新程序的vruntime大于父程序的vruntine的情況下,如果新程序的vruntime小于父程序的vruntime的情況下,那麼在check_preempt_curr的時候,搶占是必然的,2.6的核心是搶占的,cfs的核心的更加搶占的。

如果沒有使用者設定的子程序優先運作一說,我可能沒有說清事情的本質,其實子程序優先運作一說和cfs的調服是兩碼事,cfs的排程器的特性在使用者設定子程序優先運作的情況下并且沒有設定debit的情況下确實會引起一些寫時複制,但是真的不是很嚴重,因為這種情況下要麼子程序就是min_vruntime直接搶占,要麼就是子程序的vruntime在目前程序運作一會兒後超過小于目前程序,總之就是将寫時複制的劣勢降低到最低,這就夠了。記住,一切都是權衡!!

我馬上就要送出的patch就是做到完全的公平,任何時候隻要有程序搶占,就又有響應!!!

 本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1274139

繼續閱讀