喚醒實時進程時對目標cpu的選擇策略與存在的問題

以下三篇文章闡述了喚醒實時進程時對目標cpu的選擇策略與可能存在的問題。

1,對待實時進程(RT)搶佔的問題

一個進程被喚醒,在linux中是調用try_to_wake_up函數,對於RT進程也不例外,對於一般進程而言,如果在一個cpu運行隊列上被喚醒的進程的優先級大於該cpu的當前進程,那麼就會發生搶佔,而如果兩個進程都是RT進程則不會發生搶佔,理由是cache的保持,如果發生搶佔的話,被搶佔的RT進程將丟失其所有的cache,可是這樣做合理嗎?
     先看一下RT進程的特徵,這種進程一旦運行,除非自願放棄cpu,一般是不會停止運行的,對於高優先級的RT進程總是優先運行,如果對於RT進程爲了保持cache不搶佔的話,除了cache的保持之外,其實是得不到任何優勢的,並且,在這種情況下還會造成RT進程的乒乓效應,也就是造成RT進程頻繁遷移,在很多RT進程爭搶一個資源,比如lock的情況下,這種乒乓效應帶來的弊端更加明顯,低優先級的RT進程釋放了lock,喚醒了高優先級的RT進程,由於此時當前cpu上的進程仍然是低優先級的進程,所有被喚醒的高優先級進程不得不被遷移到別的cpu上執行,如果低優先級的進程沒有釋放資源,而由於另一個原因睡眠了,此時它的優先級可能已被提升,當它再度被另一RT進程喚醒的時候,即使它擁有高優先級也不得不遷移到另外的cpu上,這些遷移動作肯定會影響性能的。可以說,RT進程調度的無條件不搶佔特性大大削弱了高優先級RT進程的優勢,高優先級RT進程除了在運行隊列的位置靠前之外,沒有多少優勢可言了。
     確實,如果是由於非爭搶資源被釋放原因的喚醒,那搶佔正在運行的rt進程確實對於cache優化是一種抵消,然而要知道即使這樣我們也只是損失了相對低優先級rt進程的性能,我們得到的是避免了一次高優先級rt進程的遷移。另一方面,當我們面對多個rt進程爭搶資源這種類型的睡眠/喚醒時,搶佔的優勢就明顯了,有時一個又有很高優先級的rt進程在運行中由於得不到資源而睡眠,這種睡眠時間一般不會太久,只是一小會兒,它爭搶的資源此時正在被一個低優先級的rt進程佔據,此時它提升低優先級進程的優先級,以使它不被搶佔地儘快完成資源的釋放,這一切都是爲了高優先級進程不會睡得太久,於是資源儘可能早的被釋放了,高優先級進程被喚醒,此時應該知道,將此高優先級rt進程喚醒在它本來運行的cpu上是一個最佳的選擇。
     這就是一個權衡的問題了,是保留cache還是最小化遷移,還是交給調度器吧,嚴格按照優先級調度會好一些,起碼能照顧高優先級的RT進程,使得高優先級的RT進程的優勢更加明顯。於是,新的2.6.37內核對此進行了改進,補丁很簡單:
--- a/kernel/sched_rt.c
+++ b/kernel/sched_rt.c
@@ -960,18 +960,18 @@ select_task_rq_rt(struct rq *rq, struct task_struct *p, int sd_flag, int flags)
        if (unlikely(rt_task(rq->curr)) &&
+           rq->curr->prio < p->prio &&
            (p->rt.nr_cpus_allowed > 1)) {
                int cpu = find_lowest_rq(p);
 
@@ -1491,6 +1491,8 @@ static void task_woken_rt(struct rq *rq, struct task_struct *p)
        if (!task_running(rq, p) &&
            !test_tsk_need_resched(rq->curr) &&
            has_pushable_tasks(rq) &&
+           rt_task(rq->curr) &&
+           rq->curr->prio < p->prio &&
            p->rt.nr_cpus_allowed > 1)
                push_rt_tasks(rq);
}

2,實時進程cpu的選擇

