Linux內核——進程管理與調度

進程的管理與調度


進程管理


進程描寫敘述符及任務結構

    進程存放在叫作任務隊列(tasklist)的雙向循環鏈表中。鏈表中的每一項包括一個詳細進程的所有信息,類型爲task_struct,稱爲進程描寫敘述符(process descriptor),該結構定義在<linux/sched.h>文件裏。html

    Linux經過slab分配器分配task_struct結構,這樣能達到對象複用和緩存着色(cache coloring)的目的。還有一方面,爲了不使用額外的寄存器存儲專門記錄,讓像x86這樣寄存器較少的硬件體系結構僅僅要經過棧指針就能計算出task_struct的位置,該結構爲thread_info,在文件<asm/thread_info.h>中定義。linux

Linux中可以用ps命令查看所有進程的信息。算法

進程狀態

task_struct中的state描寫敘述進程的當前狀態。進程的狀態一共同擁有5種,而進程一定處於當中一種狀態:數組

    1)TASK_RUNNING(運行)——進程是可運行的,它或者正在運行,或者在運行隊列中等待運行。這是進程在用戶空間中運行惟一可能的狀態;也可以應用到內核空間中正在運行的進程。緩存

    2)TASK_INTERRUPTIBLE(可中斷)——進程正在睡眠(也就是說它被堵塞)等待某些條件的達成。一旦這些條件達成,內核就會把進程狀態設置爲執行,處於此狀態的進程也會因爲接收到信號而提早被喚醒並投入執行。安全

    3)TASK_UNINTERRUPTIBLE(不可中斷)——除了不會因爲接收到信號而被喚醒從而投入執行外,這個狀態與可打斷狀態一樣。這個狀態一般在進程必須在等待時不受干擾或等待事件很是快就會發生時出現。因爲處於此狀態的任務對信號不做響應,因此較之可中斷狀態,使用得較少。數據結構

    4)TASK_ZOMBIE(僵死)——該進程已經結束了,但是其父進程尚未調用wait4()系統調用。爲了父進程能夠獲知它的消息,子進程的進程描寫敘述符仍然被保留着。一旦父進程調用了wait4(),進程描寫敘述符就會被釋放。併發

    5)TASK_STOPPED(中止)——進程中止執行,進程沒有投入執行也不能投入執行。一般這樣的狀態發生在接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信號的時候。此外,在調試期間接收到不論什麼信號,都會使進程進入這樣的狀態。函數

    需要調整進程的狀態,最好使用set_task_state(task, state)函數,在必要的時候,它會設置內存屏障來強制其它處理器做又一次排序(SMP)。性能

進程的各個狀態之間的轉化構成了進程的整個生命週期,下圖來自http://www.cnblogs.com/wang_yb/archive/2012/08/20/2647912.html

 

進程的建立

         在Linux系統中,所有的進程都是PID爲1的init進程的後代。內核在系統啓動的最後階段啓動init進程。該進程讀取系統的初始化腳本(initscript)並運行其它的相關程序,終於完畢系統啓動的整個進程。

Linux提供兩個函數去處理進程的建立和運行:fork()和exec()。首先,fork()經過拷貝當前進程建立一個子進程。子進程與父進程的差異只在於PID(每個進程惟一),PPID(父進程的PID)和某些資源和統計量(好比掛起的信號)。exec()函數負責讀取可運行文件並將其加載地址空間開始運行。

        fork()使用寫時拷貝(copy-on-write)頁實現。內核在fork進程時不復制整個進程地址空間,讓父進程和子進程共享同一個拷貝,當需要寫入時,數據纔會被複制,使各進程擁有本身的拷貝。在頁根本不會被寫入的狀況下(fork()後立刻exec()),fork的實際開銷僅僅有複製父進程的頁表以及給子進程建立惟一的task_struct。

建立進程的fork()函數實際上終因而調用clone()函數。建立線程和進程的步驟同樣,僅僅是終於傳給clone()函數的參數不一樣。比方,經過一個普通的fork來建立進程,至關於:clone(SIGCHLD, 0);建立一個和父進程共享地址空間,文件系統資源,文件描寫敘述符和信號處理程序的進程,即一個線程:clone(CLONE_VM | CLONE_FS | CLONE_FILES |CLONE_SIGHAND, 0)。

在內核中建立的內核線程與普通的進程之間還有個主要差異在於:內核線程沒有獨立的地址空間,它們僅僅能在內核空間執行。

