深入理解Linux內核進程的創建、調度和終止
在Linux 操作系統的神秘世界里,進程就像是一個個充滿活力的小生命,它們不斷地誕生、成長、工作,最終走向終止。你可以把 Linux 內核想象成一個龐大而有序的城市,進程則是城市里形形色色的居民和組織,它們有著各自的任務和使命,在這個城市里穿梭忙碌。
進程的創建,如同新生命的誕生,為系統帶來了新的活力和任務;進程的調度,就像是城市交通的指揮者,合理地分配時間和資源,確保每個進程都能有序地運行;而進程的終止,則是生命的落幕,釋放出資源,讓系統得以持續高效地運轉。今天,就讓我們一起深入 Linux 內核進程的世界,揭開它們創建、調度和終止的神秘面紗,探尋其中的奧秘和樂趣 。
一、Linux進程的概念
進程(Process)是指計算機中已運行的程序,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。在早期面向進程設計的計算機結構中,進程是程序的基本執行實體;在當代面向線程設計的計算機結構中,進程是線程的容器。進程是程序真正運行的實例,若干進程可能與同一個程序相關,且每個進程皆可以同步或異步的方式獨立運行。
- 狹義定義:進程是正在運行的程序的實例。
- 廣義定義:進程是一個具有一定獨立功能的程序關于某個數據集合的一次運行活動。它是操作系統動態執行的基本單元,在傳統的操作系統中,進程既是基本的分配單元,也是基本的執行單元。
進程的概念主要有兩點:第一,進程是一個實體。每一個進程都有它自己的地址空間,一般情況下,包括文本區域(text region)、數據區域(data region)和堆棧(stack region)。文本區域存儲處理器執行的代碼;數據區域存儲變量和進程執行期間使用的動態分配的內存;堆棧區域存儲著活動過程調用的指令和本地變量。第二,進程是一個“執行中的程序”。程序是一個沒有生命的實體,只有處理器賦予程序生命時(操作系統執行之),它才能成為一個活動的實體,我們稱其為進程。
1.1描述進程PCB
進程:資源的封裝單位,linux用一個PCB來描述進程,即task_struct, 其包含mm,fs,files,signal…
(1)root目錄,是一個進程概念,不是系統概念
apropos chroot
man chroot 2將分區/dev/sda5掛載到/mnt/a,調用chroot,改變root目錄,當前進程下的文件b.txt即位于當前進程的根目錄。
(2)fd也是進程級概念
(base) leon@leon-Laptop:/proc/29171$ ls fd -l總用量 0
lrwx------ 1 leon leon 64 5月 16 10:26 0 -> /dev/pts/19
lrwx------ 1 leon leon 64 5月 16 10:26 1 -> /dev/pts/19
lrwx------ 1 leon leon 64 5月 16 10:26 2 -> /dev/pts/19(3)pid,系統全局概念
Linux總的PID是有限的,用完PID
: ( ) { : ∣ : & } ; : :()\{:|:\&\};::(){:∣:&};:每個用戶的PID也是有限的
- ulimit -u 最大進程數
- ulimit –a
(base) leon@leon-Laptop:/proc/29171$ cat /proc/sys/kernel/pid_max1.2 task_ struct內容分類
在進程執行時,任意給定一個時間,進程都可以唯一的被表征為以下元素:
- 標示符: 描述本進程的唯一標示符,?用來區別其他進程。
- 狀態: 任務狀態,退出代碼,退出信號等。
- 優先級: 相對于其他進程的優先級。
- 程序計數器: 程序中即將被執行的下一條指令的地址。
- 內存指針: 包括程序代碼和進程相關數據的指針,還有和其他進程共享的內存塊的指針
- 上下文數據: 進程執行時處理器的寄存器中的數據
- I/O狀態信息: 包括顯?示的I/O請求,分配給進程的I/O設備和被進程使用的文件列表。
- 記賬信息: 可能包括處理器時間總和,使用的時鐘數總和,時間限制,記賬號等。
1.3Linux進程的組織方式
linux里的多個進程,其實就是管理多個task_struct,那他們是怎么組織聯系的呢?
組織task_struct的數據結構:
- a.鏈表,遍歷進程
- b.樹:方便查找父子相關進程
- c.哈希表:用于快速查找
用三種數據結構來管理task_struct,以空間換時間。父進程監控子進程,linux總是白發人送黑發人。父進程通過wait,讀取task_struct的退出碼,得知進程死亡原因。并且清理子進程尸體。
Android/或者服務器,都會用由父進程監控子進程狀態,適時重啟等;
1.4進程的狀態和轉換
(1)五種狀態
進程在其生命周期內,由于系統中各進程之間的相互制約關系及系統的運行環境的變化,使得進程的狀態也在不斷地發生變化(一個進程會經歷若干種不同狀態)。通常進程有以下五種狀態,前三種是進程的基本狀態。
- 運行狀態:進程正在處理機上運行。在單處理機環境下,每一時刻最多只有一個進程處于運行狀態。
- 就緒狀態:進程已處于準備運行的狀態,即進程獲得了除處理機之外的一切所需資源,一旦得到處理機即可運行。
- 阻塞狀態,又稱等待狀態:進程正在等待某一事件而暫停運行,如等待某資源為可用(不包括處理機)或等待輸入/輸出完成。即使處理機空閑,該進程也不能運行。
- 創建狀態:進程正在被創建,尚未轉到就緒狀態。創建進程通常需要多個步驟:首先申請一個空白的PCB,并向PCB中填寫一些控制和管理進程的信息;然后由系統為該進程分 配運行時所必需的資源;最后把該進程轉入到就緒狀態。
- 結束狀態:進程正從系統中消失,這可能是進程正常結束或其他原因中斷退出運行。當進程需要結束運行時,系統首先必須置該進程為結束狀態,然后再進一步處理資源釋放和 回收等工作。
注意區別就緒狀態和等待狀態:就緒狀態是指進程僅缺少處理機,只要獲得處理機資源就立即執行;而等待狀態是指進程需要其他資源(除了處理機)或等待某一事件。之所以把處理機和其他資源劃分開,是因為在分時系統的時間片輪轉機制中,每個進程分到的時間片是若干毫秒。
也就是說,進程得到處理機的時間很短且非常頻繁,進程在運行過程中實際上是頻繁地轉換到就緒狀態的;而其他資源(如外設)的使用和分配或者某一事件的發生(如I/O操作的完成)對應的時間相對來說很長,進程轉換到等待狀態的次數也相對較少。這樣來看,就緒狀態和等待狀態是進程生命周期中兩個完全不同的狀態,很顯然需要加以區分。
(2)狀態轉換
- 就緒狀態 -> 運行狀態:處于就緒狀態的進程被調度后,獲得處理機資源(分派處理機時間片),于是進程由就緒狀態轉換為運行狀態。
- 運行狀態 -> 就緒狀態:處于運行狀態的進程在時間片用完后,不得不讓出處理機,從而進程由運行狀態轉換為就緒狀態。此外,在可剝奪的操作系統中,當有更高優先級的進程就 、 緒時,調度程度將正執行的進程轉換為就緒狀態,讓更高優先級的進程執行。
- 運行狀態 -> 阻塞狀態:當進程請求某一資源(如外設)的使用和分配或等待某一事件的發生(如I/O操作的完成)時,它就從運行狀態轉換為阻塞狀態。進程以系統調用的形式請求操作系統提供服務,這是一種特殊的、由運行用戶態程序調用操作系統內核過程的形式。
- 阻塞狀態 -> 就緒狀態:當進程等待的事件到來時 ,如I/O操作結束或中斷結束時,中斷處理程序必須把相應進程的狀態由阻塞狀態轉換為就緒狀態。
二、Linux進程誕生
2.1進程創建方式
在 Linux 系統中,主要有三種方式可以創建新的進程,分別是fork、vfork和clone系統調用 。這三種方式就像是三把不同的鑰匙,雖然都能打開進程創建的大門,但各自有著獨特的開啟方式和適用場景。
- fork函數是最常用的創建進程的方式,它就像是一個復印機,創建出的子進程幾乎是父進程的完整副本,擁有自己獨立的地址空間、堆棧和數據副本。這種方式適用于需要獨立運行的子進程,比如一個 Web 服務器程序,當有新的客戶端連接時,就可以通過fork創建一個新的子進程來處理該客戶端的請求 。
- vfork函數則像是一個特殊的克隆工具,創建的子進程與父進程共享地址空間,并且保證子進程先運行,直到子進程調用exec或exit函數后,父進程才會繼續執行。這種方式適用于子進程需要立即執行另一個程序的場景,比如在啟動一個新的應用程序時,就可以使用vfork來創建子進程,然后在子進程中調用exec函數來加載新的應用程序。
- clone函數則更加靈活,它可以根據用戶的需求,選擇性地繼承父進程的部分資源,比如可以共享文件描述符、信號處理函數等。這種方式就像是一個定制化的工廠,可以根據不同的需求生產出不同類型的進程,適用于創建線程或者需要精細控制資源共享的場景 。
fork創建一個新進程,也需要創建task_struct所有資源;實際上創建一個新進程之初,子進程完全拷貝父進程資源,如下圖示:
比如fs結構體:
struct fs_struct {
* root
* pwd
};子進程會拷貝一份fs_struct,
*p2_fs = *p1_fs;pwd路徑和root路徑與父進程相同,子進程修改當前路徑,就會修改其p2_fs->pwd值;父進程修改當前路徑,修改p1_fs->pwd;
2.2fork 函數原理
fork函數的原型非常簡潔:pid_t fork(void); 。這個函數就像是一個神奇的開關,當它被調用時,會在操作系統中引發一系列奇妙的變化。它會創建一個新的進程,這個新進程就是子進程,而調用fork的進程則是父進程。
從實現原理上看,fork函數會復制父進程的幾乎所有資源,包括虛擬地址空間、堆棧、打開的文件描述符等。在虛擬地址空間方面,父子進程各自擁有自己獨立的虛擬地址空間,但它們共享代碼段(因為代碼段通常是只讀的,不需要為每個進程單獨復制一份)。這就好比父子倆住在各自的房子里(虛擬地址空間),但他們共享同一個圖書館(代碼段) 。
在早期的 Unix 系統中,fork創建子進程時會直接復制父進程的整個地址空間,這會導致大量的內存拷貝操作,效率非常低下。后來引入了寫時拷貝(Copy-On-Write,COW)技術,大大提高了fork的效率。寫時拷貝技術的原理是,在fork創建子進程時,并不立即復制父進程的地址空間,而是讓父子進程共享相同的物理內存頁面。只有當其中一個進程試圖修改共享的內存頁面時,系統才會為修改的頁面創建一個副本,分別分配給父子進程。這就好比父子倆一開始共享同一本書(物理內存頁面),當其中一個人想要在書上做筆記(修改內存頁面)時,才會復制一本新的書給他 。
其他資源大體與fs類似,最復雜的是mm拷貝,需借助MMU來完成拷貝;
即寫時拷貝技術:
#include <sched.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int data = 10;
int child_process()
{
printf(“Child process %d, data %d\n”,getpid(),data);
data = 20;
printf(“Child process %d, data %d\n”,getpid(),data);
_exit(0);
}
int main(int argc, char* argv[])
{
int pid;
pid = fork();
if(pid==0) {
child_process();
}
else{
sleep(1);
printf(“Parent process %d, data %d\n”,getpid(), data);
exit(0);
}
return 0;
}寫時拷貝技術帶來了很多好處。首先,它節省了內存開銷,因為在大多數情況下,父子進程在fork之后并不會立即修改共享的內存,所以不需要一開始就復制大量的內存。其次,它提高了進程創建的效率,減少了fork操作的時間開銷 。
第一階段:只有一個進程P1,數據段可讀可寫:
第二階段,調用fork之后創建子進程P2,P2完全拷貝一份P1的mm_struct,其指針指向相同地址,即P1/P2虛擬地址,物理地址完全相同,但該內存的頁表地址變為只讀;
第三階段:當P2改寫data時,子進程改寫只讀內存,會引起內存缺頁中斷,在ISR中申請一片新內存,通常是4K,把P1進程的data拷貝到這4K新內存。再修改頁表,改變虛實地址轉換關系,使物理地址指向新申請的4K,這樣子進程P2就得到新的4K內存,并修改權限為可讀寫,然后從中斷返回到P2進程寫data才會成功。整個過程虛擬地址不變,對應用程序員來說,感覺不到地址變化。
誰先寫,誰申請新物理內存;Data=20;這句代碼經過了賦值無寫權限,引起缺頁中斷,申請內存,修改頁表,拷貝數據…回到data=20再次賦值,所以整個執行時間會很長。
這就是linux中的寫時拷貝技術(copy on write), 誰先寫誰申請新內存,沒有優先順序;cow依賴硬件MMU實現,沒有MMU的系統就沒法實現cow,也就不支持fork函數,只有vfork;
2.3vfork 函數原理
vfork函數的原型同樣簡單:pid_t vfork(void); 。它與fork函數有著明顯的區別。vfork創建的子進程與父進程共享地址空間,這意味著子進程完全運行在父進程的地址空間上,如果子進程修改了某個變量,那么父進程中的這個變量也會被改變。而且,vfork保證子進程先運行,在子進程調用exec或exit函數之前,父進程會被阻塞,處于暫停狀態 。
這種特性使得vfork適用于一些特定的場景,比如子進程需要立即執行另一個程序的情況。因為子進程共享父進程的地址空間,所以在創建子進程時不需要復制大量的內存,從而節省了時間和資源。例如,當我們需要啟動一個新的程序時,可以使用vfork創建子進程,然后在子進程中調用exec函數來加載新的程序,這樣可以快速地啟動新程序,而不會浪費過多的資源 。
不過,使用vfork也需要特別小心。由于子進程和父進程共享地址空間,如果子進程在調用exec或exit之前對共享的變量進行了修改,可能會影響到父進程的正常運行。所以,在使用vfork時,一定要確保子進程盡快調用exec或exit函數,以避免出現意想不到的問題 。
2.4clone 函數原理
clone函數的原型相對復雜一些:int clone(int (*fn)(void *), void *child_stack, int flags, void *arg); 。它接受多個參數,這些參數賦予了clone函數強大的靈活性。
fn參數是一個函數指針,指向新進程開始執行的函數,就像是為新進程設定了一個起點。child_stack參數指定了新進程的用戶態棧指針,為新進程提供了一個獨立的堆棧空間 。
flags參數是一個標志位,它決定了新進程如何繼承父進程的資源。通過設置不同的標志位,可以實現不同的資源共享和進程創建方式。比如,CLONE_VM標志表示新進程與父進程共享內存描述符和所有的頁表,這樣它們就可以共享內存空間;CLONE_FS標志表示共享文件系統,包括根目錄、當前目錄和文件權限掩碼等;CLONE_FILES標志表示共享打開的文件描述符,使得父子進程可以訪問相同的文件 。
arg參數是傳遞給fn函數的參數,就像是給新進程傳遞了一份 “行李”,讓它在開始執行時可以使用 。
clone函數創建進程的獨特之處在于它可以根據用戶的需求,精細地控制新進程對父進程資源的繼承方式,既可以創建完全獨立的進程,也可以創建共享部分資源的輕量級進程(線程)。這種靈活性使得clone函數在很多場景下都能發揮重要作用,比如在實現多線程庫時,就可以利用clone函數來創建共享內存和文件描述符的輕量級進程,模擬線程的行為 。
Linux中創建進程(fork,vfork)和線程(pthread_create),在內核都是調用do_fork()–>clone(),參數clone_flags標記表明哪些資源是需要克隆的,創建線程時,所有資源都克隆;
從調度的角度理解線程,從資源角度來理解進程,內核里只要是task_struct,就可以被調度;linux中的線程又叫輕量級進程lwp;
ret = pthread_create(&tid1, NULL, thread_fun, NULL);
if (ret == -1) {
perror(“cannot create new thread”);
return -1;
}
strace ./a.out2.5幕后英雄:do_fork 和 copy_process
在進程創建的過程中,do_fork函數是真正的核心。它就像是一場精彩演出背后的導演,負責協調和安排進程創建的各個環節 。do_fork函數定義在 Linux 內核的kernel/fork.c文件中,它接受多個參數,包括clone_flags(克隆標志)、stack_start(棧起始地址)、regs(寄存器值)和stack_size(棧大小)等。這些參數就像是導演手中的劇本和道具清單,決定了新進程的各種特性和資源分配 。
do_fork函數主要完成以下幾個關鍵任務:首先,它會根據clone_flags標志來檢查創建進程的合法性,確保滿足各種條件和限制。然后,它會調用copy_process函數來創建新進程的描述符和其他內核數據結構 。在創建過程中,它會為新進程分配一個唯一的進程 ID(PID),這個 PID 就像是新進程的身份證,用于在系統中唯一標識這個進程 。
copy_process函數則是do_fork函數的得力助手,它負責具體創建新進程的描述符和其他內核數據結構。它就像是一個勤勞的工匠,精心打造新進程所需的各種 “零件” 。copy_process函數首先會調用dup_task_struct函數復制當前進程的task_struct結構體,這個結構體是進程描述符,包含了進程的各種信息,如進程狀態、優先級、打開的文件描述符等 。
接著copy_process函數會檢查新進程的資源限制,確保不會超過系統的限制。然后,它會初始化新進程的各種屬性,如設置進程的狀態為可運行狀態(TASK_RUNNING),初始化進程的信號處理函數、文件系統信息等 。在初始化過程中,它會根據clone_flags標志來決定新進程如何繼承父進程的資源,是完全復制還是共享部分資源 。
最后,copy_process函數會調用copy_thread函數來初始化子進程的內核棧,為子進程的執行做好準備。通過do_fork和copy_process函數的協同工作,一個新的進程就誕生了,它帶著自己的使命和資源,開始在 Linux 系統這個大舞臺上展現自己的活力 。
三、Linux進程調度
3.1調度策略
在 Linux 的進程調度舞臺上,有多種調度策略可供選擇,每種策略都有其獨特的特點和適用場景,就像是不同風格的舞蹈,在不同的音樂節奏下展現出各自的魅力 。
首先是SCHED_OTHER,它是默認的分時調度策略,適用于大多數普通應用程序。在這種策略下,系統會根據進程的nice值(取值范圍為 - 20 到 19,數值越小優先級越高)來分配 CPU 時間。nice值就像是進程的 “好感度”,好感度越高(nice值越小)的進程,獲得 CPU 時間的機會就越大 。例如,一個普通的文本編輯程序,它對實時性要求不高,就可以采用SCHED_OTHER調度策略,讓系統合理地分配 CPU 時間,保證它能正常運行 。
SCHED_FIFO是實時調度策略中的先進先出策略,適用于對時間要求嚴格、需要立即執行的實時任務 。一旦一個SCHED_FIFO類型的進程獲得了 CPU,它就會一直運行下去,直到它主動放棄 CPU(比如因為等待 I/O 操作而阻塞)或者被更高優先級的進程搶占 。這就好比一場緊張的賽車比賽,一旦賽車駛入賽道,就會一直飛馳,直到遇到特殊情況才會停下來 。比如,工業控制系統中的一些實時控制任務,需要對外部設備的信號做出快速響應,就可以使用SCHED_FIFO策略,確保任務能及時執行,避免因延遲而導致生產事故 。
SCHED_RR也是實時調度策略,它是時間片輪轉調度策略 。與SCHED_FIFO不同的是,SCHED_RR為每個進程分配一個固定的時間片,當進程的時間片用完后,它會被放到就緒隊列的末尾,等待下一次調度 。這就像是一場接力賽跑,每個選手都有自己的固定跑步時間,時間一到就把接力棒交給下一位選手 。對于一些對響應時間要求嚴格且執行時間較短的實時任務,比如多媒體播放中的音頻和視頻解碼任務,使用SCHED_RR策略可以保證每個任務都能得到及時處理,避免出現音頻卡頓或視頻掉幀的情況 。
在優先級關系上,實時進程(采用SCHED_FIFO和SCHED_RR策略)的優先級高于普通進程(采用SCHED_OTHER策略) 。這意味著當有實時進程就緒時,系統會優先調度實時進程,確保它們能及時運行,而普通進程則需要等待實時進程執行完畢或者主動放棄 CPU 后才有機會運行 。
3.2調度算法
Linux 內核中,完全公平調度器(CFS)是進程調度的核心算法之一,它的設計目標是實現所有進程公平分享 CPU 資源 。CFS 就像是一位公正的裁判,努力讓每個進程都能在 CPU 這個賽場上獲得公平的比賽時間 。
CFS 的工作原理基于虛擬運行時間(vruntime) 。每個進程都有一個vruntime值,它表示進程的虛擬運行時間 。vruntime的計算方式為:vruntime = 實際運行時間 × 1024 / 進程權重 。這里的進程權重由進程的nice值決定,nice值越小(優先級越高),權重越大 。例如,一個nice值為 - 5 的進程,它的權重會比nice值為 5 的進程大,在相同的實際運行時間下,它的vruntime增長速度會更慢 。
CFS 使用紅黑樹來存儲可運行進程,以vruntime為鍵值 。每次調度時,CFS 會選擇紅黑樹中最左側節點(即vruntime最小的進程)運行 。這就好比在一場比賽中,裁判總是選擇那些 “跑得最慢”(vruntime最小)的選手先上場,讓每個選手都有機會展示自己 。通過這種方式,CFS 保證了每個進程都能公平地獲得 CPU 時間,避免了某些進程長時間占用 CPU 而導致其他進程饑餓的情況 。
實時調度策略主要包括SCHED_FIFO和SCHED_RR,它們適用于對時間要求嚴格的實時任務 。實時調度策略的特點是優先級較高,能夠確保實時任務在規定的時間內完成 。對于SCHED_FIFO策略的任務,一旦獲得 CPU 就會一直運行,直到主動放棄或被更高優先級任務搶占;而SCHED_RR策略的任務則是按照時間片輪轉的方式運行,每個任務在自己的時間片內運行,時間片用完后會被放到就緒隊列末尾 。
這些實時調度策略在工業控制、航空航天等對實時性要求極高的領域有著廣泛的應用,比如在航空航天領域,飛行器的飛行控制任務需要實時處理各種傳感器數據,對時間的準確性要求極高,使用實時調度策略可以確保這些任務能夠及時、準確地執行,保障飛行器的安全飛行 。
3.3調度時機
進程調度的時機就像是一場音樂會中節奏的轉換點,把握得恰到好處才能讓整個演出流暢進行 。在 Linux 系統中,進程調度會在多種情況下發生 :
當系統調用返回時,是一個常見的調度時機 。系統調用就像是進程向操作系統發出的服務請求,當請求處理完成返回時,系統會檢查是否有更合適的進程需要運行 。比如,一個進程調用read系統調用來讀取文件數據,在讀取完成返回后,系統可能會發現有其他進程的優先級更高或者等待時間更長,這時就會進行進程調度,讓更合適的進程獲得 CPU 。
中斷處理完成后也會觸發進程調度 。中斷就像是緊急事件的通知,當硬件設備(如鍵盤、鼠標、網卡等)產生中斷時,操作系統會暫停當前進程的執行,去處理中斷事件 。當中斷處理完成后,系統會重新評估當前的進程狀態,決定是否進行進程調度 。例如,當用戶按下鍵盤時,會產生鍵盤中斷,操作系統會暫停當前正在運行的進程,去處理鍵盤輸入事件,處理完成后,再根據情況決定是否調度其他進程 。
進程主動放棄 CPU 也是調度的時機之一 。有些進程在執行過程中,可能會因為等待某些資源(如等待其他進程發送信號、等待 I/O 操作完成等)而主動調用schedule函數放棄 CPU 。比如,一個進程需要等待另一個進程完成某項任務后才能繼續執行,它就會主動調用schedule函數,將 CPU 讓給其他可運行的進程,直到等待的條件滿足后再重新競爭 CPU 。
另外,當進程的時間片用完時,也會進行進程調度 。在采用時間片輪轉調度策略(如SCHED_RR)的情況下,每個進程被分配一個固定的時間片,當時間片用完后,進程就會被迫讓出 CPU,等待下一次調度 。這就像是一場限時演講比賽,每個選手的演講時間一到,就必須下臺,讓下一位選手上臺演講 。通過這些調度時機的合理把握,Linux 系統能夠高效地管理進程,確保每個進程都能在合適的時間運行,提高系統的整體性能 。
四、Linux進程終止
4.1 終止原因
進程終止是進程生命周期的最后階段,就像是一場演出的落幕。進程終止的原因多種多樣,常見的有以下幾種 。
當進程調用exit函數時,它就像是一個演員主動向導演示意演出結束,決定主動終止自己的運行 。在 C 語言中,exit函數是標準庫函數,用于正常終止程序的執行 。它接受一個整數參數,這個參數通常被稱為退出狀態碼,用于向父進程或操作系統傳遞進程終止的相關信息 。比如,退出狀態碼為 0 通常表示進程正常結束,就像一場演出完美謝幕;而其他非零值則表示進程在執行過程中遇到了問題,比如文件讀取失敗、內存分配錯誤等,就像是演出過程中出現了意外狀況 。
進程收到特定信號也會導致其終止 。信號是 Linux 系統中進程間通信的一種方式,它就像是一個緊急通知,當進程收到某些特定信號時,可能會被迫終止 。例如,SIGKILL信號是一個強制終止信號,它就像是一把 “尚方寶劍”,一旦發送給進程,進程就會立即無條件終止,沒有任何機會進行資源清理或其他善后工作 。SIGTERM信號則相對溫和一些,它是一個正常的終止信號,用于通知進程優雅地終止 。當進程收到SIGTERM信號時,它可以選擇捕獲這個信號,并在信號處理函數中進行一些清理工作,如關閉打開的文件、釋放內存等,然后再正常終止 。就好比一個演員在收到導演的 “謝幕” 通知后,會先整理好自己的道具和服裝,然后再優雅地退場 。
4.2終止過程
當進程調用exit函數后,會經歷一系列復雜而有序的步驟,最終完成終止過程 。這個過程就像是一場精心安排的閉幕式,每個環節都至關重要 。
exit函數最終會調用內核中的do_exit函數,do_exit函數就像是這場閉幕式的總導演,負責協調和執行進程終止的各個環節 。do_exit函數首先會設置進程描述符task_struct的標志成員為PF_EXITING,這個標志就像是一個 “演出結束” 的牌子,向系統表明該進程正在退出 。
接著,do_exit函數會調用del_timer_sync函數刪除任意內核定時器 。內核定時器就像是進程演出過程中的定時提醒裝置,在進程終止時,這些定時器不再需要,所以要將它們刪除 。
如果 BSD 的進程記賬功能是開啟的,do_exit函數會調用acct_update_integrals函數來輸出記賬信息 。進程記賬功能就像是一個記錄進程 “演出開銷” 的賬本,記錄了進程占用 CPU 時間等信息,在進程終止時,需要將這些信息輸出 。
然后,do_exit函數會調用exit_mm函數釋放進程占用的mm_struct,如果沒有別的進程使用(即沒有被共享),就會徹底釋放 。mm_struct結構體管理著進程的虛擬內存空間,就像是進程的 “演出場地”,在進程終止時,要將這個場地歸還給系統 。
do_exit函數還會調用exit_sem函數,如果進程排隊等待 IPC(進程間通信)信息,則讓它離開該隊列 。IPC 就像是進程之間的 “交流渠道”,在進程終止時,要斷開這些交流渠道 。
調用exit_files和exit_fs函數,分別處理文件描述符和文件系統數據的引用計數 。如果引用計數降為 0,說明沒有其他進程使用這些資源,這時就可以釋放它們 。文件描述符和文件系統數據就像是進程演出時使用的 “道具”,在演出結束后,要將這些道具妥善處理 。
do_exit函數會把task_struct中的exit_code成員(退出代碼)置為exit函數傳入的參數或其他相關值,這個退出代碼會供父進程檢索,用于了解子進程終止的原因 。就好比演員在退場時,會給導演留下一張便條,說明自己退場的原因 。
do_exit函數會調用exit_notify函數向父進程發送信號,告知父進程自己即將終止 。同時,會給該進程尋找一個 “養父”,這個 “養父” 可能是線程組的其他線程或init進程 。并且會把進程狀態改為EXIT_ZOMBIE(僵尸狀態) 。在僵尸狀態下,進程雖然已經基本停止運行,但它的進程描述符等信息還會被保留,直到父進程對其進行處理 。
do_exit函數會調用schedule函數切換到新的進程 。此時,原進程已經完成了大部分的終止工作,系統會調度其他可運行的進程繼續執行,就像是一場演出結束后,舞臺會為下一場演出做好準備 。
4.3僵死進程與避免
僵死進程,也被稱為僵尸進程,是 Linux 系統中一種特殊的進程狀態 。當子進程先于父進程退出,且父進程沒有及時讀取子進程的退出狀態時,子進程就會進入僵死狀態,成為僵死進程 。這就好比一個孩子提前離開了舞臺,但家長卻沒有來接他,他只能在舞臺邊等待 。
僵死進程會在系統中保留其進程描述符、進程 ID 等信息,雖然它不再占用大量的系統資源,但如果大量的僵死進程存在,會占用有限的進程 ID 資源,導致系統無法創建新的進程 。這就像是舞臺邊擠滿了等待家長的孩子,使得新的演員無法上臺表演 。
為了避免僵死進程的產生,可以采取以下幾種方法 。父進程可以調用wait系列函數(如wait、waitpid)來等待子進程結束,并獲取子進程的退出狀態 。wait函數會使父進程阻塞,直到有子進程退出,然后它會收集子進程的信息,并把它徹底銷毀 。waitpid函數則更加靈活,它可以指定等待特定的子進程,并且可以設置非阻塞模式 。這就好比家長在孩子表演結束后,及時到舞臺邊接孩子,將孩子安全地帶回家 。
父進程可以安裝SIGCHLD信號的處理函數 。當子進程退出時,系統會向父進程發送SIGCHLD信號,父進程可以在信號處理函數中調用waitpid函數來處理子進程的退出,這樣可以避免父進程阻塞,提高程序的并發性能 。這就好比家長給孩子設置了一個信號器,當孩子表演結束時,信號器會通知家長,家長可以及時去接孩子 。
還可以使用 “兩次fork” 的技巧 。父進程先fork出一個子進程,然后子進程再fork出一個孫子進程,接著子進程立即exit退出 。這樣,孫子進程就會成為孤兒進程,被init進程收養,init進程會負責清理孫子進程,從而避免了僵死進程的產生 。這就好比家長讓孩子先找到一個臨時監護人,然后自己離開,臨時監護人會照顧好孩子,確保孩子不會無人照料 。通過這些方法,可以有效地避免僵死進程的產生,保證系統的穩定運行 。
五、Linux進程案例分析
Linux的調度器類主要實現兩類進程調度算法:實時調度算法和完全公平調度算法(CFS),實時調度算法SCHED_FIFO和SCHED_RR,按優先級執行,一般不會被搶占。直到實時進程執行完,才會執行普通進程。而大多數的普通進程,用的就是CFS算法。
進程調度的時機:
①進程狀態轉換時刻:進程終止、進程睡眠;
②當前進程的”時間片”用完;
③主動讓出處理器,用戶調用sleep()或者內核調用schedule();
④從中斷,系統調用或異常返回時。
每個進程task_struct中都有一個struct sched_entity se成員,這就是調度器的實體結構,進程調度算法實際上就是管理所有進程的這個se。
結構任務結構{
揮發性長狀態; / * - 1 不可運行, 0 可以運行, > 0 已停止* /
無效*堆棧;
atomic_t 用法;
無符號整型標志; / *每個進程標志,定義如下* /
無符號整型ptrace ;
int鎖深度; / * BKL鎖定深度* /
#ifdef CONFIG_SMP
#ifdef __ARCH_WANT_UNLOCKED_CTXSW
int oncpu ;
#萬一
#萬一
int prio , static_prio ,正常_prio ;
無符號整數rt_priority ;
const struct sched_class * sched_class ;
struct sched_entity se; //進程調度實體
結構 sched_rt_entity rt ;
……
}CFS基于一個簡單的理念:所有任務都應該公平的分配處理器。理想情況下,n個進程的調度系統中,每個進程獲得1/n處理器時間,所有進程的vruntime也是相同的。
CFS完全拋棄了時間片的概念,而是分配一個處理器使用比來度量。每個進程一個調度周期內分配的時間(類似于傳統的“時間片”)跟三個因素有關:進程總數,優先級,調度周期
5.1理解CFS的首先要理解vruntime的含義
簡單說vruntime就是該進程的運行時間,但這個時間是通過優先級和系統負載等加權過的時間,而非物理時鐘時間,按字面理解為虛擬運行時間,也很恰當。
每個進程的調度實體se都保存著本進程的虛擬運行時間。
結構sched_entity {
struct load_weight 負載; / * 用于負載均衡* / _
結構 rb_node run_node ;
struct list_head group_node ;
無符號整型 on_rq ;
u64 exec_start ;
u64 sum_exec_runtime ;
u64 vruntime; //虛擬運行時間
u64 prev_sum_exec_runtime ;
……
}而進程相關的調度方法如下:
靜態常量結構 sched_class fair_sched_class = {
。下一個 = & idle_sched_class ,
。 enqueue_task = enqueue_task_fair ,
。出隊任務 =出隊任務公平,
。產量任務 =產量任務公平,
。 check_preempt_curr = check_preempt_wakeup ,
。 pick_next_task = pick_next_task_fair ,
。 put_prev_task = put_prev_task_fair ,
#ifdef CONFIG_SMP
。 select_task_rq = select_task_rq_fair ,
。 rq_online = rq_online_fair ,
。 rq_offline = rq_offline_fair ,
。任務喚醒 =任務喚醒公平,
#萬一
。 set_curr_task = set_curr_task_fair ,
。任務滴答 =任務滴答公平,
。任務分叉 =任務分叉公平,
。 prio_changed = prio_changed_fair ,
。 Switched_to = Switched_to_fair ,
。 get_rr_interval = get_rr_interval_fair ,
#ifdef CONFIG_FAIR_GROUP_SCHED
。任務移動組 =任務移動組公平,
#萬一
} ;5.2vruntime的值如何跟新?
時鐘中斷產生時,會依次調用tick_periodic()-> update_process_times()->scheduler_tick()。
無效調度程序_tick (無效)
{
……
raw_spin_lock ( & rq- > lock ) ; _
update_rq_clock ( rq ) ;
update_cpu_load ( rq ) ;
curr->sched_class->task_tick(rq, curr, 0); //執行調度器tick,更新進程vruntime
raw_spin_unlock ( & rq- > lock ) ; _
……
}
task_tick_fair ()調用entity_tick()如下:
靜態無效entity_tick (結構cfs_rq * cfs_rq ,結構sched_entity * curr , int排隊)
{
update_curr ( cfs_rq ) ;
……
if ( cfs_rq - > nr_running > 1 | | ! sched_feat ( WAKEUP_PREEMPT ) )
check_preempt_tick(cfs_rq, curr); //檢查當前進程是否需要調度
}這里分析兩個重要函數update_curr()和check_preempt_tick()。
靜態無效 update_curr (結構 cfs_rq * cfs_rq )
{
struct sched_entity * curr = cfs_rq - > curr ;
u64現在 = rq_of ( cfs_rq ) - >時鐘;
無符號長 delta_exec ;
如果 (不太可能(! curr ))
返回;
// delta_exec獲得最后一次修改后,當前進程所運行的實際時間
delta_exec = ( unsigned long ) ( now - curr - > exec_start ) ;
如果 (! delta_exec )
返回;
__update_curr ( cfs_rq , curr , delta_exec ) ;
curr->exec_start = now; //運行時間已保存,更新起始執行時間
如果 ( entity_is_task (當前)) {
struct task_struct * curtask = task_of ( curr ) ;
trace_sched_stat_runtime ( curtask , delta_exec , curr - > vruntime ) ;
cpuacct_charge ( curtask , delta_exec ) ;
account_group_exec_runtime ( curtask , delta_exec ) ;
}
}主要關心__update_curr()函數。
靜態內聯 void __update_curr ( struct cfs_rq * cfs_rq , struct sched_entity * curr , unsigned long delta_exec )
{
無符號長 delta_exec_weighted ;
schedstat_set ( curr - > exec_max , max ( ( u64 ) delta_exec , curr - > exec_max ) ) ;
curr->sum_exec_runtime += delta_exec;//累計實際運行時間
schedstat_add ( cfs_rq , exec_clock , delta_exec ) ;
delta_exec_weighted = calc_delta_fair ( delta_exec , curr ) ; //對delta_exec加權
curr->vruntime += delta_exec_weighted;//累計入vruntime
update_min_vruntime(cfs_rq); //更新cfs_rq最小vruntime(保存所有進程中的最小vruntime)
}關注calc_delta_fair()加權函數如何實現。
/ *
* δ / = w
* /
靜態內聯無符號長整型
calc_delta_fair ( unsigned long delta , struct sched_entity * se )
{
if (不太可能( se - > load .weight ! = NICE_0_LOAD ) )
delta = calc_delta_mine ( delta , NICE_0_LOAD , & se - >負載) ;
返回增量;
}若當前進程nice為0,直接返回實際運行時間,其他所有nice值的加權都是以0nice值為參考增加或減少的。
/ *
* delta * =重量/ lw
* /
靜態無符號長整型
calc_delta_mine (無符號長 delta_exec ,無符號長權重,
結構 load_weight * lw )
{
u64 tmp ;
if ( ! lw - > inv_weight ) {
if ( BITS_PER_LONG > 32 & &不太可能( lw - >權重> = WMULT_CONST ) )
lw - > inv_weight = 1 ;
別的
lw- > inv_weight = 1 + ( WMULT_CONST - lw- >權重/ 2 ) _
/ (lw->weight+1);//這公式沒弄明白
}
tmp = ( u64 ) delta_exec *權重;
/ *
*檢查64位乘法是否溢出:
* /
如果 (不太可能( tmp > WMULT_CONST ))
tmp = SRR ( SRR ( tmp , WMULT_SHIFT / 2 ) * lw - > inv_weight ,
WMULT_SHIFT / 2 ) ;
別的
tmp = SRR(tmp * lw->inv_weight, WMULT_SHIFT);//做四舍五入
返回(無符號長)分鐘( tmp , ( u64 )(無符號長) LONG_MAX );
}當nice!=0時,實際是按公式delta *= weight / lw來計算的weight=1024是nice0的權重,lw是當前進程的權重,該lw和nice值的換算后面介紹,上面還書的lw計算公式沒弄明白,總之這個函數就是把實際運行時間加權為進程調度里的虛擬運行時間,從而更新vruntime。
更新完vruntime之后,會檢查是否需要進程調度。
返回 static voidentity_tick ( struct cfs_rq * cfs_rq , struct sched_entity * curr , int排隊)
{
update_curr ( cfs_rq ) ;
……
if ( cfs_rq - > nr_running > 1 | | ! sched_feat ( WAKEUP_PREEMPT ) )
check_preempt_tick(cfs_rq, curr); //檢查當前進程是否需要調度
}更新完cfs_rq之后,會檢查當前進程是否已經用完自己的“時間片”。
/ *
*如果需要,用新喚醒的任務搶占當前任務:
* /
靜態無效
check_preempt_tick ( struct cfs_rq * cfs_rq , struct sched_entity * curr )
{
無符號長的 Ideal_runtime , delta_exec ;
ideal_runtime = sched_slice(cfs_rq, curr);//ideal_runtime是理論上的處理器運行時間片
delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;//該進程本輪調度累計運行時間
if (delta_exec > ideal_runtime) {// 假如實際運行超過調度器分配的時間,就標記調度標志
resched_task ( rq_of ( cfs_rq ) - > curr ) ;
/ *
*當前任務運行足夠長的時間,確保它不會得到
*由于好友的青睞而再次當選。
* /
清除好友( cfs_rq , curr ) ;
返回;
}
/ *
*確保錯過喚醒搶占的任務
*窄邊距不必等待完整的切片。
*這也減少了負載下好友引起的延遲。
* /
if ( ! sched_feat ( WAKEUP_PREEMPT ) )
返回;
if ( delta_exec < sysctl_sched_min_capsularity )
返回;
如果 ( cfs_rq - > nr_running > 1 ) {
struct sched_entity * se = __pick_next_entity ( cfs_rq ) ;
s64 delta = curr - > vruntime - se - > vruntime ;
if (增量>理想運行時間)
resched_task ( rq_of ( cfs_rq ) - > curr ) ;
}
}當該進程運行時間超過實際分配的“時間片”,就標記調度標志resched_task(rq_of(cfs_rq)->curr);,否則本進程繼續執行。中斷退出,調度函數schedule()會檢查此標記,以選取新的進程來搶占當前進程。
5.3如何選擇下一個可執行進程
CFS選擇具有最小vruntime值的進程作為下一個可執行進程,CFS用紅黑樹來組織調度實體,而鍵值就是vruntime。那么CFS只要查找選擇最左葉子節點作為下一個可執行進程即可。實際上CFS緩存了最左葉子,可以直接選取left_most葉子。
上面代碼跟蹤到timer tick中斷退出,若“ideal_runtime”已經用完,就會調用schedule()函數選中新進程并且完成切換。
asmlinkage void __sched 時間表( void )
{
if ( prev - > state && ! ( preempt_count ( ) & PREEMPT_ACTIVE ) ) { _
if (不太可能( signal_pending_state ( prev - > state , prev ) ) )
上一個 - >狀態= TASK_RUNNING ;
別的
deactivate_task(rq, prev, 1);//如果狀態已經不可運行,將其移除運行隊列
switch_count = &上一個- > nvcsw ;
}
pre_schedule ( rq ,上一個) ;
if (不太可能( ! rq - > nr_running ) )
空閑平衡( CPU , RQ );
put_prev_task(rq, prev); //處理上一個進程
next = pick_next_task(rq);//選出下一個進程
……
context_switch(rq, prev, next); /* unlocks the rq *///完成進程切換
……
}如果進程狀態已經不是可運行,那么會將該進程移出可運行隊列,如果繼續可運行put_prev_task()會依次調用put_prev_task_fair()->put_prev_entity()。
靜態無效put_prev_entity (結構cfs_rq * cfs_rq ,結構sched_entity * prev )
{
/ *
* 如果仍在運行隊列中,則deactivate_task ( )
*沒有被調用并且update_curr ( )必須被完成:
* /
if (上一個- > on_rq )
update_curr ( cfs_rq ) ;
check_spread ( cfs_rq ,上一個) ;
如果 (上一個- > on_rq ) {
update_stats_wait_start ( cfs_rq ,上一個) ;
/ *將“當前”放回樹中。 * /
__enqueue_entity ( cfs_rq ,上一個) ;
}
cfs_rq - > curr = NULL ;
}__enqueue_entity(cfs_rq, prev) 將上一個進程重新插入紅黑樹(注意,當前運行進程是不在紅黑樹中的)pick_next_task()會依次調用pick_next_task_fair()。
靜態結構task_struct * pick_next_task_fair (結構rq * rq )
{
結構任務結構* p ;
struct cfs_rq * cfs_rq = & rq - > cfs ;
結構 sched_entity * se ;
if ( ! cfs_rq - > nr_running )
返回空值;
做 {
se = pick_next_entity(cfs_rq);//選出下一個可執行進程
set_next_entity(cfs_rq, se); //把選中的進程(left_most葉子)從紅黑樹移除,更新紅黑樹
cfs_rq = group_cfs_rq ( se );
} while ( cfs_rq ) ;
p = task_of ( se ) ;
htick_start_fair ( rq , p ) ;
返回 p ;
}set_next_entity()函數會調用__dequeue_entity(cfs_rq, se)把選中的下一個進程即最左葉子移出紅黑樹。最后context_switch()完成進程的切換。
5.4何時更新rbtree
①上一個進程執行完ideal_time,還可繼續執行時,會插入紅黑樹;
②下一個進程被選中移出rbtree紅黑樹時;
③新建進程;
④進程由睡眠態被激活,變為可運行態時;
⑤調整優先級時也會更新rbtree。
5.5新建進程如何加入紅黑樹
新建進程會做一系列復雜的工作,這里我們只關心與紅黑樹有關部分
Linux使用fork,clone或者vfork等系統調用創建進程,最終都會到do_fork函數實現,如果沒有設置CLONE_STOPPED,do_fork會執行兩個與紅黑樹相關的函數: copy_process()和wake_up_new_task()
(1)copy_process()->sched_fork()->task_fork()
static void place_entity ( struct cfs_rq * cfs_rq , struct sched_entity * se , int初始值)
{
u64 vruntime = cfs_rq->min_vruntime;//以cfs的最小vruntime為基準
/ *
* “當前”期間已承諾當前任務, _
*然而,新任務的額外重量會減慢他們的速度
*小,放置新任務,使其適合該插槽
*最后保持打開狀態。
* /
if (初始&& sched_feat ( START_DEBIT ) ) _
vruntime += sched_vslice(cfs_rq, se);// 加上一個調度周期內的"時間片"
/ *休眠至單個延遲不計算在內。 * /
if ( ! initial & & sched_feat ( FAIR_SLEEPERS ) ) {
無符號長閾值= sysctl_sched_latency ;
/ *
*將休眠閾值轉換為虛擬時間。
* SCHED_IDLE是一個特殊的子類。我們關心
*公平性僅相對于其他 SCHED_IDLE 任務,
*所有這些都具有相同的重量。
* /
if ( sched_feat ( NORMALIZED_SLEEPER ) & & ( ! entity_is_task ( se ) | |
task_of ( se ) - >策略!= SCHED_IDLE ) )
thresh = calc_delta_fair ( thresh , se ) ;
/ *
*將睡眠時間的影響減半, 以允許
* 為睡眠者帶來更溫和的效果:
* /
如果 ( sched_feat ( GENTLE_FAIR_SLEEPERS ))
脫粒> > = 1 ;
vruntime - = 閾值;
}
/ *確保我們永遠不會因為被排在后面而贏得時間。 * /
vruntime = max_vruntime ( self - > vruntime , vruntime ) ;
se - > vruntime = vruntime ;
}計算新進程的vruntime值,加上一個“平均時間片”表示剛執行完,避免新建進程立馬搶占CPU。
(2)調用wake_up_new_task函數
無效wake_up_new_task ( struct task_struct * p ,無符號長clone_flags )
{
……
rq = task_rq_lock ( p , & flags ) ;
update_rq_clock ( rq ) ;
activate_task(rq, p, 0);//激活當前進程,添加入紅黑樹
check_preempt_curr(rq, p, WF_FORK);//確認是否需要搶占
……
}更新時鐘,激活新建的進程activate_task()會調用。
static void enqueue_task ( struct rq * rq , struct task_struct * p , intwakeup , bool head )
{
如果 (喚醒)
p- > se 。_ start_runtime = p - > se 。 sum_exec_運行時;
sched_info_queued ( p ) ;
p- > sched_class- > enqueue_task ( rq , p ,喚醒, head ) ; _ _
p- > se 。_ on_rq = 1 ;
}將新建的進程加入rbtree。
5.6喚醒進程
調用try_to_wake_up()->activate_task()->enqueue_task_fair()->enqueue_entity()注意enqueue_entity 函數調用place_entity對進程vruntime做補償計算,再次考察place_entity(cfs_rq, se, 0)。
static void place_entity ( struct cfs_rq * cfs_rq , struct sched_entity * se , int初始值)
{
u64 vruntime = cfs_rq->min_vruntime;//以cfs的最小vruntime為基準
/ *
* “當前”期間已承諾當前任務, _
*然而,新任務的額外重量會減慢他們的速度
*小,放置新任務,使其適合該插槽
*最后保持打開狀態。
* /
if (初始&& sched_feat ( START_DEBIT ) ) _
vruntime += sched_vslice(cfs_rq, se);//一個調度周期內的"時間片"
/ *休眠至單個延遲不計算在內。 * /
if ( ! initial & & sched_feat ( FAIR_SLEEPERS ) ) {
無符號長閾值= sysctl_sched_latency ;
/ *
*將休眠閾值轉換為虛擬時間。
* SCHED_IDLE是一個特殊的子類。我們關心
*公平性僅相對于其他 SCHED_IDLE 任務,
*所有這些都具有相同的重量。
* /
if ( sched_feat ( NORMALIZED_SLEEPER ) & & ( ! entity_is_task ( se ) | |
task_of ( se ) - >策略!= SCHED_IDLE ) )
thresh = calc_delta_fair ( thresh , se ) ;
/ *
*將睡眠時間的影響減半, 以允許
* 為睡眠者帶來更溫和的效果:
* /
如果 ( sched_feat ( GENTLE_FAIR_SLEEPERS ))
脫粒> > = 1 ;
vruntime -= thresh;//對于睡眠進程喚醒,給予vruntime補償
}
/ *確保我們永遠不會因為被排在后面而贏得時間。 * /
vruntime = max_vruntime(se->vruntime, vruntime);//避免通過睡眠來獲得運行時間
se - > vruntime = vruntime ;
}當initial=1時,新建進程vruntime=cfs最小vruntime值+時間片,放入紅黑樹最右端。
當initial=0時,表示喚醒進程,vruntime要減去一個thresh.這個thresh由調度周期sysctl_sched_latency加權得到虛擬時間,這樣做可以對睡眠進程做一個補償,喚醒時會得到一個較小的vruntime, 使它可以盡快搶占CPU(可以快速響應I/O消耗型進程)。
注意注釋/* ensure we never gain time by being placed backwards. */這個設計是為了給睡眠較長時間的進程做時間補償的,既使其可以快速搶占,又避免因太小的vruntime值而長期占用CPU。但有些進程只是短時間睡眠,這樣喚醒時自身vruntime還是大于min_vruntime的,為了不讓進程通過睡眠獲得額外運行時間補償,最后vruntime取計算出的補償時間和進程本身的vruntime較大者。從這可以看出,雖然CFS不再區分I/O消耗型,CPU消耗型進程,但是CFS模型對IO消耗型天然的提供了快速的響應。
5.7改變進程優先級,如何調整rbtree
Linux中改變進程優先級會調用底層的set_user_nice()。
void set_user_nice ( struct task_struct * p ,長nice )
{
……
dequeue_task(rq, p, 0); //把進程從紅黑樹中取出
……
p->static_prio = NICE_TO_PRIO(nice);//將nice(-20~19)值映射到100~139,0~99是實時進程優先級
設置負載重量( p );
……
enqueue_task ( rq , p , 0 , false ) ;
}set_user_nice把進程從紅黑樹取出,調整優先級(nice值對應權重),再重新加入紅黑樹。
set_load_weight()函數是設置nice值對應的權重。
靜態無效set_load_weight (結構task_struct * p )
{
如果 ( task_has_rt_policy ( p )) {
p- > se 。_負載。重量= 0 ;
p- > se 。_負載。 inv_weight = WMULT_CONST ;
返回;
}
/ *
* SCHED_IDLE 任務獲得最小權重:
* /
如果 ( p - >政策== SCHED_IDLE ){ _
p- > se 。_負載。重量= WEIGHT_IDLEPRIO ;
p- > se 。_負載。 inv_weight = WMULT_IDLEPRIO ;
返回;
}
p- > se 。_負載。權重= prio_to_weight [ p - > static_prio - MAX_RT_PRIO ] ;
p- > se 。_負載。 inv_weight = prio_to_wmult [ p - > static_prio - MAX_RT_PRIO ] ;
}數組prio_to_weight[]是將nice值(-20~19)轉化為以nici 0(1024)值為基準的加權值,根據內核注釋每一個nice差值,權重相差10%,即在負載一定的條件下,每增加或減少一個nice值,獲得的CPU時間相應增加或減少10%。
靜態常量 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 ,
} ;上面calc_delta_mine()函數用到這個數組加權值,這個轉化過程還沒弄明白,有明白的朋友,指點一二,不勝感激。
/ *
* prio_to_weight [ ]數組的反( 2^32 / x )值,預先計算。
*
* 在重量不經常變化的情況下,我們可以使用
*預先計算逆數以通過轉動除法來加速算術
*轉化為乘法:
* /
靜態常量u32 prio_to_wmult [ 40 ] = {
/ * - 20 * / 48388 , 59856 , 76040 , 92818 , 118348 ,
/ * - 15 * / 147320 , 184698 , 229616 , 287308 , 360437 ,
/ * - 10 * / 449829 , 563644 , 704093 , 875809 , 1099582 ,
/ * - 5 * / 1376151 , 1717300 , 2157191 , 2708050 , 3363326 ,
/ * 0 * / 4194304、5237765、6557202、8165337、10153587 、 _ _ _ _ _ _ _ _
/ * 5 * / 12820798、15790321、19976592、24970740、31350126 、_ _ _ _ _ _ _ _
/ * 10 * / 39045157、49367440、61356676、76695844、95443717 、 _ _ _ _ _ _ _ _
/ * 15 * / 119304647、148102320、186737708、238609294、286331153 、 _ _ _ _ _ _ _ _
} ;最后,說下對CFS “完全公平” 的理解:
①不再區分進程類型,所有進程公平對待
②對I/O消耗型進程,仍然會提供快速響應(對睡眠進程做時間補償)
③優先級高的進程,獲得CPU時間更多(vruntime增長的更慢)
可見CFS的完全公平,并不是說所有進程絕對的平等,占用CPU時間完全相同,而是體現在vruntime數值上,所有進程都用虛擬時間來度量,總是讓vruntime最小的進程搶占,這樣看起來是完全公平的,但實際上vruntime的更新,增長速度,不同進程是不盡一樣的。CFS利用這么個簡單的vruntime機制,實現了以往需要相當復雜算法實現的進度調度需求,高明!

