實時(real-time)進程cmcld在CPU2的運行隊列(run queue)中靜坐了14.6秒,一直未能得到運行機會,當時CPU2上正在運行的進程JobWrk6653處於內核態,由於SLES內核是非搶佔式的,所以cmcld無法搶佔CPU2。但是,這臺機器有288個CPU,只有CPU2在忙,其餘287個CPU都是空閒狀態,爲什麼cmcld進程一味死等CPU2、不能到其他CPU上去運行呢?

考慮這個問題要抓住兩個點:

1,實時進程在喚醒時怎麼進了CPU2的運行隊列,而沒有選擇其他CPU?

2,在運行隊列中的實時進程怎麼未能被migrate到其他CPU上?

第2點留待下次討論,今天先研究第1點。

實時進程被喚醒時會選擇哪個CPU呢?這是由select_task_rq_rt()決定的。

調用路徑是try_to_wake_up -> select_task_rq -> select_task_rq_rt

從以下的select_task_rq_rt 源程序可以看到它是如何挑選CPU的:它先找到被喚醒進程上次運行的CPU,如果此CPU上正在運行的進程比被喚醒的進程優先級更低,那麼被喚醒的進程就還留在此CPU上、不會尋找其它CPU--無論其它CPU空閒與否;只有兩種情況下會尋找別的CPU給被喚醒的進程用:一是此CPU上正在運行的進程是實時類型並且該進程綁定在此CPU上,二是此CPU上正在運行的進程的優先級不比被喚醒的進程低。

這個算法背後的邏輯是:兩個進程爭奪CPU時,優先級更高的進程總想擠走優先級更低的,哪怕對方是先來的,哪怕其他CPU都是空閒的,不管先來後到,也不管有沒有其他空閒CPU可用。這真是非常霸道呀,假想對話如下:

“你放開那個CPU,那是我用慣了的,”

“可是我先來的呀。”

“我的優先級比你高,”

“旁邊別的CPU都閒着呢,你用那些不行嗎?”

“要走你走。”

但是,優先級更低的進程並不是想擠走就能擠走的,有時候會當釘子戶,我們這個案例就是碰到了擠不走的釘子戶。

那麼,剛被喚醒的優先級更高的進程作爲後來者,打算如何擠走先來的優先級更低的進程呢?是通過一個稱爲wakeup preemption的過程。我們接着看源程序。

try_to_wake_up通過select_task_rq()給被喚醒的進程選擇CPU之後,再調用ttwu_queue()把被喚醒的進程插入到目標CPU的運行隊列中;

ttwu_queue的操作有點眼花繚亂,基本思路是先判斷執行喚醒任務的CPU與目標CPU是否共享L3 cache,如果共享的話就直接把被喚醒的進程插入運行隊列並進行後續操作,如果不共享的話就採用IPI中斷的方式通知目標CPU自己把進程插入運行隊列。在此不細述,有興趣的自己閱讀源碼。

被喚醒的進程進入目標CPU的運行隊列以後,check_preempt_curr()函數會檢查被喚醒的進程是否有權搶佔當前進程,如果有權搶佔的話就調用resched_task()觸發wakeup preemption。

resched_task()只是給當前進程設置一個 TIF_NEED_RESCHED 標誌,觸發搶佔(preemption),並不實際切換進程。

 

在resched_task()觸發preemption(搶佔)之後,什麼時候切換進程呢?這裏分兩種情況:

1,用戶態搶佔(user preemption)的時機

  • 在系統調用和中斷返回用戶態的時候,會檢查當前進程的 TIF_NEED_RESCHED 標誌,發現置位則切換進程;

  • 運行中的進程主動調用schedule切換進程。

2,內核態搶佔(kernel preemption)的時機

  • 在我們這個案例中,SLES內核關閉了內核搶佔,所以根本就不會發生內核態搶佔。但如果允許內核搶佔的情況下,切換進程的時機是:

  • 中斷結束並返回到內核空間之前;

  • 重新打開內核搶佔的時候,preempt_enable會調用preempt_schedule檢查 TIF_NEED_RESCHED標誌,發現置位的話就切換進程。

