linux內核學習之進程管理(待續)

一基本概念

1 進程和線程

進程就是正在執行的程序代碼的實時結果,包括可執行程序代碼(代碼段),內存地址空間、數據段、打開的文件等資源。即進程是處於執行期的程序以及相關的資源的總稱。

線程:是進程中活動的對象,擁有獨立的程序計數器、進程棧,進程寄存器。內核調度的對象(最小單位)是線程,而非進程。

Linux內核中也將進程稱爲任務(task,且將線程看做一種特殊的進程(沒有獨立的地址空間而已)。進程是資源分配管理的最小單元,而線程是程序執行(調度)的最小單元。

Linux中使用多線程的好處有

 創建線程的開銷遠遠小於進程的創建(必須給新的進程分配獨立的地址空間),而且線程間切換所需的時間也遠小於進程切換需要的時間。

線程間通信方便,不同的進程(分別擁有獨立的地址空間)之間數據的傳遞需要通過IPC方式,費時且不方便。而同一個進程下的線程之間共享數據空間,線程間數據的通信使用方便。

2 進程描述符

任務隊列(內核中存放進程列表的一個雙向循環鏈表)的(每一項都是)類型爲task_struct的結構,其包含了一個具體進程的所有信息。

進程標識值PID一個int類型的數,是進程的唯一標識。所有的進程都是PID爲1的init進程的後臺,內核在系統啓動的最後階段啓動init進程

3.進程上下文  中斷上下文

 處理器總處於以下狀態中的一種: 

1、內核態,運行於進程上下文,內核代表進程運行於內核空間;

2、內核態,運行於中斷上下文,內核代表硬件運行於內核空間;

3、用戶態,運行於用戶空間。 

用戶空間的應用程序,通過系統調用,進入內核空間。這個時候用戶空間的進程要傳遞很多變量、參數的值給內核,內核態運行的時候也要保存用戶進程的一些寄存器值、變量等。所謂的“進程上下文”,可以看作是用戶進程傳遞給內核的這些參數以及內核要保存的那一整套的變量和寄存器值和當時的環境等。 

硬件通過觸發信號,導致內核調用中斷處理程序,進入內核空間。這個過程中,硬件的一些變量和參數也要傳遞給內核,內核通過這些參數進行中斷處理。所謂的“中斷上下文”,其實也可以看作就是硬件傳遞過來的這些參數和內核需要保存的一些其他環境(主要是當前被打斷執行的進程環境)。   

 Linux內核工作在進程上下文或者中斷上下文。提供系統調用服務的內核代碼代表發起系統調用的應用程序運行在進程上下文;另一方面,中斷處理程序,異步運行在中斷上下文。中斷上下文和特定進程無關。              

運行在進程上下文的內核代碼是可以被搶佔的(Linux2.6支持搶佔)。但是一箇中斷上下文,通常都會始終佔有CPU(當然中斷可以嵌套,但我們一般不這樣做),不可以被打斷。正因爲如此,運行在中斷上下文的代碼就要受一些限制,不能做下面的事情:

 1、睡眠或者放棄CPU。  這樣做的後果是災難性的,因爲內核在進入中斷之前會關閉進程調度,一旦睡眠或者放棄CPU,這時內核無法調度別的進程來執行,系統就會死掉

 2、嘗試獲得信號量 如果獲得不到信號量,代碼就會睡眠,會產生和上面相同的情況 

3、執行耗時的任務中斷處理應該儘可能快,因爲內核要響應大量服務和請求,中斷上下文佔用CPU時間太長會嚴重影響系統功能。

 4、訪問用戶空間的虛擬地址 因爲中斷上下文是和特定進程無關的,它是內核代表硬件運行在內核空間,所以在中端上下文無法訪問用戶空間的虛擬地址

二 進程創建

1 fork   exec

Linux將進程的創建分爲兩個單獨的函數中:forkexec

fork通過copy當前進程創建一個子進程(子進程與父進程僅僅PID 以及一些統計量(掛起的信號)不同),exec負責讀取可執行文件並將其載入地址空間開始執行。

2fork函數的寫時拷貝

Fork調用時,內核此時並不複製整個進程地址空間,而是讓父進程和子進程共享一個拷貝。只有在需要寫入的時候,數據才被複制,從而使各個進程擁有各自的拷貝,例如在fork後立即調用exec它們就無需複製了。Fork的實際開銷就是複製父進程的頁表和爲子進程創建唯一的進程描述符。所以UNIX下進程的創建非常迅速。Linux通過clone系統調用實現fork(子進程首先執行)。

3 vfork

與fork唯一不同時不拷貝父進程的頁表項,不建議使用。

三 線程機制

Linux內核中線程被視爲一個與其他進程共享某些資源的進程,每個線程擁有唯一隸屬於自己的task_struct,也稱爲輕量級進程。這種方式與windows的線程實現機制不同。

內核線程(kernel thread)是由內核自己創建的線程,也叫做守護線程(deamon)。在終端上用命令"ps -Al"列出的所有進程中,名字以k開關以d結尾的往往都是內核線程,比如kthreadd、kswapd。

內核線程與用戶線程的相同點是:

都由do_fork()創建,每個線程都有獨立的task_struct和內核棧;

都參與調度,內核線程也有優先級,會被調度器平等地換入換出。

不同之處在於:

內核線程只工作在內核態中;

而用戶線程則既可以運行在內核態,也可以運行在用戶態;

內核線程沒有用戶空間,所以對於一個內核線程來說,它的0~3G的內存空間是空白的,它的current->mm是空的,與內核使用同一張頁表;而用戶線程則可以看到完整的0~4G內存空間。(linux中, 將最高的1G字節(從虛擬地址0xC00000000xFFFFFFFF),供內核使用,稱爲內核空間。而將較低的3G字節(從虛擬地址 0x000000000xBFFFFFFF),供各個進程使用,稱爲用戶空間)

四 進程調度

4.1 目的

負責決定將那個進程投入運行,何時運行以及運行多長時間,調度程序是在可運行態進程之間分配有限的處理器資源的內核子系統。

進程調度 基本策略: 分時的調度 必須要 時鐘中斷,  優先級搶佔的 不需要 時鐘中斷。所以分時進程調度需要時鐘中斷的參與,linux中具體是scheduler_tick函數(主要用於更新時間片)。

4.2 基本概念

(進程的)時間片:分配給每個可運行進程的處理器時間段

CFS:linux 2.6.23後 版本使用的調度算法,稱爲完全公平調度算法,取代了之前的O(1)調度算法。

I/O消耗型進程:指進程的大部分時間用來提交I/O請求或者是等待I/O請求。

處理器耗費型進程:時間大多用在執行代碼上。

Linux更傾向於有限調度I/O消耗型進程。

進程優先級:linux採用了2種:用nice值和實時優先級。

Nice值:範圍是-20到19,默認值爲0,值越大表示優先級越低,低nice值的進程可以獲得更多的處理器時間。

實時優先級:默認範圍是從0到99,數值越高意味着進程優先級越高.

PS:進程的2種優先級會讓人不好理解,到底哪個優先級更優先?一個進程同時有2種優先級怎麼辦?

其實linux的內核早就有了解決辦法。

對於第一個問題,到底哪個優先級更優先?

答案是實時優先級高於nice值,在內核中,實時優先級的範圍是 0~MAX_RT_PRIO-1 MAX_RT_PRIO的定義參見 include/linux/sched.h

1611 #define MAX_USER_RT_PRIO        100
1612 #define MAX_RT_PRIO             MAX_USER_RT_PRIO

nice值在內核中的範圍是 MAX_RT_PRIO~MAX_RT_PRIO+40 即 MAX_RT_PRIO~MAX_PRIO

1614 #define MAX_PRIO                (MAX_RT_PRIO + 40)

第二個問題,一個進程同時有2種優先級怎麼辦?

答案很簡單,就是一個進程不可能有2個優先級。一個進程有了實時優先級就沒有Nice值,有了Nice值就沒有實時優先級。

我們可以通過以下命令查看進程的實時優先級和Nice值:(其中RTPRIO是實時優先級,NI是Nice值)

$ ps -eo state,uid,pid,ppid,rtprio,ni,time,comm
S   UID   PID  PPID RTPRIO  NI     TIME COMMAND
S     0     1     0      -   0 00:00:00 systemd
S     0     2     0      -   0 00:00:00 kthreadd
S     0     3     2      -   0 00:00:00 ksoftirqd/0
S     0     6     2     99   - 00:00:00 migration/0
S     0     7     2     99   - 00:00:00 watchdog/0
S     0     8     2     99   - 00:00:00 migration/1
S     0    10     2      -   0 00:00:00 ksoftirqd/1
S     0    12     2     99   - 00:00:00 watchdog/1
S     0    13     2     99   - 00:00:00 migration/2
S     0    15     2      -   0 00:00:00 ksoftirqd/2
S     0    16     2     99   - 00:00:00 watchdog/2
S     0    17     2     99   - 00:00:00 migration/3
S     0    19     2      -   0 00:00:00 ksoftirqd/3
S     0    20     2     99   - 00:00:00 watchdog/3
S     0    21     2      - -20 00:00:00 cpuset
S     0    22     2      - -20 00:00:00 khelper

4.3 CFS 調度器(參考linux內核設計與實現一書中第四章)

CFS 完全公平調度是一個針對普通進程的調度類,在linux 中稱爲SCHED_NORMAL;在POSIX中稱爲SCHED_OTHER..

調度實體(sched entiy):就是調度的對象,可以理解爲進程。
虛擬運行時間(vruntime):即每個調度實體的運行時間。
公平調度隊列(cfs_rq):採取公平調度的調度實體的運行隊列。

Linux系統是搶佔式的,是否將一個進程投入運行(搶佔當前進程),是完全由實時進程優先級和是否有時間片決定的,而linux使用CFS調度器,其搶佔時機取決於新的可執行消耗了多少處理器使用比。如果消耗的使用比比當前進程小,則新進程立刻投入運行,即搶佔當前進程,否則將推遲其運行。

CFS在所有可執行進程總數基礎上計算一個進程應該運行多久,而不是依靠nice值計算時間片,nice值在cfs中被用作進程獲得的處理區運行比的權重,更低的nice值的進程獲得更高的處理器使用權重。(只有相對的nice值纔會影響處理器時間的分配比例)。在CFS中不再有時間片的概念。

目標延遲:最小粒度默認爲1ms。

任何進程獲得的處理器時間是有它自己和所有其他可運行線程nice值的相對差值決定的。Nice值對時間片的作用是幾何加權(非算數加權)。

調度的實現:

cfs之前的linux調度器一般使用用戶設定的靜態優先級,加上對於進程交互性的判斷來生成動態優先級,再根據動態優先級決定進程被調度的順序,以及調度後可以運行的時間片。反過來,隨着進程的運行,內核可能發現其交互性發生改變,從而調整其動態優先級(獎勵睡眠多的交互式進程、懲罰睡眠少的批處理進程)。
cfs原理
cfs定義了一種新的模型,它給cfs_rq(cfs的run queue)中的每一個進程安排一個虛擬時鐘,vruntime。如果一個進程得以執行,隨着時間的增長(也就是一個個tick的到來),其vruntime將不斷增大。沒有得到執行的進程vruntime不變。而調度器總是選擇vruntime跑得最慢(最小)的那個進程來執行。這就是所謂的“完全公平”。
爲了區別不同優先級的進程,優先級高的進程vruntime增長得慢,以至於它可能得到更多的運行機會。

進程調度的主要入口函數是schedule():選擇那個進程可以運行,何時將其投入運行。每次定時器中斷調用的最重要的更新時間片的函數 —— scheduler_tick函數。(當每次時鐘節拍到來時(定時器產生中斷,OS時間中斷處理程序),即我們提到過的timer_interrupt會調用do_timer_interrupt_hook,從而調用do_timer和update_process_times函數,update_process_times則就是用來更新進程使用到的一些跟時間相關的字段,其最重要的是調用scheduler_tick()更新時間片剩餘節拍數:)PS:該函數內部會對實時進程和普通進程分別處理,更新它們的時間片。

PS: 1 每個進程的weight值是如何確定的呢?
上面談到公平的依據,CFS的公平依據就是每個調度實體的權重(weight),這個權重是有優先級來決定的,即優先級越高權重越高,linux內核採用了nice-prio-weight的一個轉換關係來實現了每個調度實體權重的確定。我們來回顧,進程被創建的時候他的優先級是繼承自父進程的,如果想改變有優先級,linux內核提供了幾個系統調用來改變進程的nice值,從而改變權重,如sys_nice()系統調用,下面來看一下他們之間的轉換關係:

#define NICE_TO_PRIO(nice)(MAX_RT_PRIO + (nice) + 20)
#define PRIO_TO_NICE(prio) ((prio) - MAX_RT_PRIO - 20)
#define TASK_NICE(p) PRIO_TO_NICE((p)->static_prio)

其中,MAX_RT_PRIO=100,nice的值在-20到19之前,那麼優先級就在100 - 139之間。

static const int prio_to_weight[40] = {
 /* -20 */     88761,     71755,     56483,     46273,     36291,
 /* -15 */     29154,     23254,     18705,     14949,     11916,
 /* -10 */      9548,      7620,      6100,      4904,      3906,
 /*  -5 */      3121,      2501,      1991,      1586,      1277,
 /*   0 */      1024,       820,       655,       526,       423,
 /*   5 */       335,       272,       215,       172,       137,
 /*  10 */       110,        87,        70,        56,        45,
 /*  15 */        36,        29,        23,        18,        15,
};

2 基於這些weight,CFS又是怎麼來體現公平的呢?

   CFS可實現幾種不同的公平策略,這些策略是根據調度的對象的不同來區分的。

默認的是不開組調度的公平策略,即調度的單位是每個調度實體。我們來詳細看一下是怎麼調度的:

   假設現在系統有A,B,C三個進程,A.weight=1,B.weight=2,C.weight=3.那麼我們可以計算出整個公平調度隊列的總權重是cfs_rq.weight = 6,很自然的想法就是,公平就是你在重量中佔的比重的多少來拍你的重要性,那麼,A的重要性就是1/6,同理,B和C的重要性分別是2/6,3/6.很顯然C最重要就應改被先調度,而且佔用的資源也應該最多,即假設A,B,C運行一遍的總時間假設是6個時間單位的話,A佔1個單位,B佔2個單位,C佔三個單位。這就是CFS的公平策略。

   linux內核採用了計算公式:

ideal_time = sum_runtime * se.weight/cfs_rq.weight
ideal_time:每個進程應該運行的時間
sum_runtime:運行隊列中所有任務運行完一遍的時間
se.weight:當前進程的權重
cfs.weight:整個cfs_rq的總權重

這裏se.weight和cfs.weight根據上面講解我們可以算出,sum_runtime是怎們計算的呢,linux內核中這是個經驗值,其經驗公式是:
  (1) sum_runtime=sysctl_sched_min_granularity * nr_running(if 進程數 > 5)
(2) sum_runtime=sysctl_sched_latency = 20 ms           (if 進程數 <= 5)
注:sysctl_sched_min_granularity =4ms
linux內核代碼中是通過一個叫vruntime的變量來實現上面的原理的。

4.4 linux調度策略

調度程序運行時,要在所有可運行狀態的進程中選擇最值得運行的進程投入運行。選擇進程的依據是什麼呢?在每個進程的task_struct結構中有以下四項:policy、priority、counter、rt_priority。這四項是選擇進程的依據。其中,policy是進程的調度策略,用來區分實時進程和普通進程,實時進程優先於普通進程運行;priority是進程(包括實時和普通)的靜態優先級;counter是進程剩餘的時間片,它的起始值就是priority的值;由於counter在後面計算一個處於可運行狀態的進程值得運行的程度goodness時起重要作用,因此,counter也可以看作是進程的動態優先級。rt_priority是實時進程特有的,用於實時進程間的選擇。
調度方法:
1,SCHED_OTHER 分時調度策略,
2,SCHED_FIFO實時調度策略,先到先服務
3,SCHED_RR實時調度策略,時間片輪轉
實時進程將得到優先調用,實時進程根據實時優先級決定調度權值,分時進程則通過nice和counter值決定權值,nice越小,counter越大,被調度的概率越大,也就是曾經使用了cpu最少的進程將會得到優先調度。
SHCED_RR和SCHED_FIFO的不同:
當採用SHCED_RR策略的進程的時間片用完,系統將重新分配時間片,並置於就緒隊列尾。放在隊列尾保證了所有具有相同優先級的RR任務的調度公平。
SCHED_FIFO一旦佔用cpu則一直運行。一直運行直到有更高優先級任務到達或自己放棄。
如果有相同優先級的實時進程(根據優先級計算的調度權值是一樣的)已經準備好,FIFO時必須等待該進程主動放棄後纔可以運行這個優先級相同的任務。而RR可以讓每個任務都執行一段時間。
相同點:
RR和FIFO都只用於實時任務。
創建時優先級大於0(1-99)。
按照可搶佔優先級調度算法進行。
就緒態的實時任務立即搶佔非實時任務。
       策略:
調度程序運行時,要在所有處於可運行狀態的進程之中選擇最值得運行的進程投入運行。選擇進程的依據是什麼呢?在每個進程的task_struct 結構中有這麼四項:
policy, priority , counter, rt_priority
這四項就是調度程序選擇進程的依據.其中,policy是進程的調度策略,用來區分兩種進程-實時和普通;priority是進程(實時和普通)的優先級;counter 是進程剩餘的時間片,它的大小完全由priority決定;rt_priority是實時優先級,這是實時進程所特有的,用於實時進程間的選擇。
首先,Linux 根據policy從整體上區分實時進程和普通進程,因爲實時進程和普通進程度調度是不同的,它們兩者之間,實時進程應該先於普通進程而運行,然後,對於同一類型的不同進程,採用不同的標準來選擇進程:
對於普通進程,Linux採用動態優先調度,選擇進程的依據就是進程counter的大小。進程創建時,優先級priority被賦一個初值,一般爲0~70之間的數字,這個數字同時也是計數器counter的初值,就是說進程創建時兩者是相等的。字面上看,priority是“優先級”、counter是“計數器”的意思,然而實際上,它們表達的是同一個意思-進程的“時間片”。Priority代表分配給該進程的時間片,counter表示該進程剩餘的時間片。在進程運行過程中,counter不斷減少,而priority保持不變,以便在counter變爲0的時候(該進程用完了所分配的時間片)對counter重新賦值。當一個普通進程的時間片用完以後,並不馬上用priority對counter進行賦值,只有所有處於可運行狀態的普通進程的時間片(p->;;counter==0)都用完了以後,才用priority對counter重新賦值,這個普通進程纔有了再次被調度的機會。這說明,普通進程運行過程中,counter的減小給了其它進程得以運行的機會,直至counter減爲0時才完全放棄對CPU的使用,這就相對於優先級在動態變化,所以稱之爲動態優先調度。至於時間片這個概念,和其他不同操作系統一樣的,Linux的時間單位也是“時鐘滴答”,只是不同操作系統對一個時鐘滴答的定義不同而已(Linux爲10ms)。進程的時間片就是指多少個時鐘滴答,比如,若priority爲20,則分配給該進程的時間片就爲20個時鐘滴答,也就是20*10ms=200ms。Linux中某個進程的調度策略(policy)、優先級(priority)等可以作爲參數由用戶自己決定,具有相當的靈活性。內核創建新進程時分配給進程的時間片缺省爲200ms(更準確的,應爲210ms),用戶可以通過系統調用改變它。
對於實時進程,Linux採用了兩種調度策略,即FIFO(先來先服務調度)和RR(時間片輪轉調度)。因爲實時進程具有一定程度的緊迫性,所以衡量一個實時進程是否應該運行,Linux採用了一個比較固定的標準。實時進程的counter只是用來表示該進程的剩餘時間片,並不作爲衡量它是否值得運行的標準,這和普通進程是有區別的。上面已經看到,每個進程有兩個優先級,實時優先級就是用來衡量實時進程是否值得運行的.
當policy分別爲以下值時:
1) SCHED_OTHER:這是普通的用戶進程,進程的缺省類型,採用動態優先調度策略,選擇進程的依據主要是根據進程goodness值的大小。這種進程在運行時,可以被高goodness值的進程搶先。
2) SCHED_FIFO:這是一種實時進程,遵守POSIX1.b標準的FIFO(先入先出)調度規則。它會一直運行,直到有一個進程因I/O阻塞,或者主動釋放CPU,或者是CPU被另一個具有更高rt_priority的實時進程搶先。在Linux實現中,SCHED_FIFO進程仍然擁有時間片-只有當時間片用完時它們才被迫釋放CPU。因此,如同POSIX1.b一樣,這樣的進程就象沒有時間片(不是採用分時)一樣運行。Linux中進程仍然保持對其時間片的記錄(不修改counter)主要是爲了實現的方便,同時避免在調度代碼的關鍵路徑上出現條件判斷語句 if (!(current->;;policy&;;SCHED_FIFO)){...}-要知道,其他大量非FIFO進程都需要記錄時間片,這種多餘的檢測只會浪費CPU資源。(一種優化措施,不該將執行時間佔10%的代碼的運行時間減少到50%;而是將執行時間佔90%的代碼的運行時間減少到95%。0.9+0.1*0.5=0.95>;;0.1+0.9*0.9=0.91)
3) SCHED_RR:這也是一種實時進程,遵守POSIX1.b標準的RR(循環round-robin)調度規則。除了時間片有些不同外,這種策略與SCHED_FIFO類似。當SCHED_RR進程的時間片用完後,就被放到SCHED_FIFO和SCHED_RR隊列的末尾。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章