fork和vfork的差異

fork()與vfock()都是建立一個進程,那他們有什麼差異呢?總結有下面三點差異: 
1.  fork  ():子進程拷貝父進程的數據段,代碼段 
    vfork ( ):子進程與父進程共享數據段 
2.  fork ()父子進程的運行次序不肯定 
    vfork 保證子進程先執行,在調用exec 或exit 以前與父進程數據是共享的,在它調用exec
     或exit 以後父進程纔可能被調度執行。 
3.  vfork ()保證子進程先執行,在她調用exec 或exit 以後父進程纔可能被調度執行。假設在
   調用這兩個函數以前子進程依賴於父進程的進一步動做,則會致使死鎖。 

進程終止

進程在執行結束,或接受到它既不能處理也不能忽略的信號,或異常時,都會被終結。此時,依靠do_exit()(在kernel/exit.c文件裏)把與進程相關聯的所有資源都被釋放掉(若是進程是這些資源的惟一使用者)。至此,與進程相關的所有資源都被釋放掉了。進程不可執行(實際上也沒有地址空間讓它執行)並處於TASK_ZOMBIE狀態。它佔用的所有資源就是內核棧、thread_info和task_struct。此時進程存在的惟一目的就是想它的父進程提供信息。在父進程得到已終結的子進程的信息後,或者通知內核它並不關注那些信息後,子進程持有的task_struct等剩餘內存才被釋放。

孤兒進程問題

假設父進程在子進程以前退出,必須有機制保證子進程能找到一個新的父類,不然的話這些成爲孤兒的進程就會在退出時永遠處於僵死狀態,白白的耗費內存。解決方法是給子進程在當前線程組內找一個線程做爲父親,假設不行,就讓init作它們的父進程。

進程調度

什麼是調度

現在的操做系統都是多任務的,爲了能讓不少其它的任務能同一時候在系統上更好的執行,需要一個管理程序來管理計算機上同一時候執行的各個任務(也就是進程)。

這個管理程序就是調度程序,它的功能提及來很是easy:

1.決定哪些進程執行,哪些進程等待

2.決定每個進程執行多長時間

此外,爲了得到更好的用戶體驗,執行中的進程還能夠立刻被其它更緊急的進程打斷。總之,調度是一個平衡的過程。一方面,它要保證各個執行的進程能夠最大限度的使用CPU(即儘可能少的切換進程,進程切換過多,CPU的時間會浪費在切換上);還有一方面,保證各個進程能公平的使用CPU(即防止一個進程長時間獨佔CPU的狀況)。

策略

I/O消耗型和處理器消耗型的進程

I/O消耗型進程:大部分時間用來提交I/O請求或是等待I/O請求,經常處於可執行狀態,但執行時間短,等待請求過程時處於堵塞狀態。如交互式程序。

       處理器消耗型進程:時間大都用在執行代碼上,除非被搶佔不然一直不停的執行。

       調度策略要在:進程響應迅速(響應時間短)和最大系統利用率(高吞吐量)之間尋找平衡。      

Linux爲了保證交互式應用,因此對進程的對應作了優化,更傾向於優先調度I/O消耗型進程。

進程優先級

調度算法中最主要的一類就是基於優先級的調度。這是一種依據進程的價值和其對處理器時間的需求來對進程分級的想法。優先級高的進程先執行,低的後執行,一樣優先級的進程按輪轉方式進行調度。

        Linux依據以上思想實現了一種基於動態優先級的調度方法。一開始,該方法先設置主要的優先級,然而它贊成調度程度依據需要來加、減優先級。好比,假設一個進程在I/O等待上耗費的時間多於其執行時間,那麼該進程明顯屬於I/O消耗型,它的優先級會被動態提升。相反,處理器消耗型進程的優先級會被動態減小。

        Linux內核提供兩組獨立的優先級範圍。第一種是nice值,範圍從-20到+19,默認值是0。nice值越大優先級越低。另一種是實時優先級,其值可配置,範圍從0到99,不論什麼實時進程的優先級都高於普通的進程。

時間片

時間片是一個數值,它代表進程在被搶佔前所能持續執行的時間,I/O消耗型不需要長的時間片,而處理器消耗型的進程則但願越長越好。時間片的大小設置並不簡單,設大了,系統響應變慢(調度週期長);設小了,進程頻繁切換帶來的處理器消耗。

         Linux調度程序提升交互程序的優先級,讓它們運行得更頻繁。因而,調度程序提供了比較長的默認時間片給交互程序。此外,Linux調度程序還能依據進程的優先級動態調整分配給它的時間片。從而保證優先級高的進程,假定也是重要性高的進程,運行的頻率高,運行時間長。經過實現這樣一種動態調整優先級和時間片長度的機制,Linux調度性性能不但很是穩定而且也很是強健。

