一基本概念
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將進程的創建分爲兩個單獨的函數中:fork和exec。
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字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱爲“內核空間”。而將較低的3G字節(從虛擬地址 0x00000000到0xBFFFFFFF),供各個進程使用,稱爲“用戶空間))
四 進程調度
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.weightideal_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調度策略