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 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 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就是做到完全的公平,任何時候只要有進程搶佔,就又有響應!!!