注意,進程並不是必定非要一次就用完它所有的時間片,好比一個擁有100毫秒時間片的進程,可以經過反覆調度,分5次每次20毫秒用完這些時間片。

當一個進程的時間耗盡時,就以爲到期了。沒有時間片的進程不會再投入執行,除非等到其它所有的進程都耗盡了他們的時間片。那個時候,所有進程的時間片會被又一次計算。

進程搶佔

Linux是搶佔式的。當一個進程進入TASK_RUNNING狀態,內核會檢查它的優先級是否高於當前正在執行的進程。假設是這樣,調度程序會被喚醒,搶佔當前正在執行的進程並執行新的可執行進程。此外,當一個進程的時間片變爲0時,它會被搶佔,調度程序被喚醒以選擇一個新的進程。

 

調度算法

可運行隊列

調度程序中最主要的數據結構式運行隊列(runqueue)。可運行隊列是給定處理器上的可運行進程的鏈表,每個處理器一個。每個可投入運行的進程都惟一的歸屬於一個可運行隊列。此外,可運行隊列中還包括每個處理器的調度信息。因此,可運行隊列也是每個處理器最重要的數據結構。

        爲了不死鎖,要鎖住多個執行隊列的代碼必須老是依照相同的順序獲取這些鎖:依照可執行隊列地址從低向高的順序。

優先級數組

每個執行隊列都有兩個優先級數組,一個活躍的和一個過時的。優先級數組是一種能夠提供O(1)級算法複雜度的數據結構。優先級數組使可執行處理器的每一種優先級都包括一個相應的隊列,而這些隊列包括相應優先級上的可執行進程鏈表。優先級數組還擁有一個優先級位圖,當需要查找當前系統內擁有最高優先級的可執行進程時,它能夠幫助提升效率。

又一次計算時間片

不少操做系統在所有進程的時間片都用完時,都採用一種顯示的方法來計算時間片。典型的實現是循環訪問每個進程,這樣可能會耗費至關長的時間,最壞狀況爲O(N);重算時必須考鎖的形式來保護任務隊列和每個進程描寫敘述符,這樣作會加重對鎖的爭用;又一次計算時間的實際不肯定。

活躍數組內的可運行隊列上的進程都還有時間片剩餘,而過時數組內的都耗盡了時間片。當一個進程的時間片耗盡時,它會被移至過時數組,但在此以前,時間片已經給它又一次計算好。又一次計算時間片變得很easy,僅僅要在活躍和過時數組之間來回切換,這是O(1)級調度程序的核心。

schedule()

 選定下一個進程並切換到它去運行是經過schedule()函數實現的。當內核代碼想要休眠時,會直接調用該函數,另外,假設有哪一個進程將被搶佔,那麼該函數也會被喚起運行。schedule()函數獨立於每個處理器運行。

首先要在活動優先級數組中找到第一個被設置的位,該位對於這優先級最高的可運行進程。而後,調度程序選擇這個級別鏈表裏的有一個進程。這就是系統中優先級最高的可運行程序。假設被選中的進程不是當前進程,就進行上下文切換。

計算優先級和時間片

        nice值之因此起名爲靜態優先級,是因爲它從一開始由用戶指定後,就不能改變。動態優先級經過一個關於靜態優先級和進程交互性的函數關係計算而來。effective_prio()函數可以返回一個進程的動態優先級。這個函數以nice值爲基數,再加上-5到+5之間的進程交互性的獎勵或罰分。

        怎麼經過一些判斷來獲取準確反映進程到底是I/O消耗型的仍是處理器消耗型的。最明顯的標準莫過於進程休眠的時間長短了。假設一個進程的大部分時間都在休眠,那麼它就是I/O消耗型的。假設一個進程運行的時間比休眠的時間長,那它就是處理器消耗型的。

        還有一方面,又一次計算時間片相對簡單了。它僅僅要以靜態優先級爲基礎就可以了。在一個進程建立的時候,新建的子進程和父進程均分父進程剩餘的進程時間片。這種分配很是公平並且防止用戶經過不斷建立新進程來不停地獲取時間片。task_timeslice()函數爲給定任務返回一個新的時間片。時間片的計算僅僅需要把優先級按比例縮放,使其符合時間片的數值範圍要求就可以了。進程的靜態優先級越高,它每次運行獲得的時間片就越長。

