CFS排程演算法的思想和細節

2021-08-25 01:25:04 字數 4097 閱讀 6606

今天在郵件列表裡面有位朋友問了乙個問題,問題表述如下:

在喚醒程序的時候,發現在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)

其 中__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)

static unsigned long wakeup_gran(struct sched_entity *se)

看 完了上面兩個函式後就可以說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)

se->vruntime = vruntime;

} sched_vslice很重要,它其實就是乙個有意義的值,我們看一下:

static u64 sched_vslice(struct cfs_rq *cfs_rq, struct sched_entity *se)

static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se) //返回乙個理想的執行時間

static inline unsigned long calc_delta_fair(unsigned long delta, struct sched_entity *se)

static u64 __sched_period(unsigned long 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)

今天在乙個問題的激勵下,我終於寫了一篇描述cfs的文章,呵呵

CFS排程演算法的思想和細節

今天在郵件列表裡面有位朋友問了乙個問題,問題表述如下 在喚醒程序的時候,發現在check preempt wakeup 中.會將 cfs rq next設定為喚醒的程序,cfs rq last設定為當前的執行程序.然後將要喚醒的程序重新入列,即 enqueue task 在pick next tas...

CFS排程器的思想的新理解

本文通過詳細分析老的排程器來說明cfs的優勢。總是新理解新理解的,我怎麼這麼沒完沒了啊,其實每隔一段時間我都會在工作之餘再讀一讀linux核心源 的關鍵部分,每次讀都有新的理解,然後就第一時間將心得記錄下來,今天又讀了cfs排程器,越來越發現其美妙了。這次配合了sched nice design.t...

CFS排程器的思想的新理解

本文通過詳細分析老的排程器來說明cfs的優勢。總是新理解新理解的,我怎麼這麼沒完沒了啊,其實每隔一段時間我都會在工作之餘再讀一讀linux核心源 的關鍵部分,每次讀都有新的理解,然後就第一時間將心得記錄下來,今天又讀了cfs排程器,越來越發現其美妙了。這次配合了sched nice design.t...