進程的管理與調度

進程管理


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

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

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

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


發佈了3 篇原創文章 · 獲贊 12 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章