电竞比分网-中国电竞赛事及体育赛事平台

分享

linux內(nèi)核分析筆記----調(diào)度

 rookie 2012-04-12

       調(diào)度?咋這熟悉,我們是不是常在哪里聽到。沒錯,是的,調(diào)度我們時常聽過,比如交通管制調(diào)度啦等。這不,夏天這熱, 標(biāo)語貼的好:相應(yīng)國電電力調(diào)度,做文明市民,好別扭啊!不管了。你要是還是不懂,再啰嗦講個事,過年回家,和漂亮的GF回家,為了張普通的硬座票還要排老 久對,甚至還可能被坑拿到黃牛票,這時你嘴里咧咧的啥:XX,啥火車站,做的啥春運調(diào)度?。“?,這次你說到點上了。

       總結(jié)一下:調(diào)度就是通過調(diào)度程序的合理調(diào)度,實現(xiàn)系統(tǒng)資源的最大限度發(fā)揮作用。多進(jìn)程的并發(fā)正是這樣的效果。其實原理一點也不復(fù)雜,道理也一樣簡單:只要又可以執(zhí)行的進(jìn)程,那么就總是會有進(jìn)程正在執(zhí)行。但簡單的問題也會復(fù)雜化,比如:我們買票為啥抱怨調(diào)度,歸根接地感謝當(dāng)年的人海戰(zhàn)術(shù)(多說一句,其實現(xiàn)實的很多問題,一個人海戰(zhàn)術(shù)解決所有,這戰(zhàn)術(shù)中國人用起來最得心應(yīng)手)。好么,一般系統(tǒng)中進(jìn)程的數(shù)目總會比處理器的個數(shù)多,所以競爭在所難免,調(diào)度的問題就集中在解決競爭的問題。

       種類問題不多說:搶占和非搶占。linux提供了搶占式的多任務(wù)模式,下一階段誰得到CPU的執(zhí)行時間由調(diào)度程序決定。這怎么決定,是不是請個客,喝個酒 啥的。對不起,linux無情的說,我是開源的,對所有人公平,哥不吃這一套。我有自己的一套原則(策略,這個我們待會兒再講)。接著來術(shù)語,上面的過程 叫做搶占,進(jìn)程得到CPU的執(zhí)行機會這段時間叫時間片,是由系統(tǒng)定義好的。后面我們會看到,linux調(diào)度程序采用動態(tài)方法計算時間片。說了搶占,再說說 非搶占,就是說沒人跟你搶,除非自己放棄,否則你自己運行下去吧。進(jìn)程主動放棄掛起自己的行為叫讓步。這種機制看似很和諧(不要被和諧哦),但問題挺多, 比如系統(tǒng)不知道每個進(jìn)程執(zhí)行多長時間。而且一旦一個懸掛進(jìn)程不愿意或者忘了做出讓步,那系統(tǒng)崩了咋辦,我的money,我的future,我的 lover,從此一去不復(fù)返。

       說了那多,開始正題。原則,一切都有原則,linux的原則。開始原則以前,再來認(rèn)識一下我們前邊說過的進(jìn)程,其實進(jìn)程遠(yuǎn)不如以前那樣簡單。進(jìn)程也有種類 的,至少分為IO型和CPU型。IO型指那種大部分時間用來提交I/O請求或是等待I/O請求的進(jìn)程。這樣的進(jìn)程挺討人厭的,他通常都是運行一小小會兒, 因為它總是在等待更多的I/O請求時最后總會阻塞。與之對應(yīng)的則是CPU型進(jìn)程。這樣的進(jìn)程把時間大多放在執(zhí)行代碼上,除非被搶占,他們就一直貪得無厭的 運行。因為它們沒有太多的IO請求。由于它們不是IO驅(qū)動類型,也就不存在用戶交互的問題(IO驅(qū)動不僅僅指磁盤,還指鍵盤鼠標(biāo)等),所有從系統(tǒng)交互響應(yīng) 速度而言,調(diào)度器不應(yīng)該經(jīng)常讓他們運行。即降低它們的運行頻率,而將它們的運行時間拖長一些。這里所說的原則就是:調(diào)度策略要在進(jìn)程響應(yīng)迅速(相應(yīng)時間 短)和進(jìn)程吞吐量高之間做出艱難的決定。這個原則有時不公平,很殘酷。殘酷的讓人不懂,不懂低優(yōu)先級的有時不能公平對待。由于交互式進(jìn)程屬于IO型進(jìn)程, 所以linux為了保證交互式應(yīng)用,所以調(diào)度策略會向IO型進(jìn)行傾斜。

        上面提到優(yōu)先級,調(diào)度算法中最基本的一類當(dāng)然就是基于優(yōu)先級的調(diào)度。挺簡單:優(yōu)先級高的先運行,相同優(yōu)先級的按輪轉(zhuǎn)式進(jìn)行調(diào)度。優(yōu)先級高的進(jìn)程使用的時間 片也長。調(diào)度程序總是選擇時間片未用盡且優(yōu)先級最高的進(jìn)程運行。這句話就是說用戶和系統(tǒng)可以通過設(shè)置進(jìn)程的優(yōu)先級來響應(yīng)系統(tǒng)的調(diào)度?;诖耍琹inux設(shè) 計上一套動態(tài)優(yōu)先級的調(diào)度方法。一開始,先為進(jìn)程設(shè)置一個基本的優(yōu)先級,然而它允許調(diào)度程序根據(jù)需要來加減優(yōu)先級。linux內(nèi)核提供了兩組獨立的優(yōu)先級 范圍。第一種是nice值,范圍從-20到19,默認(rèn)為0.nice值越大優(yōu)先級越小。另外nice值也用來決定分配給進(jìn)程時間片的長短。第二種范圍是實 時優(yōu)先級。默認(rèn)范圍是從0到99.任何實時的優(yōu)先級都高于普通優(yōu)先級。這個我們后面再說。

       說了那么多的時間片,說起來不費勁,做起來那不是一個麻煩可以說清楚的。時間片就是一個數(shù)值,它表明進(jìn)程在被搶占前所能持續(xù)運行的時間。默認(rèn)的時間片是 20ms,很短。linux調(diào)度程序還能根據(jù)進(jìn)程的優(yōu)先級動態(tài)調(diào)整分配給他的時間片。特別說明的是,進(jìn)程不一定要一次用完所有分配給它的時間片。一旦進(jìn)程 的時間片耗盡后,就認(rèn)為進(jìn)程到期了。沒有時間片的進(jìn)程不會再投入運行。除非等到其他的進(jìn)程都耗盡了它們的時間片,這時,所有進(jìn)程的時間片就會被重新計算。

       前邊講到了搶占的話題,當(dāng)一個進(jìn)程進(jìn)入TASK_RUNNING狀態(tài)時,內(nèi)核就會檢測它的優(yōu)先級是否高于當(dāng)前正在執(zhí)行的進(jìn)程。如果是,則調(diào)度程序會被喚 醒,重新選擇新的進(jìn)程執(zhí)行(套用書上的話, 應(yīng)該是剛剛進(jìn)入可運行狀態(tài)的這個進(jìn)程)。此外,當(dāng)一個進(jìn)程的時間片變?yōu)?時,它會被搶占,調(diào)度程序被喚醒以選擇一個新的進(jìn)程。(具體的例子大家可以看看參 考書籍的p28頁3.1.5)

       linux的調(diào)度程序定義于kernel/sched.c中,在2.6內(nèi)核中的調(diào)度程序和以前有了很大的差別,體現(xiàn)在下面:

1.充分實現(xiàn)O(1)調(diào)度.這個應(yīng)該懂得,可以懂的
2.在多核處理器的今天,linux也不落伍,全面實現(xiàn)SMP的擴展性。每個處理器擁有自己的鎖和自己的可執(zhí)行隊列
3.強化SMP的親和力。這是有關(guān)多核CPU親和力的說明,我目前正在研究這個,后面我會專門在一篇博文中詳細(xì)說明。
4.加強交互能力。
5.保證公平。
6.雖然最常見的優(yōu)化情況是系統(tǒng)中只有1~2個可運行進(jìn)程,但是優(yōu)化也完全有能力擴展到具有多處理器且每個處理器上運行多個進(jìn)程的系統(tǒng)中。

      上面第二條可執(zhí)行隊列,它是調(diào)度程序中最基本的數(shù)據(jù)結(jié)構(gòu),定義于kernel/sched.c中,由結(jié)構(gòu)runquene表示。它是給定處理器上的可執(zhí)行 進(jìn)程的鏈表,每個處理器維護一個。每個可投入運行得進(jìn)程都唯一的歸屬于一個可執(zhí)行隊列。此外,可執(zhí)行隊列中還包含每個處理器的調(diào)度信息。具體的結(jié)構(gòu)這里就 不給了,自己要有看源代碼的能力哦。宏cpu_rq(processor)用于返回給定處理器可執(zhí)行隊列的指針,this_rq()用于來返回當(dāng)前處理器 的可執(zhí)行隊列,task_rq(task)返回給定任務(wù)所在的隊列指針。

       在其擁有者讀取或改寫其隊列成員的時候,可執(zhí)行隊列包含的鎖用來防止隊列被其他代碼改動,由于每個可執(zhí)行隊列唯一地對應(yīng)一個處理器,所以很少出現(xiàn)一個處理 器需要鎖其他處理器的可執(zhí)行隊列的情況,但這種情況是事實存在的。最常見的情況是發(fā)生在一個特定的任務(wù)在運行隊列上執(zhí)行時。此時需要用到 task_rq_lock()和task_rq_unlock()函數(shù),或者可以用this_rq_lock()來鎖住當(dāng)前的可執(zhí)行隊列,用 rq_unlock(struct runqueue *rq)釋放給定隊列上的鎖。關(guān)于鎖的操作,我們再次飄過,為啥?一方面,這超出了本節(jié)的重點,二者我在linux驅(qū)動理論帖中對加解鎖做了詳細(xì)說明,三者,我后面可能還會詳細(xì)說。所以,那句話真是太真理了:暫時的放下,是再次的拿起。

        從運行隊列的結(jié)構(gòu)中,我們發(fā)現(xiàn)了兩個有關(guān)優(yōu)先級的數(shù)組(定義在kernel/sched.c中,名字叫prio_array,具體自己去看吧):活躍的和 過去的。這兩個數(shù)組是實現(xiàn)O(1)級算法的數(shù)據(jù)結(jié)構(gòu)。優(yōu)先級數(shù)組使可運行處理器的每一種優(yōu)先級都包含一個相應(yīng)的隊列,而這些隊列包含對應(yīng)優(yōu)先級上的可執(zhí)行 進(jìn)程鏈表。結(jié)構(gòu)中的優(yōu)先級位圖主要是為了幫助提高查找當(dāng)前系統(tǒng)內(nèi)擁有最高優(yōu)先級的可執(zhí)行進(jìn)程的效率而設(shè)計的。關(guān)于結(jié)構(gòu)中的定義常量,MAX_PRIO定義 了系統(tǒng)擁有的優(yōu)先級個數(shù),默認(rèn)值是140,BITMAP_SIZE是優(yōu)先級位圖數(shù)組的大小,值是5,這個是可以計算出來的。比如:位圖數(shù)組的類型是 unsigned long類型,長是32位,假定每一位代表一個優(yōu)先級(4*32 = 128<140),就需要5個這樣的長整型才能表示。所以,bitmap就正好含有5個數(shù)組項。

        開始時,位圖成員的所有位都被清0,當(dāng)某一優(yōu)先級的進(jìn)程開始準(zhǔn)備執(zhí)行時,位圖中對應(yīng)的位就置1,這樣,查找系統(tǒng)中最高的優(yōu)先級就變成了查找位圖中被設(shè)置的 第一位。鑒于優(yōu)先級個數(shù)的定值性,查找的時間就不受系統(tǒng)到底有多少可執(zhí)行進(jìn)程的影響,是個定值。關(guān)于對位圖的快速查找算法對應(yīng)的函數(shù)是 sched_find_first_bit().

        優(yōu)先級數(shù)組的list_head隊列是一個鏈表,這個鏈表包含了該處理器上相應(yīng)優(yōu)先級的全部可運行線程。要找到下一個要運行的任務(wù),直接從該鏈表中選擇下 一個元素。對于給定的優(yōu)先級,按輪轉(zhuǎn)方式調(diào)度任務(wù)。優(yōu)先級數(shù)組的計數(shù)器nr_active,它保存了該優(yōu)先級數(shù)組內(nèi)可執(zhí)行進(jìn)程的數(shù)目。

         下一話題,我前邊反復(fù)提到時間片的話題,一旦所有進(jìn)程的時間片都用完會怎樣了,總不能系統(tǒng)宕掉吧,好在操作系統(tǒng)提供了一種顯式的方法來重新計算每個進(jìn)程的時間片。典型就是循環(huán)訪問每個進(jìn)程。借用書上的一個例子:

for (系統(tǒng)中的每個任務(wù)){
           重新計算優(yōu)先級
           重新計算時間片

       這種方法簡單,但弊端很多:

1.可能耗費相當(dāng)長的時間。
2.為了保護任務(wù)隊列和每個進(jìn)程的描述符,必要要用到鎖的機制。這樣做很明顯會加劇對鎖的爭用,影響系統(tǒng)性能。
3.重算時間片的時機不確定。
4.實現(xiàn)顯得很粗糙。

       現(xiàn)在采用的是新的調(diào)度方法,每個處理器擁有上面提到的兩個優(yōu)先級數(shù)組,活動數(shù)組上的進(jìn)程都有時間片剩余,而過期數(shù)組中的進(jìn)程都耗盡了時間片,當(dāng)一個進(jìn)程的 時間片耗盡時,它會移至過期數(shù)組,但在此之前,時間片已經(jīng)給它重新計算好了,重新計算時間片現(xiàn)在變的非常簡單,只要在活動和過期數(shù)組之間來回切換就可以 了。又因為數(shù)組是通過指針進(jìn)行訪問的,所以交換它們用的時間就是交換指針需要的時間。這個動作由schedule()完成:

struct prio_array *array = rq->active;
if(!array->nr_active){
        rq->active = rq->expired;
        rq->expired = array;
}

       這種交換是O(1)級調(diào)度程序的核心。O(1)級調(diào)度程序根本不需要從頭到尾都忙著重新計算時間片,它只要完成一個兩個步驟就能實現(xiàn)數(shù)組的切換,這種實現(xiàn) 完美地解決了前面列舉的所有弊端。linux的調(diào)度程序是在schedule()函數(shù)實現(xiàn)的。當(dāng)內(nèi)核代碼想要休眠時,會直接調(diào)用該函數(shù),如果哪個進(jìn)程將被 搶占,也會被喚起執(zhí)行。調(diào)度的第一步是判斷誰是優(yōu)先級最高的進(jìn)程:

struct task_struct *prev, *next;
struct list_head *queue;
struct prio_array array;
int idx;
prev = current;
array = rq->active;
idx = sched_find_first_bit(array->bitmpa);
queue = array->queue + idx;
next = list_entry(queue->next, struct task_struct, run_list);

       分析上面這段代碼,首先在活動優(yōu)先級數(shù)組中找到第一個被設(shè)置的bit位,該位對應(yīng)著最高的可執(zhí)行進(jìn)程。然后,調(diào)度程序選擇這個級別鏈表里的頭一個進(jìn)程。這 個進(jìn)程就是系統(tǒng)中優(yōu)先級最高的可執(zhí)行程序,也是馬上就會被調(diào)度執(zhí)行的進(jìn)程。上面中的prev和next不等,說明被選中的進(jìn)程不是當(dāng)前進(jìn)程,這時就要調(diào)用 context_switch來進(jìn)程切換。最后強調(diào)一點,不存在任何影響schedule()時間長短的因素,因為前邊說過,所用的時間是恒定的。

       前邊多次說過:調(diào)度程序是利用優(yōu)先級和時間片來做出決定的。下邊看看實際代碼的實現(xiàn)方式。進(jìn)程結(jié)構(gòu)體告訴我們進(jìn)程擁有一個初始的優(yōu)先級,叫做nice值, 最低是19,最高是20,結(jié)構(gòu)體的static_prio域就存放這個值,static就是說這個一開始就由用戶指定好了,不能改變。調(diào)度程序要用的動態(tài) 優(yōu)先級用prio域表示,兩者的關(guān)系在于通過一個關(guān)于靜態(tài)優(yōu)先級和進(jìn)程交互性的函數(shù)計算而來。啥叫進(jìn)程交互性?這是個新詞,第一次提到,這樣說吧,我們前 邊說過I/O消耗性和CPU消耗性的進(jìn)程區(qū)別,進(jìn)程交互性就是指IO消耗性的,因為和用戶進(jìn)行交互就是電腦IO和用戶交互,像鼠標(biāo)鍵盤等。但我們怎么確定 一個進(jìn)程的交互性的強弱呢?最明顯的標(biāo)準(zhǔn)莫過于通過計算進(jìn)程休眠的時間長短來做預(yù)測了。大部分時間都在休眠的進(jìn)程當(dāng)然是IO消耗型的,如果一個進(jìn)程把所有 的時間幾乎都放在了CPU執(zhí)行處理上,那..不說了。在linux中,用值tast_struct中的sleep_avg域記錄了一個進(jìn)程用于休眠和用于 執(zhí)行的時間,范圍為0~MAX_SLEEP_AVG,默認(rèn)值是10ms。當(dāng)一個進(jìn)程從休眠狀態(tài)恢復(fù)到執(zhí)行狀態(tài)時,sleep_avg會根據(jù)它休眠時間的長 短而增長直到最大。相反,進(jìn)程沒運行一個時鐘節(jié)拍,sleep_avg就做相應(yīng)的遞減,到0為止。這種方法不僅會獎勵交互性強的進(jìn)程,還會懲罰處理器耗時 量的進(jìn)程。再加之獎勵和處罰都是建立在作為基數(shù)的nice值上,所有用戶仍然能夠通過改變nice指來調(diào)整程序的調(diào)度。effective_prio() 函數(shù)可以返回一個進(jìn)程的動態(tài)優(yōu)先數(shù),該函數(shù)以nice值為基數(shù),再加上我們上面提到的從-5到+5的進(jìn)程交互性型獎勵處罰分。

       一旦有了上面的動態(tài)優(yōu)先級,再來重新計算時間片就方便多了。另外,當(dāng)一個進(jìn)程創(chuàng)建的時候,新建的子進(jìn)程和父進(jìn)程均分父進(jìn)程剩余的時間片。這樣的分配很平均 并且防止用戶通過不斷的創(chuàng)建新進(jìn)程來不斷攫取時間片。task_timeslice()函數(shù)為給定任務(wù)返回一個新的時間片,時間片的計算只需要把優(yōu)先級按 比例縮放,使其符合時間片的數(shù)值范圍要求就可以了。優(yōu)先級最高的進(jìn)程能獲得的最大時間片長度(MAX_TIMESLICE)是200ms,最短時間片 (MIN_TIMESLICE)是10ms。默認(rèn)優(yōu)先級(nice值為0,沒有交互性獎罰分)的進(jìn)程得到的時間片長度為100ms。上面我們也提到過,活 動數(shù)組內(nèi)的可執(zhí)行隊列上的進(jìn)程的時間片耗盡時,它就會移植到過期數(shù)組。但是,如果一個進(jìn)程的交互性很強,特別特別強,那么調(diào)度程序支持另外一種機制來支持 這種交互進(jìn)程:當(dāng)時間片用完后,它會被再放置到活動數(shù)組而不是過期數(shù)組中?;貞浺幌律厦鍻(1)調(diào)度器的實現(xiàn)機制:進(jìn)程用盡其時間片后,都會被移動到過期 數(shù)組,當(dāng)活動數(shù)組中沒有剩余進(jìn)程的時候,這個時候兩個數(shù)組就會被交換。但是在發(fā)生這種交換以前,交互性很強的一個進(jìn)程有可能已經(jīng)處于過期數(shù)組中了,當(dāng)他需 要交互的時候,這時可能數(shù)組交互還沒有發(fā)生,所以交互也就無法進(jìn)行,這時對于這種交互特別特別強的進(jìn)程重新插入到活動數(shù)組就可以避免這種問題。注意,雖然 被放回到活動數(shù)組,但不會立即就運行,它還要和其他具有相同優(yōu)先級的進(jìn)程輪流運行。該邏輯在scheduler_tick()中實現(xiàn),該函數(shù)會被定時器中 斷調(diào)用。看下面的實現(xiàn)代碼:

struct  task_struct *task = current;
struct runqueue *rq = this_rq();
if(!—task->time_slice){
       if(!TASK_INTERACTIVE(task) || (EXPIRED_STARVING(rq)))
                 enqueue_task(task, rq->expired);
       else
                 enqueue_task(task, rq->active);
}

       這段代碼先減少進(jìn)程時間片的值。宏TASK_INTERACTIVE主要是基于進(jìn)程的nice值來查看這個進(jìn)程是否是交互性很強的進(jìn)程。宏 EXPIRED_STARVING負(fù)責(zé)檢查過期數(shù)組內(nèi)的進(jìn)程是否處于饑餓狀態(tài),是否有相當(dāng)長的時間沒有發(fā)生數(shù)組交換了。如果最近一直沒有發(fā)生切換,那么再 把當(dāng)前的進(jìn)程放置到活動數(shù)組會進(jìn)一步拖延切換--過期數(shù)組內(nèi)的進(jìn)程會來越來越餓.只要不發(fā)生這種情況,進(jìn)程就會被重新設(shè)置在活動數(shù)組里,否則,進(jìn)程會被放 入過期數(shù)組里。

      有關(guān)休眠我就不用多講了。內(nèi)核對休眠的處理都相同:進(jìn)程把自己標(biāo)記成休眠狀態(tài),然后將自己從可執(zhí)行隊列移出,放入等待隊列,然后調(diào)用schedule() 選擇和執(zhí)行一個其他進(jìn)程,喚醒的過程剛好相反。要明白一點就是:即使休眠也有兩種進(jìn)程狀態(tài),TASK_INTERRUPTIBLE和 TASK_UNINTERRUPTIBLE.前者如果接收到一個信號會被提前喚醒并響應(yīng)該信號,后者會忽略信號。這兩種進(jìn)程位于同一等待隊列 (wake_queue_head_t)上,等待某些事情,不能夠運行。有關(guān)等待隊列的知識,跳過(我已經(jīng)在Linux驅(qū)動開發(fā)理論帖里講過,自己去看吧)現(xiàn)在,在內(nèi)核中進(jìn)行休眠的推薦操作相比較以前而言復(fù)雜了很多,但更安全:

1.調(diào)用DECLARE_WAITQUEUE() 創(chuàng)建一個等待隊列的項。
2.調(diào)用add_wait_queue()把自己加入到等帶對列中。
3.將進(jìn)程的狀態(tài)變更為TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE.
4.檢查條件是否為真,如果是的話,就沒必要休眠了。不為真,就調(diào)用schedule().
5.當(dāng)進(jìn)程被喚醒的時候,它會再次檢查條件是否為真。如果是,它就退出循環(huán),如果不是,它再次調(diào)用schedule()并一直重復(fù)這步操作。
6.當(dāng)條件滿足后,進(jìn)程將自己設(shè)置為TASK_RUNNING并調(diào)用remove_wait_queue把自己移出等待隊列。

       如果在進(jìn)程開始休眠之前條件就已經(jīng)達(dá)成了,那么循環(huán)會退出,進(jìn)程不會存在錯誤的進(jìn)入休眠的傾向。有休眠就有喚醒,喚醒是通過wake_up()進(jìn)行。她喚 醒指定的等待隊列上的所有進(jìn)程。它調(diào)用try_to_wake_up,該函數(shù)負(fù)責(zé)將進(jìn)程設(shè)置為TASK_RUNNING狀態(tài),調(diào)用 active_task()將此進(jìn)程放入可執(zhí)行隊列,如果被喚醒進(jìn)程的優(yōu)先級比當(dāng)前正在執(zhí)行的進(jìn)程的優(yōu)先級高,還要設(shè)置need_resched標(biāo)志。編 程的技巧,通常哪段代碼促使等待條件達(dá)成,它就要負(fù)責(zé)隨后調(diào)用wake_up函數(shù)。說明:存在虛假的喚醒,有時候進(jìn)程被喚醒并不是因為它所等待的條件達(dá)成 了,所以才需要用一個循環(huán)處理來保證它等待的條件真正到達(dá)。

       開始新的問題,現(xiàn)在市場上,你說單核CPU,你都沒臉見人,現(xiàn)在是啥時代,多核的時代,瞧,我這都酷睿i7了(四核),我用的服務(wù)器實驗平臺是8核心的。 和我有啥關(guān)系,和我今天講的有啥關(guān)系。關(guān)系大了去了。前邊提到過,linux的調(diào)度程序為對稱多處理器系統(tǒng)的每個處理器準(zhǔn)備了單獨的可執(zhí)行隊列和鎖,出于 效率的考慮,整個調(diào)度系統(tǒng)從每個處理器來看都是獨立的。那么這樣的情況咋辦呢?一個處理器上有5個進(jìn)程,另外一個處理器的隊列上只有一個進(jìn)程,這時很明顯 出現(xiàn)了系統(tǒng)負(fù)載不均衡的情況。這時咋辦呢?由負(fù)載均衡程序來解決(kernel/sched.c的load_balance來實現(xiàn))。它有兩種調(diào)用方法, 在schedule執(zhí)行的時候,只要當(dāng)前的可執(zhí)行對列為空,它就會被調(diào)用。此外,它也會被定時器調(diào)用,系統(tǒng)空閑時每隔1ms調(diào)用一次或者在其他情況下每隔 200ms調(diào)用一次。單系統(tǒng)中,它不會被調(diào)用,甚至不會被編譯進(jìn)內(nèi)核,因為那里邊只有一個可執(zhí)行隊列,根本沒有平衡負(fù)載的必要。 load_balances調(diào)用時需要鎖住當(dāng)前處理器的可執(zhí)行隊列并且屏蔽中斷,以避免可執(zhí)行隊列被并發(fā)的訪問,所完成的操作步驟如下所示:

1.load_balance()調(diào)用find_busiest_queue(),找到最繁忙的可執(zhí)行隊列(進(jìn)程數(shù)目最多)并返回該隊列。如果沒有哪個可執(zhí)行隊列中進(jìn)程的數(shù)
  目比當(dāng)前隊列中的數(shù)目多25%或25%以上。它就返回NULL,同時load_balance也返回。
2.load_balance從最繁忙的運行隊列中選擇一個優(yōu)先級數(shù)組以便抽取進(jìn)程。最好是過期數(shù)組,如果過期數(shù)組為空,那就只能選活動數(shù)組。
3.load_balance尋找到含有進(jìn)程并且優(yōu)先級最高的鏈表,因為把優(yōu)先級高的進(jìn)程分散開來才是最重要的。
4.分析找到的所有這些優(yōu)先級相同的進(jìn)程,這樣一個不是正在執(zhí)行,也不會因為處理器相關(guān)性而不可移動,并且不在高速緩存中的進(jìn)程。如
  果有,就調(diào)用pull_task將其從最繁忙的隊列中抽取到當(dāng)前隊列
5.只要可執(zhí)行隊列之間仍然不平衡,就重復(fù)上面兩個步驟,這最終會消除不平衡。然后解除對當(dāng)前運行隊列進(jìn)行的鎖定,從load_balance返
  回。

我們后面會提到的上下文切換,就是從一個可執(zhí)行進(jìn)程切換到另一個可執(zhí)行進(jìn)程,由定義在kernel/sched.c的context_switch函數(shù)負(fù)責(zé)處理。每當(dāng)一個新的進(jìn)程被選出來準(zhǔn)備投入運行的時候,schedule就會調(diào)用該函數(shù)。它主要完成如下兩個工作:

1.調(diào)用定義在include/asm/mmu_context.h中的switch_mm().該函數(shù)負(fù)責(zé)把虛擬內(nèi)存從上一個進(jìn)程映射切換到新進(jìn)程中。
2.調(diào)用定義在include/asm/system.h的switch_to(),該函數(shù)負(fù)責(zé)從上一個進(jìn)程的處理器狀態(tài)切換到新進(jìn)程的處理器狀態(tài),這包括保存,恢復(fù)
   棧信息和寄存器信息。

      內(nèi)核也必須知道什么時候調(diào)用schedule(),單靠用戶代碼顯示調(diào)用schedule(),他們可能就會永遠(yuǎn)地執(zhí)行下去,相反,內(nèi)核提供了一個 need_resched標(biāo)志來表明是否需要重新執(zhí)行一次調(diào)度。當(dāng)某個進(jìn)程耗盡它的時間片時,scheduler_tick()就會設(shè)置這個標(biāo)志,當(dāng)一個 優(yōu)先級高的進(jìn)程進(jìn)入可執(zhí)行狀態(tài)的時候,try_to_wake_up()也會設(shè)置這個標(biāo)志。下表給出了用于訪問和操作need_resched的函數(shù)。

函數(shù)

說明

set_tsk_need_resched(task) 設(shè)置指定進(jìn)程中的need_resched標(biāo)志
clear_tsk_need_resched(task) 清除指定進(jìn)程中的nedd_resched標(biāo)志
need_resched() 檢查need_resched標(biāo)志的值,如果被設(shè)置就返回真,否則返回假

       再返回用戶空間以及從中斷返回的時候,內(nèi)核也會檢查need_resched標(biāo)志,如果已被設(shè)置,內(nèi)核會在繼續(xù)執(zhí)行之前調(diào)用該調(diào)度程序。最后,每個進(jìn)程都 包含一個need_resched標(biāo)志,這是因為訪問進(jìn)程描述符內(nèi)的數(shù)值要比訪問一個全局變量要快(因為current宏速度很快并且描述符通常都在高速 緩存中)。在2.6內(nèi)核中,他被移到了thread_info結(jié)構(gòu)體里,這是和以前不同的。

       搶占,還是搶占!我們總是逃不了搶占的話題。內(nèi)核放回到用戶空間的時候,如果need_resched標(biāo)志被設(shè)置,就會導(dǎo)致schedule()被調(diào)用。簡兒言之,用戶搶占在以下情況下發(fā)生:從系統(tǒng)調(diào)用返回到用戶空間和從中斷處理程序返回用戶空間。

       在linux2.6內(nèi)核中,內(nèi)核引入了搶占內(nèi)力。為了支持內(nèi)核搶占所做的第一個變動就是為每個進(jìn)程的thread_info引入 preempt_count計數(shù)器,該計數(shù)器初始為0,每當(dāng)使用鎖的時候,該值減1。當(dāng)為0的時候,內(nèi)核就可執(zhí)行搶占。從中斷返回內(nèi)核空間的時候,內(nèi)核會 檢查need_resched和preempt_count的值,如果need_resched被設(shè)置,并且preempt_count為0的時候,這說 明有一個更重要的任務(wù)需要執(zhí)行并且安全地?fù)屨迹藭r,調(diào)度程序就會被調(diào)度。如果,preempt_count不為0,說明當(dāng)前任務(wù)持有鎖,這時搶占是不安 全的。這時,就會想通常那樣直接從中斷返回當(dāng)前執(zhí)行進(jìn)程。如果當(dāng)前進(jìn)程持有的鎖都被釋放了,那么preempt_count就會重新為0。此時,釋放鎖的 代碼會檢查need_sheduled是否被設(shè)置,如果是的話,就會調(diào)用調(diào)度程序,有些內(nèi)核需要允許或禁止內(nèi)核搶占,這個后面具體問題具體分析。如果內(nèi)核 中的進(jìn)程阻塞了,或它顯示地調(diào)用了schedule(),內(nèi)核搶占也會顯示地發(fā)生,這種形式的內(nèi)核搶占從來都是支持的,因為根本無需額外的邏輯來保證內(nèi)核 可以安全地被搶占,如果代碼顯示的調(diào)用了schedule(),那么它清楚自己是可以安全地被強化的。

       我們前邊講的是普通的,非實時的調(diào)度策略(SCHED_OTHER),Linux也支持兩種實時調(diào)度策略(SCHED_FIFO和 SCHED_RR).SCHED_FIFO從名字中我們也知道,就不說了。它不使用時間片。該級的進(jìn)程會比任何SCHED_OTHER級的進(jìn)程都先得到調(diào) 度。一旦一個SCHED_FIFO級進(jìn)程處理可執(zhí)行狀態(tài),就會一直執(zhí)行,直到它自己被阻塞或是顯示地釋放處理器為止。由于不基于時間片,所以它會一直執(zhí)行 下去。多個SCHED_FIFO級進(jìn)程存在時,他們會輪流執(zhí)行。而且只要有SCHED_FIFO級的進(jìn)程存在,普通級進(jìn)程級只能等待它結(jié)束后才有機會執(zhí) 行。SCHED_RR是一種帶有時間片的SCHED_FIFO。以上兩種實時調(diào)度算法實現(xiàn)都是靜態(tài)優(yōu)先級的。內(nèi)核不為實時進(jìn)程計算動態(tài)優(yōu)先級。這樣就能保 證給定優(yōu)先級級別的實時進(jìn)程總能搶占優(yōu)先級比它低的進(jìn)程。

       linux的實時調(diào)度算法分為軟實時和硬實時。軟實時的含義是,內(nèi)核調(diào)度進(jìn)程盡可能使進(jìn)程在它的限定時間到來前執(zhí)行,但內(nèi)核不會拍著胸脯說:我一定能保 證。對應(yīng)地,硬實時會這樣哥們義氣哦。linux下的實時調(diào)度不做任何保證,只保證只要實時進(jìn)程可執(zhí)行,就盡量去執(zhí)行它?,F(xiàn)在開來,效果還是不錯的。

       實時優(yōu)先級范圍從0~MAX_RT_PRIO-1.默認(rèn)情況下,MAX_RT_PRIO為100.SCHED_OTHER級進(jìn)程的nice值共享了這個取 值空間。它的取值范圍是從MAX_RT_PRIO到MAX_RT_PRIO+40.也就是說,nice值從-20到+19對應(yīng)的是從100到140的實時 優(yōu)先級范圍。

       最后,是一些關(guān)于和調(diào)度有關(guān)的系統(tǒng)調(diào)用,這個我就不說了,感覺就是函數(shù)調(diào)用,說了沒啥意思,自己看吧。反正就是一書在手,萬事不愁啊..呵呵

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多