linux 進程調度__linux

來源:互聯網
上載者:User
Linux進程調度原理

    Linux進程調度的目標

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

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

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

    4.SMP調度:發送器必須支援多處理系統;

    5.軟即時調度:系統必須有效調用即時進程,但不保證一定滿足其要求;

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核心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一下:
kouu@kouu-one:~/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一下:
kouu@kouu-one:~/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呢。那麼就不可能存在等待鎖的進程了。)
而如果不禁用核心搶

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.