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

來源:互聯網
上載者:User

標籤:des   style   blog   http   color   使用   io   檔案   

進程的管理與調度
進程管理
進程描述符及任務結構

    進程存放在叫做任務隊列(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做它們的父進程。

進程調度什麼是調度

現在的作業系統都是多任務的,為了能讓更多的任務能同時在系統上更好的運行,需要一個管理程式來管理電腦上同時啟動並執行各個任務(也就是進程)。

這個管理程式就是發送器,它的功能說起來很簡單:

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);重算時必須考鎖的形式來保護任務隊列和每個進程描述符,這樣做會加劇對鎖的爭用;重新計算時間的實際不確定。

活躍數組內的可執行隊列上的進程都還有時間片剩餘,而到期數組內的都耗盡了時間片。當一個進程的時間片耗盡時,它會被移至到期數組,但在此之前,時間片已經給它重新計算好。重新計算時間片變得非常簡單,只要在活躍和到期數組之間來回切換,這是O(1)級發送器的核心。

schedule()

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

首先要在活動優先順序數組中找到第一個被設定的位,該位對於這優先順序最高的可執行進程。然後,發送器選擇這個層級鏈表裡的有一個進程。這就是系統中優先順序最高的可執行程式。如果被選中的進程不是當前進程,就進行環境切換。

計算優先順序和時間片

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

        怎麼通過一些推斷來擷取準確反映進程到底是I/O消耗型的還是處理器消耗型的。最明顯的標準莫過於進程休眠的時間長短了。如果一個進程的大部分時間都在休眠,那麼它就是I/O消耗型的。如果一個進程執行的時間比休眠的時間長,那它就是處理器消耗型的。

        另一方面,重新計算時間片相對簡單了。它只要以靜態優先順序為基礎就可以了。在一個進程建立的時候,建立的子進程和父進程均分父進程剩餘的進程時間片。這樣的分配很公平並且防止使用者通過不斷建立新進程來不停地擷取時間片。task_timeslice()函數為給定任務返回一個新的時間片。時間片的計算只需要把優先順序按比例縮放,使其符合時間片的數值範圍要求就可以了。進程的靜態優先順序越高,它每次執行得到的時間片就越長。

發送器還提供了另外一種機制以支援互動進程:如果一個進程的互動性非常強,那麼當它時間片用完後,它會被放置到活動數組而不是到期數組中。

睡眠與喚醒

       休眠(被阻塞)的進程處於一個特殊的不可執行狀態。進程把它自已標幟成休眠狀態,把自己從可執行隊列移出,放入等待隊列,然後調用schedule()選擇和執行一個其他進程。喚醒的過程剛好相反:進程被設定為可執行狀態,然後再從等待隊列中移到可執行隊列。

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

Server Load Balancer

         Linux的發送器為堆成多處理系統的每個處理器準備了單獨的可執行隊列和鎖。為了使各個可執行隊列上的Server Load Balancer,提供了Server Load Balancer程式。如果它發現了不平衡,就會把相抵繁忙的隊列中的進程抽到當前的可自行隊列中來。

Server Load Balancer程式有kernel/sched.c中的函數load_balance()來實現。它有兩種調用方法。在schedule()執行的時候,只要當前的可執行隊列為空白,它就會被調用。此外,它還會被定時器調用:系統空閑時每隔1毫秒調用一次或者在其他情況下每隔200毫秒調用一次。Server Load Balancer程式調用時需要鎖住當前處理器的可執行隊列並且屏蔽中斷,以避免可執行隊列被並發地訪問。

搶佔和環境切換

環境切換,也就是從一個可執行進程切換到另一個可執行進程。進程切換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核心設計與實現

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.