今天在郵件列表里面有位朋友問了一個問題,問題表述如下:
在喚醒進程的時候,發現在check_preempt_wakeup()中.會將 cfs_rq->next設置為喚醒的進程,cfs_rq->last設置為當前的運行進程.然后將要喚醒的進程重新入列,即 enqueue_task().在pick_next_task_fair()中選擇下一個調度進程的時候,有這樣的選擇 pick_next_task_fair() ---> pick_next_entity():
static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq)
{
struct sched_entity *se = __pick_next_entity(cfs_rq);
if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, se) < 1)
return cfs_rq->next;
if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, se) < 1)
return cfs_rq->last;
return se;
}
其 中__pick_next_entity()用來選擇rb_tree中最左端的se.然后,再調用wakeup_preempt_entity()來判斷 選擇出的se是否可以搶占cfs_rq->next和cfs_rq->last.現在我的疑問是: cfs_rq->next和cfs_rq->last是拿來做什么的呢? 它是為了保證喚醒時的當前進程和被喚醒的進程優先運行嗎?但是,喚醒進程的時候已經調整了它的vruntime,並且調用enqueue_task()入 列,這樣,它在選擇下一個進程的時候,為什么直接按照vruntime值來調度呢?
<-問題描述結束
之所以有這樣的疑問就是因為這位朋友沒有從全局去考慮和理解cfs調度算法,而迷失在了局部的代碼細節,這在讀linux源代碼的時候是一大 忌,linux的設計思想是很好很模塊化很清晰的,但是具體到代碼細節就不是這么美好了,這其實是一個編程習慣問題而不是什么設計問題。解決上述的問題很 容易,其實只要找一下check_preempt_wakeup的調用點就會發現,並不是僅僅在喚醒進程的時候才調用的,比如在更改進程優先級或者創建新 進程或者遷移進程的時候都要調用它。要點就是,如果入隊的時候沒有更新vruntime,那么就有必要將pick_up_next的結果也就是紅黑樹最左 下的結點和新入隊的做一番比較,因為入隊時的情況是不確定的,如果沒有更新入隊進程的vruntime但是其權值已經改變或者綁定的運行處理器已經改變的 話,比如遷移進一個新cpu的運行隊列,那么就不能用它保留的原來的vruntime來競爭cpu了,但是又不想破壞代碼的簡潔而重新每次都在入隊時計算 vruntime,那么只有先保留一個cfsq->next字段用來記錄這個需要仲裁的新進程了,另外調度粒度也是一個很重要的參數,粒度過小的話 在cfs的平滑調度機制下就會發生頻繁調度,系統的大部分時間都用到調度上了,對於調度器來說這樣簡直太精確了,但是對於整個系統來說調度僅僅是一個確保 公平的手段而已,僅是個服務,不能過多的占用處理器,相反調度粒度過大就又會回到時間片調度的那種低效狀態,因此調度粒度是一個很重要的參數。判斷入隊時 是否更新vruntime的是enqueue_task_fair(struct rq *rq, struct task_struct *p, int wakeup)的waleup參數,比如在__migrate_task中就有調用activate_task(rq_dest, p, 0);check_preempt_curr(rq_dest, p);另外喚醒一個新進程的情況下入隊時wakeup參數也可能為0,那么就不更新vruntime,這樣就必須在pick_up_next的時候仲裁 了。
為何計算新入隊的vruntime會破壞代碼的簡潔呢?linux內核由很多人編寫,因此代碼的模塊化顯得很重要,最好是只在一個地方修改一個變量而不是 到處都在修改,那么常規修改vruntime的地方就是update_curr了,當然喚醒睡眠進程或者新進程的時候也要修改,但是那不是常規修改,於是 要想修改調度實體的vruntime就必須使其成為curr,然后在更新curr時期更新其vruntime,於是就只有將未決進程,也就是cfsq的 next,和pick_up_next的結果進行比較,因為也只有pick_up_next的結果有資格參與比較,比較的另一方就是未決進程,它是確定 的,就是cfsq->next。首先看看這個神秘的wakeup_preempt_entity吧,待會兒再看看place_entity--另一 個設置vruntime的地方:
static int wakeup_preempt_entity(struct sched_entity *curr, struct sched_entity *se)
{
s64 gran, vdiff = curr->vruntime - se->vruntime; //vruntime的差值
if (vdiff <= 0) //cfsq的vruntime是單調遞增的,也就是一個基准,各個進程的vruntime追趕競爭cfsq的vruntime,如果curr的 vruntime比較小,說明curr更加需要補償,即se無法搶占curr
return -1;
gran = wakeup_gran(curr); //計算curr的最小搶占期限粒度
if (vdiff > gran) //當差值大於這個最小粒度的時候才搶占,這可以避免頻繁搶占。
return 1;
return 0;
}
static unsigned long wakeup_gran(struct sched_entity *se)
{
unsigned long gran = sysctl_sched_wakeup_granularity; //NICE_0_LOAD的基准最小運行期限
if (!sched_feat(ASYM_GRAN) || se->load.weight > NICE_0_LOAD) //非NICE_0_LOAD的進程要計算其自己的最小運行期限
gran = calc_delta_fair(sysctl_sched_wakeup_granularity, se); //計算進程運行的期限,即搶占的粒度。
return gran;
}
看 完了上面兩個函數后就可以說cfs的設計思想了,這里不再談2.6.23的內核,僅以2.6.25以后的為准,本文討論2.6.28的內核,這些新內核的 cfs算法和最開始的2.6.23的cfs的思想有些小不同。在cfs中,沒有確定時間片的概念,不再像以前那樣根據進程的優先值為進程分配一個確定的時 間片,在這個時間片過期后發生無條件進程切換,而未過期時則可以發生搶占。這個時間片的思想從早期的分時unix繼承而來,已經不再適應現在搶占,特別是 內核搶占無處不在的新世界了,如今的處理器速度大大提高,時鍾大大精確了,另外外設越來越智能,為cpu分擔的工作越來越多,cpu仍然作為計算機的中心 就不能對外設為所欲為了,外設的中斷更加頻繁和有效,但是如果應用這些外設的運行於cpu的進程如果還是延遲響應的話,事情就會顯得有些不和諧。這就要求 調度器必須改進,以前的時鍾不精確,中斷不頻繁,外設少,總線帶寬低,應用不豐富等原因使得內核非搶占是可以忍受的,后來雖然有了內核搶占但是還是和硬件 格格不入,應用程序總是看起來反映遲緩或者不公平,cfs調度器在這種情況下由運而生,cfs的總體思想就是盡量使進程公平的被調度,這種公平不是同等對 待所有進程,而是按照進程權值百分之百履行優先級承諾。cfs算法意味着cpu的調度和硬件行為的步調更加的一致,同時也免去了復雜的行為預測算法,這比 較符合這個世界的規則。按照以前的時間片方式硬件的時間片和操作系統調度的軟件時間片差好幾個數量級,而且軟件已經不能做的更加精確了,因此必須拋棄這種 方式,cfs調度器看上去更像是一部無級變速器,既然跟不上硬件就別用時間片跟,到最后不但還是跟不上,而且還使得時間片調度行為喪失了世界原本的性質, 所以才有了那么多復雜的預測算法。cfs回歸了世界的本質,就是公平的履行承諾。在2.6.25以后cfs中在每個隊列設置了一個字段,就是 vruntime,這個字段在系統運行期間單調增長,各個進程自己也有一個vruntime,它們相互追趕向這個vruntime看齊,並且可以最終將自 己的vruntime設置為隊列的vruntime,處理器總是挑選vruntime小的運行,這其實是一種對掉隊者的補償,這就是公平,每個進程的 vruntime相當於它自己的虛擬時鍾,如果每個進程的虛擬時鍾同步,各個進程就可以說是公平的,相互追趕vruntime並且向cfsq的 vruntime看齊就是保持虛擬時鍾同步。對於不同權值的進程,它們的虛擬時鍾快慢不同,這才是公平的真正含義,比方說權值大的進程的虛擬時鍾10秒走 一個字,而權值小的進程虛擬時鍾1秒就走一個字,虛擬時鍾都走一個字就同步了,但是權值大的進程運行了10秒而小權值的進程才運行1秒,這就是實質。現在 看看place_entity:
static void place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
u64 vruntime = cfs_rq->min_vruntime;
if (initial && sched_feat(START_DEBIT)) //如果是新進程第一次要入隊,那么就要初始化它的vruntime,一般就把cfsq的vruntime給它就可以,但是如果當前運行的所有進程被承諾 了一個運行周期,那么則將新進程的vruntime后推一個他自己的slice,實際上新進程入隊時要重新計算運行隊列的總權值,總權值顯然是增加了,但 是所有進程總的運行時期並不一定隨之增加,則每個進程的承諾時間相當於減小了,就是減慢了進程們的虛擬時鍾步伐。
vruntime += sched_vslice(cfs_rq, se); //sched_vslice計算的結果就是這個新進程
if (!initial) {
...//忽略一種情況
vruntime = max_vruntime(se->vruntime, vruntime); //如果是喚醒已經存在的進程,則單調附值
}
se->vruntime = vruntime;
}
sched_vslice很重要,它其實就是一個有意義的值,我們看一下:
static u64 sched_vslice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
return calc_delta_fair(sched_slice(cfs_rq, se), se);
}
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se) //返回一個理想的運行時間
{
unsigned long nr_running = cfs_rq->nr_running;
if (unlikely(!se->on_rq))
nr_running++;
return calc_delta_weight(__sched_period(nr_running), se); //返回一個進程se的應該運行的時間
}
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; //將delta除以總權值,得到一個值,該值的單位就是vruntime的單位。
}
static u64 __sched_period(unsigned long nr_running) //返回一個值,該值是一個每個進程最少運行一趟的總時間
{
u64 period = sysctl_sched_latency;
unsigned long nr_latency = sched_nr_latency;
if (unlikely(nr_running > nr_latency)) {
period = sysctl_sched_min_granularity;
period *= nr_running;
}
return period;
}
以 上幾個函數很重要,很多cfs中所謂的“值”都是上述函數計算而來的,比如在時鍾中斷的tick節拍函數中,為了測試當前進程是否需要被搶占調用了 check_preempt_tick,該函數進一步調用了sched_slice獲得了一個理想值,該理想值描述了這個當前進程實際上應該運行的時間, 如果這個進程實際運行的時間超過了這個理想值,那么就意味着該搶占了。
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);
delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
if (delta_exec > ideal_runtime)
resched_task(rq_of(cfs_rq)->curr);
}
今天在一個問題的激勵下,我終於寫了一篇描述cfs的文章,呵呵