調度程序還提供了第二種機制以支持交互進程:假設一個進程的交互性很強,那麼當它時間片用完後,它會被放置到活動數組而不是過時數組中。

睡眠與喚醒

       休眠(被堵塞)的進程處於一個特殊的不可運行狀態。進程把它本身標記成休眠狀態,把本身從可運行隊列移出,放入等待隊列,而後調用schedule()選擇和運行一個其它進程。喚醒的過程恰好相反:進程被設置爲可運行狀態,而後再從等待隊列中移到可運行隊列。

休眠有兩種相關的進程狀態:TASK_INTERRUPTIBLETASK_UNINTERRUPTIBLE。休眠經過等待隊列進行處理。等待隊列是由等待某些事件發生的進程組成的簡單鏈表。內核用wake_queue_head_t來表明等待隊列。等待隊列可以經過DECLARE_WAITQUEUE()靜態建立,也可以由init_waitqueue_head()動態建立。喚醒操做經過函數wake_up()進行,它會喚醒指定的等待隊列上的所有進程。

負載平衡

         Linux的調度程序爲堆成多處理系統的每個處理器準備了單獨的可運行隊列和鎖。爲了使各個可運行隊列上的負載平衡,提供了負載平衡程序。假設它發現了不平衡,就會把相抵繁忙的隊列中的進程抽到當前的可自行隊列中來。

負載平衡程序有kernel/sched.c中的函數load_balance()來實現。它有兩種調用方法。在schedule()運行的時候,僅僅要當前的可運行隊列爲空,它就會被調用。此外,它還會被定時器調用:系統空暇時每隔1毫秒調用一次或者在其它狀況下每隔200毫秒調用一次。負載平衡程序調用時需要鎖住當前處理器的可運行隊列並且屏蔽中斷,以免可運行隊列被併發地訪問。

搶佔和上下文切換

上下文切換,也就是從一個可運行進程切換到還有一個可運行進程。進程切換schedule函數調用context_switch()函數完畢下面工做:

1.調用定義在<asm/mmu_context.h>中的switch_mm(),該函數負責把虛擬內存從上一個進程映射切換到新進程中。

2.調用定義在<asm/system.h>中的switch_to(),該函數負責從上一個進程的處理器狀態切換到新進程的處理器狀態。這包含保存、恢復棧信息和寄存器信息。

前面看到schedule函數調用有很是多種狀況,全然依靠用戶來調用不能達到很是好的效果。內核需要推斷何時調用schedule,內核提供了一個need_resched標誌來代表是否需要又一次運行一次調度:

1當某個進程耗盡它的時間片時,scheduler_tick()就會設置這個標誌;

2當一個優先級高的進程進入可運行狀態的時候,try_to_wake_up()也會設置這個標誌。

每個進程都包括一個need_resched標誌,這是因爲訪問進程描寫敘述符內的數值要比訪問一個全局變量快

用戶搶佔

內核即將返回用戶空間時候,假設need_resched標誌被設置,會致使schedule函數被調用,此時發生用戶搶佔。

用戶搶佔在下面狀況時產生:

1.從系統調返回用戶空間。

2.從中斷處理程序返回用戶空間。

內核搶佔

僅僅要又一次調度是安全的,那麼內核就可以在不論什麼時間搶佔正在運行的任務。

何時又一次調度纔是安全的呢?僅僅要沒有持有鎖,內核就可以進行搶佔。

鎖是非搶佔區域的標誌。由於內核是支持SMP的,因此,假設沒有持有鎖,那麼正在運行的代碼就是可又一次導入的,也就是可以搶佔的。

內核搶佔會發生在:

1.當從中斷處理程序正在運行,且返回內核空間以前。

2.當內核代碼再一次具備可搶佔性的時候。

3.假設內核中的任務顯式的調用schedule()。

4.假設內核中的任務堵塞(這相同也會致使調用schedule())。

 

參考

http://www.cnblogs.com/pennant/archive/2012/12/17/2818922.html

http://www.cnblogs.com/wang_yb/archive/2012/09/04/2670564.html

http://blog.csdn.net/cxf100900/article/details/5775252

Linux內核設計與實現

相關文章
相關標籤/搜索