1. 程式人生 > >Linux程序排程原理

Linux程序排程原理

本文系以下文章整理而來,並添加了自己的一些見解:

(1)http://www.cnblogs.com/zhaoyl/archive/2012/09/04/2671156.html

(2)http://os.51cto.com/art/201003/187407.htm

(3)....(由於參考文章較多,恕不能一一列舉,如有作者看到,請聯絡我更正)

Linux程序的四大要素

1:一段供程序執行的程式,該程式可以被多個程序執行。
2:獨立的核心堆疊。
3:程序控制快(task_struct:有了這個資料結構,程序才能成為核心排程的一個基本單位接受核心的排程。同時,這個結構還記錄著程序所佔用的各項資源。
4:獨立的儲存空間:即擁有專有的使用者空間,除了前面的核心空間還有使用者空間。
執行緒:只有前三條,沒有第四條。
核心執行緒:完全沒有使用者空間。
使用者執行緒:共享使用者空間。

Linux程序分類:

1:互動式程序:這些程序經常和使用者發生互動,所以花費一些時間等待使用者的操作。當有輸入時,程序必須很快的啟用。通常,要求延遲在50-150毫秒。典型的互動式程序有:控制檯命令,文字編輯器,圖形應用程式。
2:批處理程序(Batch Process):不需要使用者互動,一般在後臺執行。所以不需要非常快的反應,他們經常被排程期限制。典型的批處理程序:編譯器,資料庫搜尋引擎和科學計算。
3:實時程序:對排程有非常嚴格的要求,這種型別的程序不能被低優先順序程序阻塞,並且在很短的時間內做出反應。典型的實時程序:音視訊應用程式,機器人控制等。
批處理程序可能與I/O或者CPU有關,但是實時程序完全通過Linux的排程演算法識別。
其實互動式程序和批處理程序很難區別。

Linux程序排程的目標

    排程的英文是schedule. 什麼是schedule? 它的英文解釋為" to plan that something will happen at a particular time “. 意義拆分解釋為:選擇排程時機,達成排程目標。

    作業系統要實現多程序,程序排程必不可少。

     顧名思義,程序排程就是對程序進行排程,即負責選擇下一個要執行的程序.通過合理的排程,系統資源(如CPU時間)才能最大限度地發揮作用,多程序才會有併發執行的效果.

其目標可概括如下:

  1.高效性:高效意味著在相同的時間下要完成更多的任務。排程程式會被頻繁的執行,所以排程程式要儘可能的高效;

  2.加強互動效能:在系統相當的負載下,也要保證系統的響應時間;

  3.保證公平和避免飢渴;

  4.SMP排程:排程程式必須支援多處理系統;

  5.軟實時排程:系統必須有效的呼叫實時程序,但不保證一定滿足其要求;

Linux程序排程時機

Linux程序排程時機主要有:

1、程序狀態轉換的時刻:程序終止、程序睡眠;

2、當前程序的時間片用完時(current->counter=0);

3、裝置驅動程式

4、程序從中斷、異常及系統呼叫返回到使用者態時;

時機1,程序要呼叫sleep()或exit()等函式進行狀態轉換,這些函式會主動呼叫排程程式進行程序排程;

時機2,由於程序的時間片是由時鐘中斷來更新的,因此,這種情況和時機4是一樣的。

時機3,當裝置驅動程式執行長而重複的任務時,直接呼叫排程程式。在每次反覆迴圈中,驅動程式都檢查need_resched的值,如果必要,則呼叫排程程式schedule()主動放棄CPU。

時機4,如前所述,不管是從中斷、異常還是系統呼叫返回,最終都呼叫ret_from_sys_call(),由這個函式進行排程標誌的檢測,如果必要,則呼叫呼叫排程程式。那麼,為什麼從系統呼叫返回時要呼叫排程程式呢?這當然是從效率考慮。從系統呼叫返回意味著要離開核心態而返回到使用者態,而狀態的轉換要花費一定的時間,因此,在返回到使用者態前,系統把在核心態該處理的事全部做完。

Linux程序優先順序

  程序提供了兩種優先順序,一種是普通的程序優先順序,第二個是實時優先順序。前者適用SCHED_NORMAL排程策略,後者可選SCHED_FIFO或SCHED_RR排程策略。任何時候,實時程序的優先順序都高於普通程序,實時程序只會被更高階的實時程序搶佔,同級實時程序之間是按照FIFO(一次機會做完)或者RR(多次輪轉)規則排程的。

  首先,說下實時程序的排程

  實時程序,只有靜態優先順序,因為核心不會再根據休眠等因素對其靜態優先順序做調整,其範圍在0~MAX_RT_PRIO-1間。預設MAX_RT_PRIO配置為100,也即,預設的實時優先順序範圍是0~99。而nice值,影響的是優先順序在MAX_RT_PRIO~MAX_RT_PRIO+40範圍內的程序。

  不同與普通程序,系統排程時,實時優先順序高的程序總是先於優先順序低的程序執行。知道實時優先順序高的實時程序無法執行。實時程序總是被認為處於活動狀態。如果有數個優先順序相同的實時程序,那麼系統就會按照程序出現在佇列上的順序選擇程序。假設當前CPU執行的實時程序A的優先順序為a,而此時有個優先順序為b的實時程序B進入可執行狀態,那麼只要b<a,系統將中斷A的執行,而優先執行B,直到B無法執行(無論A,B為何種實時程序)。

   不同調度策略的實時程序只有在相同優先順序時才有可比性:

   1. 對於FIFO的程序,意味著只有當前程序執行完畢才會輪到其他程序執行。由此可見相當霸道。

   2. 對於RR的程序。一旦時間片消耗完畢,則會將該程序置於佇列的末尾,然後執行其他相同優先順序的程序,如果沒有其他相同優先順序的程序,則該程序會繼續執行。

   總而言之,對於實時程序,高優先順序的程序就是大爺。它執行到沒法執行了,才輪到低優先順序的程序執行。等級制度相當森嚴啊。

  重頭戲,說下非實時程序排程

    引子 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 將當前目錄下的documents目錄打包,但不希望tar佔用太多CPU: nice -19 tar zcf pack.tar.gz documents 這個“-19”中的“-”僅表示引數字首;所以,如果希望賦予tar程序最高的優先順序,則執行: nice --19 tar zcf pack.tar.gz documents 也可修改已經存在的程序的優先順序: 將PID為1799的程序優先順序設定為最低: renice 19 1799 renice命令與nice命令的優先順序引數的形式是相反的,直接以優先順序值作為引數即可,無“-”字首說法。

言歸正傳

  Linux對普通的程序,根據動態優先順序進行排程。而動態優先順序是由靜態優先順序(static_prio)調整而來。Linux下,靜態優先順序是使用者不可見的,隱藏在核心中。而核心提供給使用者一個可以影響靜態優先順序的介面,那就是nice值,兩者關係如下:

  static_prio=MAX_RT_PRIO +nice+ 20

  nice值的範圍是-20~19,因而靜態優先順序範圍在100~139之間。nice數值越大就使得static_prio越大,最終程序優先順序就越低。

  ps -el 命令執行結果:NI列顯示的每個程序的nice值,PRI是程序的優先順序(如果是實時程序就是靜態優先順序,如果是非實時程序,就是動態優先順序)  

  而程序的時間片就是完全依賴 static_prio 定製的,見下圖,摘自《深入理解linux核心》,

     

    我們前面也說了,系統排程時,還會考慮其他因素,因而會計算出一個叫程序動態優先順序的東西,根據此來實施排程。因為,不僅要考慮靜態優先順序,也要考慮程序的屬性。例如如果程序屬於互動式程序,那麼可以適當的調高它的優先順序,使得介面反應地更加迅速,從而使使用者得到更好的體驗。Linux2.6 在這方面有了較大的提高。Linux2.6認為,互動式程序可以從平均睡眠時間這樣一個measurement進行判斷。程序過去的睡眠時間越多,則越有可能屬於互動式程序。則系統排程時,會給該程序更多的獎勵(bonus),以便該程序有更多的機會能夠執行。獎勵(bonus)從0到10不等。

系統會嚴格按照動態優先順序高低的順序安排程序執行。動態優先順序高的程序進入非執行狀態,或者時間片消耗完畢才會輪到動態優先順序較低的程序執行。動態優先順序的計算主要考慮兩個因素:靜態優先順序,程序的平均睡眠時間也即bonus。計算公式如下,

     dynamic_prio = max (100, min (static_prio - bonus + 5, 139))

  在排程時,Linux2.6 使用了一個小小的trick,就是演算法中經典的空間換時間的思想[還沒對照原始碼確認],使得計算最優程序能夠在O(1)的時間內完成。

為什麼根據睡眠和執行時間確定獎懲分數是合理的

  睡眠和CPU耗時反應了程序IO密集和CPU密集兩大瞬時特點,不同時期,一個程序可能即是CPU密集型也是IO密集型程序。對於表現為IO密集的程序,應該經常執行,但每次時間片不要太長。對於表現為CPU密集的程序,CPU不應該讓其經常執行,但每次執行時間片要長。互動程序為例,假如之前其其大部分時間在於等待CPU,這時為了調高相應速度,就需要增加獎勵分。另一方面,如果此程序總是耗盡每次分配給它的時間片,為了對其他程序公平,就要增加這個程序的懲罰分數。可以參考CFS的virtutime機制.

現代方法CFS

  不再單純依靠程序優先順序絕對值,而是參考其絕對值,綜合考慮所有程序的時間,給出當前排程時間單位內其應有的權重,也就是,每個程序的權重X單位時間=應獲cpu時間,但是這個應得的cpu時間不應太小(假設閾值為1ms),否則會因為切換得不償失。但是,當程序足夠多時候,肯定有很多不同權重的程序獲得相同的時間——最低閾值1ms,所以,CFS只是近似完全公平。

Linux程序狀態機

  

  程序是通過fork系列的系統呼叫(fork、clone、vfork)來建立的,核心(或核心模組)也可以通過kernel_thread函式建立核心程序。這些建立子程序的函式本質上都完成了相同的功能——將呼叫程序複製一份,得到子程序。(可以通過選項引數來決定各種資源是共享、還是私有。)
那麼既然呼叫程序處於TASK_RUNNING狀態(否則,它若不是正在執行,又怎麼進行呼叫?),則子程序預設也處於TASK_RUNNING狀態。
另外,在系統呼叫clone和核心函式kernel_thread也接受CLONE_STOPPED選項,從而將子程序的初始狀態置為 TASK_STOPPED。

   程序建立後,狀態可能發生一系列的變化,直到程序退出。而儘管程序狀態有好幾種,但是程序狀態的變遷卻只有兩個方向——從TASK_RUNNING狀態變為非TASK_RUNNING狀態、或者從非TASK_RUNNING狀態變為TASK_RUNNING狀態。總之,TASK_RUNNING是必經之路,不可能兩個非RUN狀態直接轉換。

也就是說,如果給一個TASK_INTERRUPTIBLE狀態的程序傳送SIGKILL訊號,這個程序將先被喚醒(進入TASK_RUNNING狀態),然後再響應SIGKILL訊號而退出(變為TASK_DEAD狀態)。並不會從TASK_INTERRUPTIBLE狀態直接退出。

    程序從非TASK_RUNNING狀態變為TASK_RUNNING狀態,是由別的程序(也可能是中斷處理程式)執行喚醒操作來實現的。執行喚醒的程序設定被喚醒程序的狀態為TASK_RUNNING,然後將其task_struct結構加入到某個CPU的可執行佇列中。於是被喚醒的程序將有機會被排程執行。

   而程序從TASK_RUNNING狀態變為非TASK_RUNNING狀態,則有兩種途徑:

  1、響應訊號而進入TASK_STOPED狀態、或TASK_DEAD狀態;
  2、執行系統呼叫主動進入TASK_INTERRUPTIBLE狀態(如nanosleep系統呼叫)、或TASK_DEAD狀態(如exit系統呼叫);或由於執行系統呼叫需要的資源得不到滿     足,而進入TASK_INTERRUPTIBLE狀態或TASK_UNINTERRUPTIBLE狀態(如select系統呼叫)。
  顯然,這兩種情況都只能發生在程序正在CPU上執行的情況下。

 通過ps命令我們能夠檢視到系統中存在的程序,以及它們的狀態:

R(TASK_RUNNING),可執行狀態。

只有在該狀態的程序才可能在CPU上執行。而同一時刻可能有多個程序處於可執行狀態,這些程序的task_struct結構(程序控制塊)被放入對應CPU的可執行佇列中(一個程序最多隻能出現在一個CPU的可執行佇列中)。程序排程器的任務就是從各個CPU的可執行佇列中分別選擇一個程序在該CPU上執行。
只要可執行佇列不為空,其對應的CPU就不能偷懶,就要執行其中某個程序。一般稱此時的CPU“忙碌”。對應的,CPU“空閒”就是指其對應的可執行佇列為空,以致於CPU無事可做。
有人問,為什麼死迴圈程式會導致CPU佔用高呢?因為死迴圈程式基本上總是處於TASK_RUNNING狀態(程序處於可執行佇列中)。除非一些非常極端情況(比如系統記憶體嚴重緊缺,導致程序的某些需要使用的頁面被換出,並且在頁面需要換入時又無法分配到記憶體……),否則這個程序不會睡眠。所以CPU的可執行佇列總是不為空(至少有這麼個程序存在),CPU也就不會“空閒”。

很多作業系統教科書將正在CPU上執行的程序定義為RUNNING狀態、而將可執行但是尚未被排程執行的程序定義為READY狀態,這兩種狀態在linux下統一為 TASK_RUNNING狀態。

S(TASK_INTERRUPTIBLE),可中斷的睡眠狀態。

處於這個狀態的程序因為等待某某事件的發生(比如等待socket連線、等待訊號量),而被掛起。這些程序的task_struct結構被放入對應事件的等待佇列中。當這些事件發生時(由外部中斷觸發、或由其他程序觸發),對應的等待佇列中的一個或多個程序將被喚醒。

通過ps命令我們會看到,一般情況下,程序列表中的絕大多數程序都處於TASK_INTERRUPTIBLE狀態(除非機器的負載很高)。畢竟CPU就這麼一兩個,程序動輒幾十上百個,如果不是絕大多數程序都在睡眠,CPU又怎麼響應得過來。

D(TASK_UNINTERRUPTIBLE),不可中斷的睡眠狀態。

與TASK_INTERRUPTIBLE狀態類似,程序處於睡眠狀態,但是此刻程序是不可中斷的。不可中斷,指的並不是CPU不響應外部硬體的中斷,而是指程序不響應非同步訊號。
絕大多數情況下,程序處在睡眠狀態時,總是應該能夠響應非同步訊號的。否則你將驚奇的發現,kill -9竟然殺不死一個正在睡眠的程序了!於是我們也很好理解,為什麼ps命令看到的程序幾乎不會出現TASK_UNINTERRUPTIBLE狀態,而總是TASK_INTERRUPTIBLE狀態。

而TASK_UNINTERRUPTIBLE狀態存在的意義就在於,核心的某些處理流程是不能被打斷的。如果響應非同步訊號,程式的執行流程中就會被插入一段用於處理非同步訊號的流程(這個插入的流程可能只存在於核心態,也可能延伸到使用者態),於是原有的流程就被中斷了(參見《linux非同步訊號handle淺析》)。
在程序對某些硬體進行操作時(比如程序呼叫read系統呼叫對某個裝置檔案進行讀操作,而read系統呼叫最終執行到對應裝置驅動的程式碼,並與對應的物理裝置進行互動),可能需要使用TASK_UNINTERRUPTIBLE狀態對程序進行保護,以避免程序與裝置互動的過程被打斷,造成裝置陷入不可控的狀態。(比如read系統呼叫觸發了一次磁碟到使用者空間的記憶體的DMA,如果DMA進行過程中,程序由於響應訊號而退出了,那麼DMA正在訪問的記憶體可能就要被釋放了。)這種情況下的TASK_UNINTERRUPTIBLE狀態總是非常短暫的,通過ps命令基本上不可能捕捉到。

linux系統中也存在容易捕捉的TASK_UNINTERRUPTIBLE狀態。執行vfork系統呼叫後,父程序將進入TASK_UNINTERRUPTIBLE狀態,直到子程序呼叫exit或exec。
通過下面的程式碼就能得到處於TASK_UNINTERRUPTIBLE狀態的程序:
#include <unistd.h>
void main() {
if (!vfork()) sleep(100);
}
編譯執行,然後ps一下:
[email protected]:~/test$ ps -ax | grep a\.out
4371 pts/0 D+ 0:00 ./a.out
4372 pts/0 S+ 0:00 ./a.out
4374 pts/1 S+ 0:00 grep a.out
然後我們可以試驗一下TASK_UNINTERRUPTIBLE狀態的威力。不管kill還是kill -9,這個TASK_UNINTERRUPTIBLE狀態的父程序依然屹立不倒。

T(TASK_STOPPED or TASK_TRACED),暫停狀態或跟蹤狀態。

向程序傳送一個SIGSTOP訊號,它就會因響應該訊號而進入TASK_STOPPED狀態(除非該程序本身處於TASK_UNINTERRUPTIBLE狀態而不響應訊號)。(SIGSTOP與SIGKILL訊號一樣,是非常強制的。不允許使用者程序通過signal系列的系統呼叫重新設定對應的訊號處理函式。)
向程序傳送一個SIGCONT訊號,可以讓其從TASK_STOPPED狀態恢復到TASK_RUNNING狀態。

當程序正在被跟蹤時,它處於TASK_TRACED這個特殊的狀態。“正在被跟蹤”指的是程序暫停下來,等待跟蹤它的程序對它進行操作。比如在gdb中對被跟蹤的程序下一個斷點,程序在斷點處停下來的時候就處於TASK_TRACED狀態。而在其他時候,被跟蹤的程序還是處於前面提到的那些狀態。
對於程序本身來說,TASK_STOPPED和TASK_TRACED狀態很類似,都是表示程序暫停下來。
而TASK_TRACED狀態相當於在TASK_STOPPED之上多了一層保護,處於TASK_TRACED狀態的程序不能響應SIGCONT訊號而被喚醒。只能等到除錯程序通過ptrace系統呼叫執行PTRACE_CONT、PTRACE_DETACH等操作(通過ptrace系統呼叫的引數指定操作),或除錯程序退出,被除錯的程序才能恢復TASK_RUNNING狀態。

Z(TASK_DEAD - EXIT_ZOMBIE),退出狀態,程序成為殭屍程序。

程序在退出的過程中,處於TASK_DEAD狀態。

在這個退出過程中,程序佔有的所有資源將被回收,除了task_struct結構(以及少數資源)以外。於是程序就只剩下task_struct這麼個空殼,故稱為殭屍。
之所以保留task_struct,是因為task_struct裡面儲存了程序的退出碼、以及一些統計資訊。而其父程序很可能會關心這些資訊。比如在shell中,$?變數就儲存了最後一個退出的前臺程序的退出碼,而這個退出碼往往被作為if語句的判斷條件。
當然,核心也可以將這些資訊儲存在別的地方,而將task_struct結構釋放掉,以節省一些空間。但是使用task_struct結構更為方便,因為在核心中已經建立了從pid到task_struct查詢關係,還有程序間的父子關係。釋放掉task_struct,則需要建立一些新的資料結構,以便讓父程序找到它的子程序的退出資訊。

父程序可以通過wait系列的系統呼叫(如wait4、waitid)來等待某個或某些子程序的退出,並獲取它的退出資訊。然後wait系列的系統呼叫會順便將子程序的屍體(task_struct)也釋放掉。
子程序在退出的過程中,核心會給其父程序傳送一個訊號,通知父程序來“收屍”。這個訊號預設是SIGCHLD,但是在通過clone系統呼叫建立子程序時,可以設定這個訊號。

通過下面的程式碼能夠製造一個EXIT_ZOMBIE狀態的程序:
#include <unistd.h>
void main() {
if (fork())
while(1) sleep(100);
}
編譯執行,然後ps一下:
[email protected]:~/test$ ps -ax | grep a\.out
10410 pts/0 S+ 0:00 ./a.out
10411 pts/0 Z+ 0:00 [a.out] <defunct>
10413 pts/1 S+ 0:00 grep a.out

只要父程序不退出,這個殭屍狀態的子程序就一直存在。那麼如果父程序退出了呢,誰又來給子程序“收屍”?
當程序退出的時候,會將它的所有子程序都託管給別的程序(使之成為別的程序的子程序)。託管給誰呢?可能是退出程序所在程序組的下一個程序(如果存在的話),或者是1號程序。所以每個程序、每時每刻都有父程序存在。除非它是1號程序。

1號程序,pid為1的程序,又稱init程序。
linux系統啟動後,第一個被建立的使用者態程序就是init程序。它有兩項使命:
1、執行系統初始化指令碼,建立一系列的程序(它們都是init程序的子孫);
2、在一個死迴圈中等待其子程序的退出事件,並呼叫waitid系統呼叫來完成“收屍”工作;
init程序不會被暫停、也不會被殺死(這是由核心來保證的)。它在等待子程序退出的過程中處於TASK_INTERRUPTIBLE狀態,“收屍”過程中則處於TASK_RUNNING狀態。

X(TASK_DEAD - EXIT_DEAD),退出狀態,程序即將被銷燬。

而程序在退出過程中也可能不會保留它的task_struct。比如這個程序是多執行緒程式中被detach過的程序(程序?執行緒?參見《linux執行緒淺析》)。或者父程序通過設定SIGCHLD訊號的handler為SIG_IGN,顯式的忽略了SIGCHLD訊號。(這是posix的規定,儘管子程序的退出訊號可以被設定為SIGCHLD以外的其他訊號。)
此時,程序將被置於EXIT_DEAD退出狀態,這意味著接下來的程式碼立即就會將該程序徹底釋放。所以EXIT_DEAD狀態是非常短暫的,幾乎不可能通過ps命令捕捉到。

一些重要的雜項

排程程式的效率
“優先順序”明確了哪個程序應該被排程執行,而排程程式還必須要關心效率問題。排程程式跟核心中的很多過程一樣會頻繁被執行,如果效率不濟就會浪費很多CPU時間,導致系統性能下降。
在linux 2.4時,可執行狀態的程序被掛在一個連結串列中。每次排程,排程程式需要掃描整個連結串列,以找出最優的那個程序來執行。複雜度為O(n);
在linux 2.6早期,可執行狀態的程序被掛在N(N=140)個連結串列中,每一個連結串列代表一個優先順序,系統中支援多少個優先順序就有多少個連結串列。每次排程,排程程式只需要從第一個不為空的連結串列中取出位於連結串列頭的程序即可。這樣就大大提高了排程程式的效率,複雜度為O(1);
在linux 2.6近期的版本中,可執行狀態的程序按照優先順序順序被掛在一個紅黑樹(可以想象成平衡二叉樹)中。每次排程,排程程式需要從樹中找出優先順序最高的程序。複雜度為O(logN)。

那麼,為什麼從linux 2.6早期到近期linux 2.6版本,排程程式選擇程序時的複雜度反而增加了呢?
這是因為,與此同時,排程程式對公平性的實現從上面提到的第一種思路改變為第二種思路(通過動態調整優先順序實現)。而O(1)的演算法是基於一組數目不大的連結串列來實現的,按我的理解,這使得優先順序的取值範圍很小(區分度很低),不能滿足公平性的需求。而使用紅黑樹則對優先順序的取值沒有限制(可以用32位、64位、或更多位來表示優先順序的值),並且O(logN)的複雜度也還是很高效的。

排程觸發的時機
排程的觸發主要有如下幾種情況:
1、當前程序(正在CPU上執行的程序)狀態變為非可執行狀態。
程序執行系統呼叫主動變為非可執行狀態。比如執行nanosleep進入睡眠、執行exit退出、等等;
程序請求的資源得不到滿足而被迫進入睡眠狀態。比如執行read系統呼叫時,磁碟快取記憶體裡沒有所需要的資料,從而睡眠等待磁碟IO;
程序響應訊號而變為非可執行狀態。比如響應SIGSTOP進入暫停狀態、響應SIGKILL退出、等等;

2、搶佔。程序執行時,非預期地被剝奪CPU的使用權。這又分兩種情況:程序用完了時間片、或出現了優先順序更高的程序。
優先順序更高的程序受正在CPU上執行的程序的影響而被喚醒。如傳送訊號主動喚醒,或因為釋放互斥物件(如釋放鎖)而被喚醒;
核心在響應時鐘中斷的過程中,發現當前程序的時間片用完;
核心在響應中斷的過程中,發現優先順序更高的程序所等待的外部資源的變為可用,從而將其喚醒。比如CPU收到網絡卡中斷,核心處理該中斷,發現某個socket可讀,於是喚醒正在等待讀這個socket的程序;再比如核心在處理時鐘中斷的過程中,觸發了定時器,從而喚醒對應的正在nanosleep系統呼叫中睡眠的程序;

核心搶佔
理想情況下,只要滿足“出現了優先順序更高的程序”這個條件,當前程序就應該被立刻搶佔。但是,就像多執行緒程式需要用鎖來保護臨界區資源一樣,核心中也存在很多這樣的臨界區,不大可能隨時隨地都能接收搶佔。
linux 2.4時的設計就非常簡單,核心不支援搶佔。程序執行在核心態時(比如正在執行系統呼叫、正處於異常處理函式中),是不允許搶佔的。必須等到返回使用者態時才會觸發排程(確切的說,是在返回使用者態之前,核心會專門檢查一下是否需要排程);
linux 2.6則實現了核心搶佔,但是在很多地方還是為了保護臨界區資源而需要臨時性的禁用核心搶佔。

也有一些地方是出於效率考慮而禁用搶佔,比較典型的是spin_lock。spin_lock是這樣一種鎖,如果請求加鎖得不到滿足(鎖已被別的程序佔有),則當前程序在一個死迴圈中不斷檢測鎖的狀態,直到鎖被釋放。
為什麼要這樣忙等待呢?因為臨界區很小,比如只保護“i+=j++;”這麼一句。如果因為加鎖失敗而形成“睡眠-喚醒”這麼個過程,就有些得不償失了。
那麼既然當前程序忙等待(不睡眠),誰又來釋放鎖呢?其實已得到鎖的程序是執行在另一個CPU上的,並且是禁用了核心搶佔的。這個程序不會被其他程序搶佔,所以等待鎖的程序只有可能執行在別的CPU上。(如果只有一個CPU呢?那麼就不可能存在等待鎖的程序了。)
而如果不禁用核心搶佔呢?那麼得到鎖的程序將可能被搶佔,於是可能很久都不會釋放鎖。於是,等待鎖的程序可能就不知何年何月得償所望了。

對於一些實時性要求更高的系統,則不能容忍spin_lock這樣的東西。寧可改用更費勁的“睡眠-喚醒”過程,也不能因為禁用搶佔而讓更高優先順序的程序等待。比如,嵌入式實時linux montavista就是這麼幹的。
由此可見,實時並不代表高效。很多時候為了實現“實時”,還是需要對效能做一定讓步的。

多處理器下的負載均衡
前面我們並沒有專門討論多處理器對排程程式的影響,其實也沒有什麼特別的,就是在同一時刻能有多個程序並行地執行而已。那麼,為什麼會有“多處理器負載均衡”這個事情呢?
如果系統中只有一個可執行佇列,哪個CPU空閒了就去佇列中找一個最合適的程序來執行。這樣不是很好很均衡嗎?
的確如此,但是多處理器共用一個可執行佇列會有一些問題。顯然,每個CPU在執行排程程式時都需要把佇列鎖起來,這會使得排程程式難以並行,可能導致系統性能下降。而如果每個CPU對應一個可執行佇列則不存在這樣的問題。
另外,多個可執行佇列還有一個好處。這使得一個程序在一段時間內總是在同一個CPU上執行,那麼很可能這個CPU的各級cache中都快取著這個程序的資料,很有利於系統性能的提升。
所以,在linux下,每個CPU都有著對應的可執行佇列,而一個可執行狀態的程序在同一時刻只能處於一個可執行佇列中。

於是,“多處理器負載均衡”這個麻煩事情就來了。核心需要關注各個CPU可執行佇列中的程序數目,在數目不均衡時做出適當調整。什麼時候需要調整,以多大力度程序調整,這些都是核心需要關心的。當然,儘量不要調整最好,畢竟調整起來又要耗CPU、又要鎖可執行佇列,代價還是不小的。
另外,核心還得關心各個CPU的關係。兩個CPU之間,可能是相互獨立的、可能是共享cache的、甚至可能是由同一個物理CPU通過超執行緒技術虛擬出來的……CPU之間的關係也是實現負載均衡的重要依據。關係越緊密,程序在它們之間遷移的代價就越小。參見《linux核心SMP負載均衡淺析》。

優先順序繼承
由於互斥,一個程序(設為A)可能因為等待進入臨界區而睡眠。直到正在佔有相應資源的程序(設為B)退出臨界區,程序A才被喚醒。
可能存在這樣的情況:A的優先順序非常高,B的優先順序非常低。B進入了臨界區,但是卻被其他優先順序較高的程序(設為C)搶佔了,而得不到執行,也就無法退出臨界區。於是A也就無法被喚醒。
A有著很高的優先順序,但是現在卻淪落到跟B一起,被優先順序並不太高的C搶佔,導致執行被推遲。這種現象就叫做優先順序反轉。

出現這種現象是很不合理的。較好的應對措施是:當A開始等待B退出臨界區時,B臨時得到A的優先順序(還是假設A的優先順序高於B),以便順利完成處理過程,退出臨界區。之後B的優先順序恢復。這就是優先順序繼承的方法。

中斷處理執行緒化
在linux下,中斷處理程式運行於一個不可排程的上下文中。從CPU響應硬體中斷自動跳轉到核心設定的中斷處理程式去執行,到中斷處理程式退出,整個過程是不能被搶佔的。
一個程序如果被搶佔了,可以通過儲存在它的程序控制塊(task_struct)中的資訊,在之後的某個時間恢復它的執行。而中斷上下文則沒有task_struct,被搶佔了就沒法恢復了。
中斷處理程式不能被搶佔,也就意味著中斷處理程式的“優先順序”比任何程序都高(必須等中斷處理程式完成了,程序才能被執行)。但是在實際的應用場景中,可能某些實時程序應該得到比中斷處理程式更高的優先順序。
於是,一些實時性要求更高的系統就給中斷處理程式賦予了task_struct以及優先順序,使得它們在必要的時候能夠被高優先順序的程序搶佔。但是顯然,做這些工作是會給系統造成一定開銷的,這也是為了實現“實時”而對效能做出的一種讓步。

參考文獻:《linux核心設計與實現》

       《現代作業系統》


原文連結:http://www.cnblogs.com/zhaoyl/archive/2012/09/04/2671156.html


相關推薦

Linux程序排程原理

本文系以下文章整理而來,並添加了自己的一些見解: (1)http://www.cnblogs.com/zhaoyl/archive/2012/09/04/2671156.html (2)http://os.51cto.com/art/201003/187407.htm (3

linux 程序排程 pick_next_task()函式

pick_next_task是程序排程的關鍵步驟,主要功能是從發生排程的CPU的執行佇列中選擇一個程序執行。本文主要介紹該函式的實現過程。系統中的排程順序是:實時程序------->普通程序------>空閒程序。分別從屬於三個排程類:rt_sched_class

linux程序排程機制

版本宣告:轉載請註明出處,未經允許,禁止商業用途。 linux是以執行緒為單位進行CPU排程的。所以下面的描述中所說的執行緒和程序從CPU排程角度來說是等效。 Linux程序優先順序: Priority。程序的優先順序是作業系統自己給定並且動態調整的。使用者可以通過nice值來調整實際優先順序。 C

linux程序排程演算法

一:什麼是程序排程   都知道linux是一種多使用者多工的作業系統,而當多個程序同時執行去,搶佔有限資源的時候,這時作業系統就會按照一定的原則將資源合理分配給請求資源的程序,這就是程序排程。 二:在linux作業系統中都有哪些程序排程演算法   1. 先進先出演算法(

Linux 程序排程淺析

作業系統要實現多程序,程序排程必不可少。程序排程是對TASK_RUNNING狀態的程序進行排程(參見《linux程序狀態淺析》)。如果程序不可執行(正在睡眠或其他),那麼它跟程序排程沒多大關係。 所以,如果你的系統負載非常低,盼星星盼月亮才出現一個可執行狀態的程序。那麼程序排程也就不會太重要。哪個程序可執行

linux程序排程方法(SCHED_OTHER,SCHED_FIFO,SCHED_RR)

linux核心的三種排程方法: 1,SCHED_OTHER   分時排程策略, 2,SCHED_FIFO        實時排程策略,先到先服務 3,SCHED_RR          實時排程策略,時間片輪轉  分時程序則通過nice和counter值決定權值,nice

Linux程序排程器的設計--Linux程序的管理與排程(十七)

1 前景回顧 1.1 程序排程 記憶體中儲存了對每個程序的唯一描述, 並通過若干結構與其他程序連線起來. 排程器面對的情形就是這樣, 其任務是在程式之間共享CPU時間, 創造並行執行的錯覺, 該任務分為兩個不同的部分, 其中一個

Linux程序排程分析

原文:http://www.2cto.com/os/201112/113229.html 操作系統要實現多程序,程序排程必不可少。 有人說,程序排程是作業系統中最為重要的一個部分。我覺得這種說法說得太絕對了一點,就像很多人動輒就說"某某函式比某某函式效率高XX倍"一樣

Linux程序排程程序與執行緒的本質區別?

簡而言之,執行緒是程序的一部分,程序是程式的一部分。 異同: 1、程序是資源分配的基本單位,而執行緒是排程的基本單位; 2、程序與程序之間是獨立的,一個程序的異常終止不會影響其它程序,而執行緒與執行

linux程序排程

1. 程序排程類初始化 建立程序時,程序排程類的初始化在函式sched_fork()中進行 (kernel/sched/core.c) int sched_fork(unsigned long clone_flags, struct task_stru

Linux程序呼叫原理

    Linux程序排程的目標     1.高效性:高效意味著在相同的時間下要完成更多的任務。排程程式會被頻繁的執行,所以排程程式要儘可能的高效;     2.加強互動效能:在系統相當的負載下,也要保證系統的響應時間;     3.保證公平和避免飢渴;     4.SMP排程:排程程式必須支援多處理系統;

Linux 程序排程小結

概述 這個問題又是面試常問問題,當時聽到感覺太寬泛了,有點大,心裡知道但是說不全,這裡做一下總結 【1】程序排程的作用 【2】排程德策略 1、 程序排程的作用 ,程序排程就是對程序進行排程,即負責選擇下一個要執行的程序.通過合理的排程,系統資源才能最大限度地發揮作用,多程

Linux程序排程CFS演算法實現分析

網上講CFS的文章很多,可能版本不一,理解不盡相同。我以問題追溯方式,跟蹤原始碼寫下我對CFS的理解,有的問題我也還沒理解透,歡迎對核心有興趣的朋友一起交流學習,原始碼版本是與LKD3配套的Linux2.6.34 背景知識: (1) Linux的排程器類主

linux程序排程演算法:分時排程策略、FIFO排程策略、RR排程策略

linux核心的三種排程方法: SCHED_OTHER 分時排程策略, SCHED_FIFO實時排程策略,先到先服務 SCHED_RR實時排程策略,時間片輪轉 注意: 實時程序將得到優先呼叫,實時程序根據實時優先順序決

程序排程演算法Linux程序排程演算法

這次介紹一下作業系統的程序排程演算法 作業系統的排程分為三種:1.遠端排程(建立新程序);2.中程排程(交換功能的一部分);3.短程排程(下次執行哪個程序) 這次講述的就是短程排程,可以簡單的看作咱們平

linux程序排程解析

1排程器的啟動通常有兩種方式:A. 主動式在核心應用中直接呼叫schedule()。這通常發生在因等待核心事件而需要將程序置於掛起(休眠)狀態的時候--這時應該主動請求排程以方便其他程序使用CPU。下面就是一個主動排程的例子:/* 節選自[drivers/input/mous

linux程序排程(2)

1.程序的排程 作為多程序的系統,Linux系統必須擔負起排程程序的責任,不斷地切換程序,以使CPU得到最大化的利用,提高系統的效率。 1.1 Linux程序排程的策略 程序排程的策略主要考慮以下幾個原則: (1) 高效 — 使處理器

Linux---程序排程相關命令解析

程序相關命令 1、ps  檢視系統中的程序 使用方式:ps [options] [--help] 說明:顯示瞬間程序 (process) 的動態 引數:ps的引數非常多, 在此僅列出幾個常用的引數並大略介紹含義 ps命令常用用法(方便檢視系統程序) 1)ps

linux程序排程淺析

     作業系統要實現多程序,程序排程必不可少。      程序排程是對TASK_RUNNING狀態的程序進行排程(參見《linux程序狀態淺析》)。如果程序不可執行(正在睡眠或其他),那麼它跟程序排程沒多大關係。      所以,如果你的系統負載非常低,盼星星盼月亮才出現一個可執行狀態的程序。那麼程序

分析linux程序排程程序切換

慕課18原創作品轉載請註明出處 + 《Linux核心分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000 一、Linux程序排程時機主要有: (1)主動排程: 程序的執行狀態發生變化時,例如等待某些事件而