回到我們的案例。cmcld以前是在CPU2上運行的,被喚醒的時候select_task_rq_rt()發現CPU2上雖然有一個進程JobWork6653正在運行,但它的優先級比cmcld低,所以仍然把cmcld放進了CPU2的運行隊列,並觸發了wakeup preemption:我們從vmcore中可以看到,'JobWrk6653'進程的TIF_NEED_RESCHED標誌位已經被設置了,證明preemption已經被觸發了:

雖然preemption已經被觸發,但是始終未能完成進程切換,因爲在SLES關閉了內核搶佔的情況下,只能等進程回到用戶態之後才能進行搶佔,這一等就是14.6秒,"JobWrk6653"在內核態一直沒有出來,它就這樣當了一個釘子戶...

這個案例表明,關閉內核搶佔的情況下select_task_rq_rt()的霸道算法無法保證實時進程及時獲得CPU。

3,可能存在的問題導致實時進程不實時

前文說到實時(real-time)進程cmcld在CPU2的運行隊列(run queue)中靜坐了14.6秒,雖然還有287個CPU處於空閒狀態,cmcld卻一直未能得到運行機會。我們已經解釋了爲什麼cmcld一開始會進入CPU2的運行隊列而未選擇其他空閒狀態的CPU,今天我們繼續分析爲什麼cmcld未能被migrate到其他CPU上。

進程從一個CPU換到另一個CPU有個術語叫做migration。已經進入運行隊列的進程發生migration只能通過負載均衡(load balance)觸發。

在多CPU的系統上,使各個CPU之間的負載保持均衡是進程調度器的工作。在我們這個案例中,大量CPU空閒的情況下還有個進程在運行隊列中等了14.6秒之久,顯然進程調度器的負載均衡算法失靈了。

Linux的進程調度器是以模塊化的方式提供的,允許多種調度算法並存,調度模塊稱爲調度類(scheduling class),最常用的是CFS class和real-time class。而CFS類和real-time類的負載均衡算法是不一樣的。

CFS的負載均衡發生在以下時刻:

  • 週期性的負載均衡(Active Balancing),通過時鐘中斷,scheduler_tick()會調用trigger_load_balance()觸發SCHED_SOFTIRQ進行負載均衡操作;

  • 空閒時的負載均衡(Idle Balancing),當CPU進入idle狀態的時候,會調用idle_balance(),試圖從其他CPU的運行隊列裏pull(拉取)進程。

Real-time的負載均衡算法基於運行隊列是否overload,所謂overload就是運行隊列中的real-time進程數量超過了1個。發生負載均衡的時刻如下,請注意它並不考慮進程在隊列中等待了多長時間,也沒有像CFS的負載均衡那樣的週期性操作:

  • 當進程進入real-time隊列的時候,會檢查隊列是否overload,如果overload則調用push_rt_task試圖從該隊列中把進程push(推)到更空閒的CPU隊列;

  • 當運行隊列中的最高進程優先級降低的時候,會檢查其他隊列是否overload,如果有overload就調用pull_rt_task試圖從其他隊列中把優先級更高的進程pull(拉)過來。

回到我們的案例,cmcld是實時進程,歸real-time調度器管,由於CPU2的運行隊列裏只有這一個實時進程,根據real-time的負載均衡算法,不滿足overload的條件,所以既不會發生push也不會發生pull,結果cmcld就這麼一直在CPU2的運行隊列裏待着...

設想如果cmcld是CFS進程,它還會在CPU2的運行隊列中等那麼長時間嗎?並不會,因爲CFS的負載均衡算法會發現CPU2上有兩個進程,會把一個進程migrate到其他的空閒的CPU上去。

這個案例暴露了real-time調度器的不足之處:real-time調度器在選擇CPU和進行負載均衡的時候,眼裏只有real-time進程,沒有考慮CFS等其他類型的進程的影響,比如它認爲隊列裏只有一個real-time進程就不算overload、無論有沒有CFS進程,這通常不會有問題,因爲CFS優先級低於real-time,對real-time進程造不成影響,然而它沒有考慮到特殊情況:在非搶佔式內核裏,CFS進程可能會因爲陷在內核態而造成real-time進程無法搶佔。所以說,也許real-time調度器還可以做得更好一點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章