Linux操作系統(tǒng)底層原理:進(jìn)程創(chuàng)建、等待與終止
在 Linux 的世界里,進(jìn)程就像是一個(gè)個(gè)充滿活力的 “小生命”,它們是程序的執(zhí)行實(shí)例,承載著程序在系統(tǒng)中的運(yùn)行使命。簡(jiǎn)單來(lái)說(shuō),當(dāng)你在 Linux 系統(tǒng)中啟動(dòng)一個(gè)程序時(shí),系統(tǒng)就會(huì)為這個(gè)程序創(chuàng)建一個(gè)進(jìn)程,這個(gè)進(jìn)程包含了程序運(yùn)行所需的各種資源和環(huán)境信息,如內(nèi)存空間、文件描述符、CPU 時(shí)間等??梢哉f(shuō),進(jìn)程是程序在運(yùn)行時(shí)的具體體現(xiàn),是操作系統(tǒng)進(jìn)行資源分配和調(diào)度的基本單位。
每個(gè)進(jìn)程都有自己的 “身份標(biāo)識(shí)”,也就是進(jìn)程 ID(PID),系統(tǒng)通過(guò) PID 來(lái)唯一地識(shí)別和管理進(jìn)程,就像每個(gè)人都有一個(gè)獨(dú)一無(wú)二的身份證號(hào)碼一樣 。同時(shí),進(jìn)程還有自己的內(nèi)存空間,包括代碼、數(shù)據(jù)和堆棧等。通過(guò)這些內(nèi)存空間,進(jìn)程可以在其生命周期內(nèi)存儲(chǔ)狀態(tài)和數(shù)據(jù),并與其他進(jìn)程進(jìn)行通信。 進(jìn)程還有不同的狀態(tài),如運(yùn)行、阻塞、就緒等,這些狀態(tài)反映了進(jìn)程當(dāng)前的執(zhí)行情況和資源需求。打個(gè)比方,運(yùn)行狀態(tài)的進(jìn)程就像是正在賽道上全力奔跑的運(yùn)動(dòng)員;阻塞狀態(tài)的進(jìn)程則像是在等待某個(gè)條件滿足(比如等待數(shù)據(jù)讀取完成)而暫時(shí)停下腳步的運(yùn)動(dòng)員;就緒狀態(tài)的進(jìn)程就像是已經(jīng)做好起跑準(zhǔn)備,等待裁判發(fā)令的運(yùn)動(dòng)員 。
一、Linux進(jìn)程是什么?
進(jìn)程(Process)是指計(jì)算機(jī)中已運(yùn)行的程序,是系統(tǒng)進(jìn)行資源分配和調(diào)度的基本單位,是操作系統(tǒng)結(jié)構(gòu)的基礎(chǔ)。在早期面向進(jìn)程設(shè)計(jì)的計(jì)算機(jī)結(jié)構(gòu)中,進(jìn)程是程序的基本執(zhí)行實(shí)體;在當(dāng)代面向線程設(shè)計(jì)的計(jì)算機(jī)結(jié)構(gòu)中,進(jìn)程是線程的容器。進(jìn)程是程序真正運(yùn)行的實(shí)例,若干進(jìn)程可能與同一個(gè)程序相關(guān),且每個(gè)進(jìn)程皆可以同步或異步的方式獨(dú)立運(yùn)行。
- 狹義定義:進(jìn)程是正在運(yùn)行的程序的實(shí)例。
- 廣義定義:進(jìn)程是一個(gè)具有一定獨(dú)立功能的程序關(guān)于某個(gè)數(shù)據(jù)集合的一次運(yùn)行活動(dòng)。它是操作系統(tǒng)動(dòng)態(tài)執(zhí)行的基本單元,在傳統(tǒng)的操作系統(tǒng)中,進(jìn)程既是基本的分配單元,也是基本的執(zhí)行單元。
進(jìn)程的概念主要有兩點(diǎn):第一,進(jìn)程是一個(gè)實(shí)體。每一個(gè)進(jìn)程都有它自己的地址空間,一般情況下,包括文本區(qū)域(text region)、數(shù)據(jù)區(qū)域(data region)和堆棧(stack region)。文本區(qū)域存儲(chǔ)處理器執(zhí)行的代碼;數(shù)據(jù)區(qū)域存儲(chǔ)變量和進(jìn)程執(zhí)行期間使用的動(dòng)態(tài)分配的內(nèi)存;堆棧區(qū)域存儲(chǔ)著活動(dòng)過(guò)程調(diào)用的指令和本地變量。第二,進(jìn)程是一個(gè)“執(zhí)行中的程序”。程序是一個(gè)沒有生命的實(shí)體,只有處理器賦予程序生命時(shí)(操作系統(tǒng)執(zhí)行之),它才能成為一個(gè)活動(dòng)的實(shí)體,我們稱其為進(jìn)程。
1.1描述進(jìn)程PCB
進(jìn)程:資源的封裝單位,linux用一個(gè)PCB來(lái)描述進(jìn)程,即task_struct, 其包含mm,fs,files,signal…
(1)root目錄,是一個(gè)進(jìn)程概念,不是系統(tǒng)概念
apropos chroot
man chroot 2將分區(qū)/dev/sda5掛載到/mnt/a,調(diào)用chroot,改變r(jià)oot目錄,當(dāng)前進(jìn)程下的文件b.txt即位于當(dāng)前進(jìn)程的根目錄。
(2)fd也是進(jìn)程級(jí)概念
(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,系統(tǒng)全局概念
Linux總的PID是有限的,用完P(guān)ID
: ( ) { : ∣ : & } ; : :()\{:|:\&\};::(){:∣:&};:每個(gè)用戶的PID也是有限的
- ulimit -u 最大進(jìn)程數(shù)
- ulimit –a
(base) leon@leon-Laptop:/proc/29171$ cat /proc/sys/kernel/pid_max1.2 task_ struct內(nèi)容分類
在進(jìn)程執(zhí)行時(shí),任意給定一個(gè)時(shí)間,進(jìn)程都可以唯一的被表征為以下元素:
- 標(biāo)示符: 描述本進(jìn)程的唯一標(biāo)示符,?用來(lái)區(qū)別其他進(jìn)程。
- 狀態(tài): 任務(wù)狀態(tài),退出代碼,退出信號(hào)等。
- 優(yōu)先級(jí): 相對(duì)于其他進(jìn)程的優(yōu)先級(jí)。
- 程序計(jì)數(shù)器: 程序中即將被執(zhí)行的下一條指令的地址。
- 內(nèi)存指針: 包括程序代碼和進(jìn)程相關(guān)數(shù)據(jù)的指針,還有和其他進(jìn)程共享的內(nèi)存塊的指針
- 上下文數(shù)據(jù): 進(jìn)程執(zhí)行時(shí)處理器的寄存器中的數(shù)據(jù)
- I/O狀態(tài)信息: 包括顯?示的I/O請(qǐng)求,分配給進(jìn)程的I/O設(shè)備和被進(jìn)程使用的文件列表。
- 記賬信息: 可能包括處理器時(shí)間總和,使用的時(shí)鐘數(shù)總和,時(shí)間限制,記賬號(hào)等。
1.3Linux進(jìn)程的組織方式
linux里的多個(gè)進(jìn)程,其實(shí)就是管理多個(gè)task_struct,那他們是怎么組織聯(lián)系的呢?
組織task_struct的數(shù)據(jù)結(jié)構(gòu):
- a.鏈表,遍歷進(jìn)程
- b.樹:方便查找父子相關(guān)進(jìn)程
- c.哈希表:用于快速查找
用三種數(shù)據(jù)結(jié)構(gòu)來(lái)管理task_struct,以空間換時(shí)間。父進(jìn)程監(jiān)控子進(jìn)程,linux總是白發(fā)人送黑發(fā)人。父進(jìn)程通過(guò)wait,讀取task_struct的退出碼,得知進(jìn)程死亡原因。并且清理子進(jìn)程尸體。
Android/或者服務(wù)器,都會(huì)用由父進(jìn)程監(jiān)控子進(jìn)程狀態(tài),適時(shí)重啟等;
1.4進(jìn)程的狀態(tài)和轉(zhuǎn)換
(1)五種狀態(tài)
進(jìn)程在其生命周期內(nèi),由于系統(tǒng)中各進(jìn)程之間的相互制約關(guān)系及系統(tǒng)的運(yùn)行環(huán)境的變化,使得進(jìn)程的狀態(tài)也在不斷地發(fā)生變化(一個(gè)進(jìn)程會(huì)經(jīng)歷若干種不同狀態(tài))。通常進(jìn)程有以下五種狀態(tài),前三種是進(jìn)程的基本狀態(tài)。
- 運(yùn)行狀態(tài):進(jìn)程正在處理機(jī)上運(yùn)行。在單處理機(jī)環(huán)境下,每一時(shí)刻最多只有一個(gè)進(jìn)程處于運(yùn)行狀態(tài)。
- 就緒狀態(tài):進(jìn)程已處于準(zhǔn)備運(yùn)行的狀態(tài),即進(jìn)程獲得了除處理機(jī)之外的一切所需資源,一旦得到處理機(jī)即可運(yùn)行。
- 阻塞狀態(tài),又稱等待狀態(tài):進(jìn)程正在等待某一事件而暫停運(yùn)行,如等待某資源為可用(不包括處理機(jī))或等待輸入/輸出完成。即使處理機(jī)空閑,該進(jìn)程也不能運(yùn)行。
- 創(chuàng)建狀態(tài):進(jìn)程正在被創(chuàng)建,尚未轉(zhuǎn)到就緒狀態(tài)。創(chuàng)建進(jìn)程通常需要多個(gè)步驟:首先申請(qǐng)一個(gè)空白的PCB,并向PCB中填寫一些控制和管理進(jìn)程的信息;然后由系統(tǒng)為該進(jìn)程分 配運(yùn)行時(shí)所必需的資源;最后把該進(jìn)程轉(zhuǎn)入到就緒狀態(tài)。
- 結(jié)束狀態(tài):進(jìn)程正從系統(tǒng)中消失,這可能是進(jìn)程正常結(jié)束或其他原因中斷退出運(yùn)行。當(dāng)進(jìn)程需要結(jié)束運(yùn)行時(shí),系統(tǒng)首先必須置該進(jìn)程為結(jié)束狀態(tài),然后再進(jìn)一步處理資源釋放和 回收等工作。
注意區(qū)別就緒狀態(tài)和等待狀態(tài):就緒狀態(tài)是指進(jìn)程僅缺少處理機(jī),只要獲得處理機(jī)資源就立即執(zhí)行;而等待狀態(tài)是指進(jìn)程需要其他資源(除了處理機(jī))或等待某一事件。之所以把處理機(jī)和其他資源劃分開,是因?yàn)樵诜謺r(shí)系統(tǒng)的時(shí)間片輪轉(zhuǎn)機(jī)制中,每個(gè)進(jìn)程分到的時(shí)間片是若干毫秒。
也就是說(shuō),進(jìn)程得到處理機(jī)的時(shí)間很短且非常頻繁,進(jìn)程在運(yùn)行過(guò)程中實(shí)際上是頻繁地轉(zhuǎn)換到就緒狀態(tài)的;而其他資源(如外設(shè))的使用和分配或者某一事件的發(fā)生(如I/O操作的完成)對(duì)應(yīng)的時(shí)間相對(duì)來(lái)說(shuō)很長(zhǎng),進(jìn)程轉(zhuǎn)換到等待狀態(tài)的次數(shù)也相對(duì)較少。這樣來(lái)看,就緒狀態(tài)和等待狀態(tài)是進(jìn)程生命周期中兩個(gè)完全不同的狀態(tài),很顯然需要加以區(qū)分。
(2)狀態(tài)轉(zhuǎn)換
- 就緒狀態(tài) -> 運(yùn)行狀態(tài):處于就緒狀態(tài)的進(jìn)程被調(diào)度后,獲得處理機(jī)資源(分派處理機(jī)時(shí)間片),于是進(jìn)程由就緒狀態(tài)轉(zhuǎn)換為運(yùn)行狀態(tài)。
- 運(yùn)行狀態(tài) -> 就緒狀態(tài):處于運(yùn)行狀態(tài)的進(jìn)程在時(shí)間片用完后,不得不讓出處理機(jī),從而進(jìn)程由運(yùn)行狀態(tài)轉(zhuǎn)換為就緒狀態(tài)。此外,在可剝奪的操作系統(tǒng)中,當(dāng)有更高優(yōu)先級(jí)的進(jìn)程就 、 緒時(shí),調(diào)度程度將正執(zhí)行的進(jìn)程轉(zhuǎn)換為就緒狀態(tài),讓更高優(yōu)先級(jí)的進(jìn)程執(zhí)行。
- 運(yùn)行狀態(tài) -> 阻塞狀態(tài):當(dāng)進(jìn)程請(qǐng)求某一資源(如外設(shè))的使用和分配或等待某一事件的發(fā)生(如I/O操作的完成)時(shí),它就從運(yùn)行狀態(tài)轉(zhuǎn)換為阻塞狀態(tài)。進(jìn)程以系統(tǒng)調(diào)用的形式請(qǐng)求操作系統(tǒng)提供服務(wù),這是一種特殊的、由運(yùn)行用戶態(tài)程序調(diào)用操作系統(tǒng)內(nèi)核過(guò)程的形式。
- 阻塞狀態(tài) -> 就緒狀態(tài):當(dāng)進(jìn)程等待的事件到來(lái)時(shí) ,如I/O操作結(jié)束或中斷結(jié)束時(shí),中斷處理程序必須把相應(yīng)進(jìn)程的狀態(tài)由阻塞狀態(tài)轉(zhuǎn)換為就緒狀態(tài)。
二、Linux進(jìn)程的創(chuàng)建
在 Linux 中,有多種方式可以創(chuàng)建進(jìn)程,其中最常見的兩種方式是:通過(guò)運(yùn)行可執(zhí)行程序來(lái)創(chuàng)建進(jìn)程,以及使用系統(tǒng)調(diào)用接口來(lái)創(chuàng)建進(jìn)程 。當(dāng)我們?cè)诿钚兄休斎胍粋€(gè)可執(zhí)行程序的名稱并按下回車鍵時(shí),系統(tǒng)就會(huì)創(chuàng)建一個(gè)新的進(jìn)程來(lái)運(yùn)行這個(gè)程序。例如,當(dāng)我們輸入 “l(fā)s” 命令時(shí),系統(tǒng)會(huì)創(chuàng)建一個(gè)進(jìn)程來(lái)執(zhí)行 “l(fā)s” 程序,該進(jìn)程會(huì)讀取當(dāng)前目錄下的文件和目錄信息,并將其顯示在終端上 。這種方式創(chuàng)建進(jìn)程非常簡(jiǎn)單直接,我們?cè)谌粘J褂?Linux 系統(tǒng)時(shí)經(jīng)常會(huì)用到。
另一種常見的方式是使用系統(tǒng)調(diào)用接口,在 Linux 中,最常用的創(chuàng)建進(jìn)程的系統(tǒng)調(diào)用是 fork () 。fork () 函數(shù)就像是一個(gè)神奇的 “分身術(shù)”,它可以從一個(gè)已存在的進(jìn)程(父進(jìn)程)中創(chuàng)建出一個(gè)新的進(jìn)程(子進(jìn)程),這個(gè)新創(chuàng)建的子進(jìn)程幾乎是父進(jìn)程的一個(gè)完全拷貝 。通過(guò)使用 fork () 函數(shù),我們可以在程序中靈活地創(chuàng)建新的進(jìn)程,實(shí)現(xiàn)多任務(wù)處理等功能。 除此之外,還有一些其他的系統(tǒng)調(diào)用函數(shù),如 vfork () 和 clone (),它們也可以用于創(chuàng)建進(jìn)程,不過(guò)它們的使用場(chǎng)景和功能略有不同 。
2.1fork () 函數(shù)
fork函數(shù)的原型非常簡(jiǎn)潔:pid_t fork(void); 。這個(gè)函數(shù)就像是一個(gè)神奇的開關(guān),當(dāng)它被調(diào)用時(shí),會(huì)在操作系統(tǒng)中引發(fā)一系列奇妙的變化。它會(huì)創(chuàng)建一個(gè)新的進(jìn)程,這個(gè)新進(jìn)程就是子進(jìn)程,而調(diào)用fork的進(jìn)程則是父進(jìn)程。
從實(shí)現(xiàn)原理上看,fork函數(shù)會(huì)復(fù)制父進(jìn)程的幾乎所有資源,包括虛擬地址空間、堆棧、打開的文件描述符等。在虛擬地址空間方面,父子進(jìn)程各自擁有自己獨(dú)立的虛擬地址空間,但它們共享代碼段(因?yàn)榇a段通常是只讀的,不需要為每個(gè)進(jìn)程單獨(dú)復(fù)制一份)。這就好比父子倆住在各自的房子里(虛擬地址空間),但他們共享同一個(gè)圖書館(代碼段) 。
在早期的 Unix 系統(tǒng)中,fork創(chuàng)建子進(jìn)程時(shí)會(huì)直接復(fù)制父進(jìn)程的整個(gè)地址空間,這會(huì)導(dǎo)致大量的內(nèi)存拷貝操作,效率非常低下。后來(lái)引入了寫時(shí)拷貝(Copy-On-Write,COW)技術(shù),大大提高了fork的效率。寫時(shí)拷貝技術(shù)的原理是,在fork創(chuàng)建子進(jìn)程時(shí),并不立即復(fù)制父進(jìn)程的地址空間,而是讓父子進(jìn)程共享相同的物理內(nèi)存頁(yè)面。只有當(dāng)其中一個(gè)進(jìn)程試圖修改共享的內(nèi)存頁(yè)面時(shí),系統(tǒng)才會(huì)為修改的頁(yè)面創(chuàng)建一個(gè)副本,分別分配給父子進(jìn)程。這就好比父子倆一開始共享同一本書(物理內(nèi)存頁(yè)面),當(dāng)其中一個(gè)人想要在書上做筆記(修改內(nèi)存頁(yè)面)時(shí),才會(huì)復(fù)制一本新的書給他 。
fork () 函數(shù)是 Linux 系統(tǒng)中創(chuàng)建進(jìn)程的核心函數(shù),它的作用是從一個(gè)已存在的進(jìn)程中創(chuàng)建一個(gè)新的進(jìn)程 。這個(gè)新創(chuàng)建的進(jìn)程被稱為子進(jìn)程,而原來(lái)的進(jìn)程則被稱為父進(jìn)程 。fork () 函數(shù)的使用非常簡(jiǎn)單,只需要在程序中調(diào)用 fork () 函數(shù)即可。例如:
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid;
pid = fork();
if (pid == 0) {
// 子進(jìn)程執(zhí)行的代碼
printf("這是子進(jìn)程,我的PID是:%d\n", getpid());
} else if (pid > 0) {
// 父進(jìn)程執(zhí)行的代碼
printf("這是父進(jìn)程,我的PID是:%d,我創(chuàng)建的子進(jìn)程的PID是:%d\n", getpid(), pid);
} else {
// fork()函數(shù)調(diào)用失敗
perror("fork");
return 1;
}
return 0;
}在這個(gè)例子中,我們調(diào)用 fork () 函數(shù)創(chuàng)建了一個(gè)子進(jìn)程。fork () 函數(shù)返回后,會(huì)有兩個(gè)進(jìn)程在執(zhí)行,一個(gè)是父進(jìn)程,一個(gè)是子進(jìn)程 。父進(jìn)程和子進(jìn)程從 fork () 函數(shù)返回后,會(huì)根據(jù) fork () 函數(shù)的返回值來(lái)區(qū)分自己是父進(jìn)程還是子進(jìn)程 。如果返回值為 0,則表示當(dāng)前進(jìn)程是子進(jìn)程;如果返回值大于 0,則表示當(dāng)前進(jìn)程是父進(jìn)程,返回值就是子進(jìn)程的 PID;如果返回值小于 0,則表示 fork () 函數(shù)調(diào)用失敗 。
fork () 函數(shù)在創(chuàng)建子進(jìn)程時(shí),會(huì)在內(nèi)核中進(jìn)行一系列復(fù)雜的操作 。內(nèi)核會(huì)為子進(jìn)程分配一個(gè)新的進(jìn)程控制塊(PCB),這個(gè) PCB 就像是子進(jìn)程的 “身份證”,里面記錄了子進(jìn)程的各種信息,如進(jìn)程 ID、狀態(tài)、優(yōu)先級(jí)、內(nèi)存映射等 。同時(shí),內(nèi)核還會(huì)為子進(jìn)程分配獨(dú)立的內(nèi)存空間,包括代碼段、數(shù)據(jù)段、堆棧段等 。不過(guò),在 Linux 系統(tǒng)中,為了提高效率,子進(jìn)程并不會(huì)立即復(fù)制父進(jìn)程的所有內(nèi)存內(nèi)容,而是采用了一種寫時(shí)復(fù)制(Copy - on - Write,COW)的技術(shù) 。也就是說(shuō),在子進(jìn)程創(chuàng)建初期,子進(jìn)程和父進(jìn)程共享相同的內(nèi)存頁(yè)面,只有當(dāng)子進(jìn)程或父進(jìn)程對(duì)某個(gè)內(nèi)存頁(yè)面進(jìn)行寫操作時(shí),系統(tǒng)才會(huì)為寫操作的進(jìn)程復(fù)制一份該內(nèi)存頁(yè)面的副本,從而保證兩個(gè)進(jìn)程的內(nèi)存獨(dú)立性 。
子進(jìn)程創(chuàng)建后,它和父進(jìn)程之間的關(guān)系就像是父子關(guān)系一樣 。子進(jìn)程會(huì)繼承父進(jìn)程的許多屬性和資源,如打開的文件描述符、信號(hào)處理方式、當(dāng)前工作目錄等 。不過(guò),子進(jìn)程也有一些自己獨(dú)有的屬性,如進(jìn)程 ID、父進(jìn)程 ID 等 。子進(jìn)程的父進(jìn)程 ID 就是創(chuàng)建它的父進(jìn)程的進(jìn)程 ID 。通過(guò)這種父子關(guān)系,系統(tǒng)可以方便地管理和調(diào)度進(jìn)程 。 例如,父進(jìn)程可以通過(guò) wait () 函數(shù)等待子進(jìn)程結(jié)束,并獲取子進(jìn)程的退出狀態(tài);子進(jìn)程也可以通過(guò) exec () 函數(shù)族來(lái)執(zhí)行一個(gè)新的程序,從而替換自己的代碼和數(shù)據(jù) 。
其他資源大體與fs類似,最復(fù)雜的是mm拷貝,需借助MMU來(lái)完成拷貝;
即寫時(shí)拷貝技術(shù):
#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;
}寫時(shí)拷貝技術(shù)帶來(lái)了很多好處。首先,它節(jié)省了內(nèi)存開銷,因?yàn)樵诖蠖鄶?shù)情況下,父子進(jìn)程在fork之后并不會(huì)立即修改共享的內(nèi)存,所以不需要一開始就復(fù)制大量的內(nèi)存。其次,它提高了進(jìn)程創(chuàng)建的效率,減少了fork操作的時(shí)間開銷 。
第一階段:只有一個(gè)進(jìn)程P1,數(shù)據(jù)段可讀可寫:
第二階段,調(diào)用fork之后創(chuàng)建子進(jìn)程P2,P2完全拷貝一份P1的mm_struct,其指針指向相同地址,即P1/P2虛擬地址,物理地址完全相同,但該內(nèi)存的頁(yè)表地址變?yōu)橹蛔x;
第三階段:當(dāng)P2改寫data時(shí),子進(jìn)程改寫只讀內(nèi)存,會(huì)引起內(nèi)存缺頁(yè)中斷,在ISR中申請(qǐng)一片新內(nèi)存,通常是4K,把P1進(jìn)程的data拷貝到這4K新內(nèi)存。再修改頁(yè)表,改變虛實(shí)地址轉(zhuǎn)換關(guān)系,使物理地址指向新申請(qǐng)的4K,這樣子進(jìn)程P2就得到新的4K內(nèi)存,并修改權(quán)限為可讀寫,然后從中斷返回到P2進(jìn)程寫data才會(huì)成功。整個(gè)過(guò)程虛擬地址不變,對(duì)應(yīng)用程序員來(lái)說(shuō),感覺不到地址變化。
誰(shuí)先寫,誰(shuí)申請(qǐng)新物理內(nèi)存;Data=20;這句代碼經(jīng)過(guò)了賦值無(wú)寫權(quán)限,引起缺頁(yè)中斷,申請(qǐng)內(nèi)存,修改頁(yè)表,拷貝數(shù)據(jù)…回到data=20再次賦值,所以整個(gè)執(zhí)行時(shí)間會(huì)很長(zhǎng)。
這就是linux中的寫時(shí)拷貝技術(shù)(copy on write), 誰(shuí)先寫誰(shuí)申請(qǐng)新內(nèi)存,沒有優(yōu)先順序;cow依賴硬件MMU實(shí)現(xiàn),沒有MMU的系統(tǒng)就沒法實(shí)現(xiàn)cow,也就不支持fork函數(shù),只有vfork;
2.2vfork () 函數(shù)
vfork () 函數(shù)也是 Linux 系統(tǒng)中用于創(chuàng)建進(jìn)程的函數(shù),它和 fork () 函數(shù)非常相似,但也有一些重要的區(qū)別 。vfork () 函數(shù)創(chuàng)建的子進(jìn)程與父進(jìn)程共享數(shù)據(jù)段,而不是像 fork () 函數(shù)那樣子進(jìn)程拷貝父進(jìn)程的數(shù)據(jù)段 。這意味著在子進(jìn)程調(diào)用 exec () 或 exit () 之前,子進(jìn)程和父進(jìn)程的數(shù)據(jù)是共享的,子進(jìn)程對(duì)數(shù)據(jù)的修改會(huì)直接影響到父進(jìn)程 。vfork () 函數(shù)保證子進(jìn)程先運(yùn)行,在子進(jìn)程調(diào)用 exec () 或 exit () 之后,父進(jìn)程才可能被調(diào)度運(yùn)行 。這與 fork () 函數(shù)不同,fork () 函數(shù)創(chuàng)建的父子進(jìn)程的執(zhí)行次序是不確定的 。
vfork () 函數(shù)的使用場(chǎng)景相對(duì)較少,主要用于當(dāng)子進(jìn)程需要立即執(zhí)行 exec () 函數(shù)族中的某個(gè)函數(shù),替換自身的代碼和數(shù)據(jù)時(shí) 。因?yàn)樵谶@種情況下,子進(jìn)程不需要自己獨(dú)立的數(shù)據(jù)段,共享父進(jìn)程的數(shù)據(jù)段可以節(jié)省內(nèi)存和時(shí)間開銷 。不過(guò),由于 vfork () 函數(shù)中子進(jìn)程和父進(jìn)程共享數(shù)據(jù)段,且子進(jìn)程先運(yùn)行,如果在子進(jìn)程調(diào)用 exec () 或 exit () 之前,子進(jìn)程依賴于父進(jìn)程的進(jìn)一步動(dòng)作,就可能會(huì)導(dǎo)致死鎖 。所以在使用 vfork () 函數(shù)時(shí)需要特別小心,確保子進(jìn)程能夠及時(shí)調(diào)用 exec () 或 exit () 。
2.3clone () 函數(shù)
clone () 函數(shù)是 Linux 系統(tǒng)中另一個(gè)用于創(chuàng)建進(jìn)程的系統(tǒng)調(diào)用,它比 fork () 和 vfork () 函數(shù)更加靈活和強(qiáng)大 。clone () 函數(shù)可以創(chuàng)建一個(gè)新的進(jìn)程,并且可以指定新進(jìn)程與調(diào)用進(jìn)程之間共享的資源,如文件描述符、內(nèi)存空間、信號(hào)處理等 。這使得 clone () 函數(shù)不僅可以用于創(chuàng)建普通的進(jìn)程,還可以用于創(chuàng)建線程 。在 Linux 系統(tǒng)中,線程實(shí)際上就是一種特殊的進(jìn)程,它們共享同一個(gè)進(jìn)程的地址空間和其他資源 。clone () 函數(shù)的原型如下:
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);其中,fn 是一個(gè)函數(shù)指針,指向新進(jìn)程(或線程)開始執(zhí)行的函數(shù);child_stack 是新進(jìn)程(或線程)使用的堆棧指針;flags 是一個(gè)標(biāo)志位,用于指定新進(jìn)程與調(diào)用進(jìn)程之間共享的資源;arg 是傳遞給 fn 函數(shù)的參數(shù) 。通過(guò)設(shè)置不同的 flags 標(biāo)志位,可以實(shí)現(xiàn)不同的共享策略 。例如,如果設(shè)置 CLONE_VM 標(biāo)志位,則新進(jìn)程與調(diào)用進(jìn)程共享同一個(gè)內(nèi)存空間,這就相當(dāng)于創(chuàng)建了一個(gè)線程;如果不設(shè)置 CLONE_VM 標(biāo)志位,則新進(jìn)程擁有自己獨(dú)立的內(nèi)存空間,這就相當(dāng)于創(chuàng)建了一個(gè)普通的進(jìn)程 。clone () 函數(shù)的使用相對(duì)復(fù)雜一些,需要對(duì) Linux 系統(tǒng)的進(jìn)程和內(nèi)存管理有深入的了解 。不過(guò),它提供了更高的靈活性和控制權(quán),適用于一些對(duì)進(jìn)程創(chuàng)建有特殊需求的場(chǎng)景 。
2.4內(nèi)核線程
內(nèi)核線程是獨(dú)立運(yùn)行在內(nèi)核空間的特殊進(jìn)程,它的運(yùn)行不受用戶空間的干擾,就像在操作系統(tǒng)內(nèi)核這個(gè)神秘世界里的 “隱形工作者”,默默地執(zhí)行著一些關(guān)鍵的系統(tǒng)任務(wù) 。內(nèi)核線程與普通進(jìn)程相比,有著獨(dú)特的性質(zhì) 。它沒有獨(dú)立的地址空間,mm 指針被設(shè)置為 NULL 。這意味著它不能像普通進(jìn)程那樣訪問(wèn)用戶空間的內(nèi)存,只能在內(nèi)核空間中活動(dòng) 。
內(nèi)核線程只在內(nèi)核態(tài)運(yùn)行,從來(lái)不切換到用戶空間去 。這使得它的運(yùn)行環(huán)境相對(duì)單純,避免了用戶空間的復(fù)雜性和潛在的干擾 。不過(guò),內(nèi)核線程和普通進(jìn)程一樣,可以被調(diào)度,也可以被搶占 。這保證了它能夠在合適的時(shí)機(jī)得到 CPU 的執(zhí)行時(shí)間,完成自己的任務(wù) 。
do_fork () 函數(shù):
在 Linux 系統(tǒng)中,無(wú)論是普通進(jìn)程還是內(nèi)核線程的創(chuàng)建,最終都離不開一個(gè)關(guān)鍵的函數(shù) ——do_fork () 。這個(gè)函數(shù)就像是進(jìn)程創(chuàng)建的 “幕后大導(dǎo)演”,負(fù)責(zé)協(xié)調(diào)和執(zhí)行一系列復(fù)雜的操作,確保新的進(jìn)程或內(nèi)核線程能夠順利誕生 。do_fork () 函數(shù)的主要功能是生成一個(gè)子進(jìn)程,并把它加入到 CPU 就緒隊(duì)列,等待 CPU 調(diào)度 。在這個(gè)過(guò)程中,它會(huì)調(diào)用 copy_process () 函數(shù),從函數(shù)名就可以看出,這個(gè)函數(shù)的作用是將父進(jìn)程的相關(guān)資源復(fù)制到子進(jìn)程,執(zhí)行生成子進(jìn)程的工作 。
具體來(lái)說(shuō),copy_process () 函數(shù)會(huì)為子進(jìn)程分配一個(gè)新的 task_struct 內(nèi)存空間,task_struct 就像是進(jìn)程的 “身份證”,里面記錄了進(jìn)程的各種信息 。同時(shí),還會(huì)為子進(jìn)程分配兩個(gè)內(nèi)存頁(yè)(32 位操作系統(tǒng)中為 8KB),用于存放 thread_union 聯(lián)合 。這個(gè)聯(lián)合包含兩個(gè)成員,一個(gè)是 thread_info 結(jié)構(gòu),內(nèi)核通過(guò)該結(jié)構(gòu)能夠快速獲得進(jìn)程結(jié)構(gòu)體 task_struct;另一個(gè)是 stack 結(jié)構(gòu),用于保存進(jìn)程內(nèi)核棧 。
除了資源復(fù)制,do_fork () 函數(shù)還會(huì)為新進(jìn)程分配唯一的進(jìn)程 ID(PID) 。PID 就像是進(jìn)程的 “學(xué)號(hào)”,系統(tǒng)通過(guò)它來(lái)唯一地識(shí)別和管理進(jìn)程 。do_fork () 函數(shù)會(huì)將新進(jìn)程加入到 CPU 就緒隊(duì)列 。就緒隊(duì)列就像是一個(gè) “等待執(zhí)行的隊(duì)伍”,新創(chuàng)建的進(jìn)程會(huì)在這里排隊(duì),等待 CPU 的調(diào)度,獲得執(zhí)行的機(jī)會(huì) 。
三、Linux進(jìn)程終止
就像任何生命都有終結(jié)的時(shí)刻一樣,Linux 進(jìn)程也會(huì)迎來(lái)它的終止。進(jìn)程終止是指操作系統(tǒng)將正在運(yùn)行的程序結(jié)束掉的過(guò)程。當(dāng)進(jìn)程終止時(shí),操作系統(tǒng)會(huì)回收該進(jìn)程所占用的系統(tǒng)資源,如內(nèi)存空間、文件描述符、CPU 資源等,確保系統(tǒng)資源高效利用 。進(jìn)程終止的原因多種多樣,總體可以分為正常終止和異常終止兩大類。
3.1進(jìn)程終止的含義與場(chǎng)景
進(jìn)程終止意味著進(jìn)程生命周期的結(jié)束,它標(biāo)志著進(jìn)程不再執(zhí)行任何指令,操作系統(tǒng)會(huì)回收進(jìn)程占用的所有資源,將其從系統(tǒng)中移除 。正常終止通常是進(jìn)程完成了它被設(shè)計(jì)要執(zhí)行的任務(wù)后,主動(dòng)請(qǐng)求操作系統(tǒng)終止運(yùn)行。比如,當(dāng)我們運(yùn)行一個(gè)計(jì)算 1 到 100 之和的程序,程序計(jì)算完成并輸出結(jié)果后,就會(huì)正常終止 。此時(shí),進(jìn)程的退出狀態(tài)通常為 0,表示成功退出 。
而異常終止則是指進(jìn)程在運(yùn)行過(guò)程中遇到了無(wú)法處理的錯(cuò)誤或被外部信號(hào)強(qiáng)制終止 。例如,程序試圖訪問(wèn)一個(gè)不存在的文件,并且沒有合適的錯(cuò)誤處理機(jī)制,可能會(huì)因?yàn)槲募x取錯(cuò)誤而崩潰終止;或者進(jìn)程接收到某些信號(hào),如 SIGINT(通常由 Ctrl+C 觸發(fā))、SIGKILL(無(wú)法被捕獲或忽略)等,也會(huì)導(dǎo)致進(jìn)程異常終止 。異常終止時(shí),進(jìn)程的退出狀態(tài)通常為非零值,具體值取決于錯(cuò)誤的類型或信號(hào)的編號(hào) 。
3.2正常終止的方式
在 Linux 中,進(jìn)程正常終止有幾種常見的方式 。一種是在 main 函數(shù)內(nèi)執(zhí)行 return 語(yǔ)句,return 語(yǔ)句的返回值會(huì)作為進(jìn)程的退出碼 。例如,在下面的代碼中,return 0 表示進(jìn)程正常結(jié)束:
#include <stdio.h>
int main() {
printf("程序執(zhí)行中...\n");
return 0;
}另一種方式是調(diào)用 exit 函數(shù),exit 函數(shù)是一個(gè)標(biāo)準(zhǔn)庫(kù)函數(shù),定義在<stdlib.h>頭文件中 。它用于正?;虍惓5亟K止程序,并執(zhí)行一些清理操作 。在調(diào)用 exit 時(shí),程序會(huì)執(zhí)行以下操作:調(diào)用所有已注冊(cè)的 atexit 函數(shù),這些函數(shù)可以用于釋放資源、關(guān)閉文件等;刷新所有輸出緩沖區(qū),確保所有數(shù)據(jù)都被寫入;關(guān)閉所有打開的文件描述符 。例如:
#include <stdio.h>
#include <stdlib.h>
void cleanup() {
printf("執(zhí)行清理函數(shù)...\n");
}
int main() {
// 注冊(cè)清理函數(shù)
atexit(cleanup);
printf("測(cè)試緩沖區(qū)行為");
exit(0);
}還有一種方式是調(diào)用_exit 或_Exit 函數(shù),它們是系統(tǒng)調(diào)用,定義在<unistd.h>頭文件中 。_exit 和_Exit 函數(shù)用于立即終止程序,不執(zhí)行任何清理操作 。這意味著它們不會(huì)調(diào)用通過(guò) atexit 注冊(cè)的函數(shù),也不會(huì)刷新輸出緩沖區(qū) 。例如:
#include <stdio.h>
#include <unistd.h>
int main() {
printf("使用_exit...\n");
printf("這是緩沖區(qū)中的內(nèi)容。");
_exit(0);
}3.3異常終止的原因
進(jìn)程異常終止通常是由程序錯(cuò)誤、資源問(wèn)題或信號(hào)等原因?qū)е碌?。程序錯(cuò)誤是導(dǎo)致進(jìn)程異常終止的常見原因之一,例如段錯(cuò)誤(Segmentation Fault),當(dāng)程序試圖訪問(wèn)它沒有權(quán)限訪問(wèn)的內(nèi)存地址,如空指針引用或者越界訪問(wèn)數(shù)組時(shí),就會(huì)發(fā)生段錯(cuò)誤 。以下是一個(gè)段錯(cuò)誤的示例代碼:
#include <stdio.h>
int main() {
int *ptr = NULL;
*ptr = 100; // 空指針解引用,會(huì)導(dǎo)致段錯(cuò)誤
return 0;
}除零錯(cuò)誤也是一種常見的程序錯(cuò)誤,當(dāng)程序嘗試除以零時(shí),就會(huì)引發(fā)除零錯(cuò)誤 。例如:
#include <stdio.h>
int main() {
int a = 10;
int b = 0;
int c = a / b; // 除零操作,會(huì)導(dǎo)致程序異常終止
return 0;
}資源問(wèn)題也可能導(dǎo)致進(jìn)程異常終止 。當(dāng)進(jìn)程使用的資源,如內(nèi)存、文件描述符等,超過(guò)了系統(tǒng)設(shè)定的限制時(shí),就會(huì)出現(xiàn)資源不足的情況 。例如,當(dāng)進(jìn)程申請(qǐng)的內(nèi)存空間超過(guò)了系統(tǒng)可用內(nèi)存時(shí),就會(huì)導(dǎo)致內(nèi)存耗盡,進(jìn)程可能會(huì)被操作系統(tǒng)終止 。信號(hào)也是導(dǎo)致進(jìn)程異常終止的一個(gè)重要原因 。在 Linux 系統(tǒng)中,有許多不同類型的信號(hào),其中一些信號(hào)是致命的,會(huì)導(dǎo)致進(jìn)程立即終止 。
例如,SIGSEGV 信號(hào)表示段錯(cuò)誤,當(dāng)進(jìn)程發(fā)生段錯(cuò)誤時(shí),操作系統(tǒng)會(huì)向該進(jìn)程發(fā)送 SIGSEGV 信號(hào),導(dǎo)致進(jìn)程異常終止 ;SIGABRT 信號(hào)表示程序異常終止,通常是由 abort 函數(shù)調(diào)用或其他嚴(yán)重錯(cuò)誤引起的 。還有一些非致命信號(hào),如 SIGINT(通常由 Ctrl+C 觸發(fā))用于中斷進(jìn)程,SIGHUP 用于通知進(jìn)程掛起 。這些信號(hào)可以被進(jìn)程捕獲并處理,如果進(jìn)程沒有處理這些信號(hào),它們也可能導(dǎo)致進(jìn)程異常終止 。
3.4僵尸進(jìn)程與托孤進(jìn)程
(1)僵尸進(jìn)程
在進(jìn)程的世界里,有兩種特殊的進(jìn)程狀態(tài),那就是僵尸進(jìn)程和托孤進(jìn)程,它們有著獨(dú)特的性質(zhì)和特點(diǎn) 。僵尸進(jìn)程是指一個(gè)子進(jìn)程已經(jīng)終止,但其父進(jìn)程尚未調(diào)用 wait () 或 waitpid () 系統(tǒng)調(diào)用來(lái)獲取子進(jìn)程的終止?fàn)顟B(tài),導(dǎo)致子進(jìn)程的進(jìn)程描述符仍然存在于系統(tǒng)中 。簡(jiǎn)單來(lái)說(shuō),僵尸進(jìn)程就像是一個(gè)已經(jīng) “死亡” 但還沒有被 “埋葬” 的進(jìn)程,它雖然不再占用 CPU 等運(yùn)行資源,但仍然占據(jù)著進(jìn)程表中的一個(gè)位置,消耗著系統(tǒng)的一些資源 。僵尸進(jìn)程的存在可能會(huì)導(dǎo)致一些問(wèn)題,比如如果系統(tǒng)中存在大量的僵尸進(jìn)程,可能會(huì)耗盡系統(tǒng)的進(jìn)程 ID 資源,因?yàn)槊總€(gè)進(jìn)程都需要一個(gè)唯一的進(jìn)程 ID 。
僵死進(jìn)程,也被稱為僵尸進(jìn)程,是 Linux 系統(tǒng)中一種特殊的進(jìn)程狀態(tài) 。當(dāng)子進(jìn)程先于父進(jìn)程退出,且父進(jìn)程沒有及時(shí)讀取子進(jìn)程的退出狀態(tài)時(shí),子進(jìn)程就會(huì)進(jìn)入僵死狀態(tài),成為僵死進(jìn)程 。這就好比一個(gè)孩子提前離開了舞臺(tái),但家長(zhǎng)卻沒有來(lái)接他,他只能在舞臺(tái)邊等待 。
僵死進(jìn)程會(huì)在系統(tǒng)中保留其進(jìn)程描述符、進(jìn)程 ID 等信息,雖然它不再占用大量的系統(tǒng)資源,但如果大量的僵死進(jìn)程存在,會(huì)占用有限的進(jìn)程 ID 資源,導(dǎo)致系統(tǒng)無(wú)法創(chuàng)建新的進(jìn)程 。這就像是舞臺(tái)邊擠滿了等待家長(zhǎng)的孩子,使得新的演員無(wú)法上臺(tái)表演 。
為了避免僵死進(jìn)程的產(chǎn)生,可以采取以下幾種方法 。父進(jìn)程可以調(diào)用wait系列函數(shù)(如wait、waitpid)來(lái)等待子進(jìn)程結(jié)束,并獲取子進(jìn)程的退出狀態(tài) 。wait函數(shù)會(huì)使父進(jìn)程阻塞,直到有子進(jìn)程退出,然后它會(huì)收集子進(jìn)程的信息,并把它徹底銷毀 。waitpid函數(shù)則更加靈活,它可以指定等待特定的子進(jìn)程,并且可以設(shè)置非阻塞模式 。這就好比家長(zhǎng)在孩子表演結(jié)束后,及時(shí)到舞臺(tái)邊接孩子,將孩子安全地帶回家 。
父進(jìn)程可以安裝SIGCHLD信號(hào)的處理函數(shù) 。當(dāng)子進(jìn)程退出時(shí),系統(tǒng)會(huì)向父進(jìn)程發(fā)送SIGCHLD信號(hào),父進(jìn)程可以在信號(hào)處理函數(shù)中調(diào)用waitpid函數(shù)來(lái)處理子進(jìn)程的退出,這樣可以避免父進(jìn)程阻塞,提高程序的并發(fā)性能 。這就好比家長(zhǎng)給孩子設(shè)置了一個(gè)信號(hào)器,當(dāng)孩子表演結(jié)束時(shí),信號(hào)器會(huì)通知家長(zhǎng),家長(zhǎng)可以及時(shí)去接孩子 。
還可以使用 “兩次fork” 的技巧 。父進(jìn)程先f(wàn)ork出一個(gè)子進(jìn)程,然后子進(jìn)程再fork出一個(gè)孫子進(jìn)程,接著子進(jìn)程立即exit退出 。這樣,孫子進(jìn)程就會(huì)成為孤兒進(jìn)程,被init進(jìn)程收養(yǎng),init進(jìn)程會(huì)負(fù)責(zé)清理孫子進(jìn)程,從而避免了僵死進(jìn)程的產(chǎn)生 。這就好比家長(zhǎng)讓孩子先找到一個(gè)臨時(shí)監(jiān)護(hù)人,然后自己離開,臨時(shí)監(jiān)護(hù)人會(huì)照顧好孩子,確保孩子不會(huì)無(wú)人照料 。通過(guò)這些方法,可以有效地避免僵死進(jìn)程的產(chǎn)生,保證系統(tǒng)的穩(wěn)定運(yùn)行 。
而托孤進(jìn)程,也就是我們常說(shuō)的孤兒進(jìn)程,是指其父進(jìn)程已經(jīng)終止或不存在,但是該進(jìn)程仍在繼續(xù)運(yùn)行的進(jìn)程 。當(dāng)一個(gè)父進(jìn)程創(chuàng)建了一個(gè)子進(jìn)程后,如果父進(jìn)程先于子進(jìn)程結(jié)束,那么這個(gè)子進(jìn)程就會(huì)成為孤兒進(jìn)程 。不過(guò),不用擔(dān)心,在 Linux 系統(tǒng)中,孤兒進(jìn)程會(huì)被 init 進(jìn)程(進(jìn)程號(hào)為 1)收養(yǎng) 。init 進(jìn)程就像是一個(gè) “超級(jí)奶爸”,會(huì)負(fù)責(zé)監(jiān)控和清理這些孤兒進(jìn)程,當(dāng)孤兒進(jìn)程結(jié)束時(shí),init 進(jìn)程會(huì)回收其占用的資源 。所以,一般情況下,孤兒進(jìn)程不會(huì)對(duì)系統(tǒng)造成嚴(yán)重的不良影響 。
(2)進(jìn)程 0 和進(jìn)程 1
在 Linux 系統(tǒng)的進(jìn)程家族中,進(jìn)程 0 和進(jìn)程 1 有著特殊的地位,它們是整個(gè)進(jìn)程體系的基礎(chǔ)和起點(diǎn) 。進(jìn)程 0 是內(nèi)核啟動(dòng)后創(chuàng)建的第一個(gè)進(jìn)程,通常被稱為 idle 進(jìn)程或 swapper 。它主要負(fù)責(zé) CPU 空閑時(shí)的調(diào)度工作 。當(dāng)系統(tǒng)中沒有其他可運(yùn)行的進(jìn)程時(shí),進(jìn)程 0 就會(huì)被調(diào)度運(yùn)行,它會(huì)讓 CPU 進(jìn)入低功耗模式,以節(jié)省能源,直到有新的進(jìn)程需要運(yùn)行時(shí)才會(huì)被喚醒 。進(jìn)程 0 在系統(tǒng)啟動(dòng)階段還扮演著重要的角色,它通過(guò)kernel_init () 函數(shù)創(chuàng)建了進(jìn)程 1 。
進(jìn)程 1,也就是 init 進(jìn)程,是所有用戶空間進(jìn)程的祖先 。它以 root 權(quán)限運(yùn)行,但受到用戶空間的一些限制 。init 進(jìn)程的主要職責(zé)是進(jìn)行系統(tǒng)初始化工作,它會(huì)加載初始化腳本,啟動(dòng)關(guān)鍵的系統(tǒng)服務(wù),比如網(wǎng)絡(luò)服務(wù)、日志服務(wù)、SSH 服務(wù)等 。init 進(jìn)程還負(fù)責(zé)回收孤兒進(jìn)程的資源,防止僵尸進(jìn)程的累積 。在不同的 Linux 系統(tǒng)中,init 進(jìn)程的實(shí)現(xiàn)可能會(huì)有所不同,傳統(tǒng)的 SysVinit 是基于 Shell 腳本的啟動(dòng)方式,逐級(jí)執(zhí)行 /etc/rc.d/rcX.d/ 中的腳本(X 為運(yùn)行級(jí)別);而現(xiàn)代的 systemd 則采用并行啟動(dòng)服務(wù)的方式,通過(guò)單元文件(.service)管理依賴關(guān)系,提供更快的啟動(dòng)速度和更強(qiáng)大的狀態(tài)監(jiān)控功能 。
3.5exit () 與_exit () 的區(qū)別
exit () 函數(shù)和_exit () 函數(shù)都用于終止進(jìn)程,但它們?cè)诠δ芎褪褂脠?chǎng)景上有一些明顯的差異 。exit () 函數(shù)是一個(gè)標(biāo)準(zhǔn)庫(kù)函數(shù),它在終止進(jìn)程之前會(huì)執(zhí)行一系列的清理操作 。它會(huì)調(diào)用通過(guò) atexit 函數(shù)注冊(cè)的清理函數(shù),這些清理函數(shù)可以用于釋放資源、關(guān)閉文件等;它會(huì)刷新所有輸出緩沖區(qū),確保所有數(shù)據(jù)都被寫入文件 。例如,當(dāng)我們使用 printf 函數(shù)輸出數(shù)據(jù)時(shí),數(shù)據(jù)可能會(huì)先被存儲(chǔ)在緩沖區(qū)中,直到遇到換行符或緩沖區(qū)滿時(shí)才會(huì)被真正寫入輸出設(shè)備 。如果在調(diào)用 exit () 函數(shù)之前有未刷新的緩沖區(qū)數(shù)據(jù),exit () 函數(shù)會(huì)將這些數(shù)據(jù)寫入輸出設(shè)備 。exit () 函數(shù)還會(huì)關(guān)閉所有打開的文件描述符,確保文件操作的完整性 。
_exit () 函數(shù)是一個(gè)系統(tǒng)調(diào)用,它直接在內(nèi)核層面終止進(jìn)程,不會(huì)執(zhí)行任何用戶空間的清理操作 。它不會(huì)調(diào)用 atexit 注冊(cè)的函數(shù),也不會(huì)刷新輸出緩沖區(qū),直接將進(jìn)程終止 。由于_exit () 函數(shù)不進(jìn)行任何清理操作,它的執(zhí)行速度比 exit () 函數(shù)更快 。在需要快速終止進(jìn)程,且不關(guān)心資源清理和緩沖區(qū)數(shù)據(jù)的情況下,可以使用_exit () 函數(shù) 。例如,在子進(jìn)程中調(diào)用 fork 后,如果子進(jìn)程不需要執(zhí)行任何額外的清理操作,可以使用_exit () 函數(shù)立即退出,以避免影響父進(jìn)程的狀態(tài)或輸出 。
在實(shí)際編程中,我們應(yīng)該根據(jù)具體的需求來(lái)選擇使用 exit () 函數(shù)還是_exit () 函數(shù) 。如果需要確保程序在終止前進(jìn)行資源清理和數(shù)據(jù)保存等操作,應(yīng)該使用 exit () 函數(shù);如果需要快速終止進(jìn)程,且不關(guān)心這些清理操作,可以使用_exit () 函數(shù) 。
四、Linux進(jìn)程案例分析
Linux的調(diào)度器類主要實(shí)現(xiàn)兩類進(jìn)程調(diào)度算法:實(shí)時(shí)調(diào)度算法和完全公平調(diào)度算法(CFS),實(shí)時(shí)調(diào)度算法SCHED_FIFO和SCHED_RR,按優(yōu)先級(jí)執(zhí)行,一般不會(huì)被搶占。直到實(shí)時(shí)進(jìn)程執(zhí)行完,才會(huì)執(zhí)行普通進(jìn)程。而大多數(shù)的普通進(jìn)程,用的就是CFS算法。
進(jìn)程調(diào)度的時(shí)機(jī):
- ①進(jìn)程狀態(tài)轉(zhuǎn)換時(shí)刻:進(jìn)程終止、進(jìn)程睡眠;
- ②當(dāng)前進(jìn)程的”時(shí)間片”用完;
- ③主動(dòng)讓出處理器,用戶調(diào)用sleep()或者內(nèi)核調(diào)用schedule();
- ④從中斷,系統(tǒng)調(diào)用或異常返回時(shí);
每個(gè)進(jìn)程task_struct中都有一個(gè)struct sched_entity se成員,這就是調(diào)度器的實(shí)體結(jié)構(gòu),進(jìn)程調(diào)度算法實(shí)際上就是管理所有進(jìn)程的這個(gè)se。
結(jié)構(gòu)任務(wù)結(jié)構(gòu){
揮發(fā)性長(zhǎng)狀態(tài); / * - 1 不可運(yùn)行, 0 可以運(yùn)行, > 0 已停止* /
無(wú)效*堆棧;
atomic_t 用法;
無(wú)符號(hào)整型標(biāo)志; / *每個(gè)進(jìn)程標(biāo)志,定義如下* /
無(wú)符號(hào)整型ptrace ;
int鎖深度; / * BKL鎖定深度* /
#ifdef CONFIG_SMP
#ifdef __ARCH_WANT_UNLOCKED_CTXSW
int oncpu ;
#萬(wàn)一
#萬(wàn)一
int prio , static_prio ,正常_prio ;
無(wú)符號(hào)整數(shù)rt_priority ;
const struct sched_class * sched_class ;
struct sched_entity se; //進(jìn)程調(diào)度實(shí)體
結(jié)構(gòu) sched_rt_entity rt ;
……
}CFS基于一個(gè)簡(jiǎn)單的理念:所有任務(wù)都應(yīng)該公平的分配處理器。理想情況下,n個(gè)進(jìn)程的調(diào)度系統(tǒng)中,每個(gè)進(jìn)程獲得1/n處理器時(shí)間,所有進(jìn)程的vruntime也是相同的。
CFS完全拋棄了時(shí)間片的概念,而是分配一個(gè)處理器使用比來(lái)度量。每個(gè)進(jìn)程一個(gè)調(diào)度周期內(nèi)分配的時(shí)間(類似于傳統(tǒng)的“時(shí)間片”)跟三個(gè)因素有關(guān):進(jìn)程總數(shù),優(yōu)先級(jí),調(diào)度周期
4.1理解CFS的首先要理解vruntime的含義
簡(jiǎn)單說(shuō)vruntime就是該進(jìn)程的運(yùn)行時(shí)間,但這個(gè)時(shí)間是通過(guò)優(yōu)先級(jí)和系統(tǒng)負(fù)載等加權(quán)過(guò)的時(shí)間,而非物理時(shí)鐘時(shí)間,按字面理解為虛擬運(yùn)行時(shí)間,也很恰當(dāng)。
每個(gè)進(jìn)程的調(diào)度實(shí)體se都保存著本進(jìn)程的虛擬運(yùn)行時(shí)間。
結(jié)構(gòu)sched_entity {
struct load_weight 負(fù)載; / * 用于負(fù)載均衡* / _
結(jié)構(gòu) rb_node run_node ;
struct list_head group_node ;
無(wú)符號(hào)整型 on_rq ;
u64 exec_start ;
u64 sum_exec_runtime ;
u64 vruntime; //虛擬運(yùn)行時(shí)間
u64 prev_sum_exec_runtime ;
……
}而進(jìn)程相關(guān)的調(diào)度方法如下:
靜態(tài)常量結(jié)構(gòu) sched_class fair_sched_class = {
。下一個(gè) = & idle_sched_class ,
。 enqueue_task = enqueue_task_fair ,
。出隊(duì)任務(wù) =出隊(duì)任務(wù)公平,
。產(chǎn)量任務(wù) =產(chǎn)量任務(wù)公平,
。 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 ,
。任務(wù)喚醒 =任務(wù)喚醒公平,
#萬(wàn)一
。 set_curr_task = set_curr_task_fair ,
。任務(wù)滴答 =任務(wù)滴答公平,
。任務(wù)分叉 =任務(wù)分叉公平,
。 prio_changed = prio_changed_fair ,
。 Switched_to = Switched_to_fair ,
。 get_rr_interval = get_rr_interval_fair ,
#ifdef CONFIG_FAIR_GROUP_SCHED
。任務(wù)移動(dòng)組 =任務(wù)移動(dòng)組公平,
#萬(wàn)一
} ;4.2vruntime的值如何跟新?
時(shí)鐘中斷產(chǎn)生時(shí),會(huì)依次調(diào)用tick_periodic()-> update_process_times()->scheduler_tick()
無(wú)效調(diào)度程序_tick (無(wú)效)
{
……
raw_spin_lock ( & rq- > lock ) ; _
update_rq_clock ( rq ) ;
update_cpu_load ( rq ) ;
curr->sched_class->task_tick(rq, curr, 0); //執(zhí)行調(diào)度器tick,更新進(jìn)程vruntime
raw_spin_unlock ( & rq- > lock ) ; _
……
}
task_tick_fair ()調(diào)用entity_tick()如下:
靜態(tài)無(wú)效entity_tick (結(jié)構(gòu)cfs_rq * cfs_rq ,結(jié)構(gòu)sched_entity * curr , int排隊(duì))
{
update_curr ( cfs_rq ) ;
……
if ( cfs_rq - > nr_running > 1 | | ! sched_feat ( WAKEUP_PREEMPT ) )
check_preempt_tick(cfs_rq, curr); //檢查當(dāng)前進(jìn)程是否需要調(diào)度
}這里分析兩個(gè)重要函數(shù)update_curr()和check_preempt_tick()
靜態(tài)無(wú)效 update_curr (結(jié)構(gòu) cfs_rq * cfs_rq )
{
struct sched_entity * curr = cfs_rq - > curr ;
u64現(xiàn)在 = rq_of ( cfs_rq ) - >時(shí)鐘;
無(wú)符號(hào)長(zhǎng) delta_exec ;
如果 (不太可能(! curr ))
返回;
// delta_exec獲得最后一次修改后,當(dāng)前進(jìn)程所運(yùn)行的實(shí)際時(shí)間
delta_exec = ( unsigned long ) ( now - curr - > exec_start ) ;
如果 (! delta_exec )
返回;
__update_curr ( cfs_rq , curr , delta_exec ) ;
curr->exec_start = now; //運(yùn)行時(shí)間已保存,更新起始執(zhí)行時(shí)間
如果 ( entity_is_task (當(dāng)前)) {
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 ) ;
}
}主要關(guān)心__update_curr()函數(shù)
靜態(tài)內(nèi)聯(lián) void __update_curr ( struct cfs_rq * cfs_rq , struct sched_entity * curr , unsigned long delta_exec )
{
無(wú)符號(hào)長(zhǎng) delta_exec_weighted ;
schedstat_set ( curr - > exec_max , max ( ( u64 ) delta_exec , curr - > exec_max ) ) ;
curr->sum_exec_runtime += delta_exec;//累計(jì)實(shí)際運(yùn)行時(shí)間
schedstat_add ( cfs_rq , exec_clock , delta_exec ) ;
delta_exec_weighted = calc_delta_fair ( delta_exec , curr ) ; //對(duì)delta_exec加權(quán)
curr->vruntime += delta_exec_weighted;//累計(jì)入vruntime
update_min_vruntime(cfs_rq); //更新cfs_rq最小vruntime(保存所有進(jìn)程中的最小vruntime)
}關(guān)注calc_delta_fair()加權(quán)函數(shù)如何實(shí)現(xiàn)
/ *
* δ / = w
* /
靜態(tài)內(nèi)聯(lián)無(wú)符號(hào)長(zhǎng)整型
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 - >負(fù)載) ;
返回增量;
}若當(dāng)前進(jìn)程nice為0,直接返回實(shí)際運(yùn)行時(shí)間,其他所有nice值的加權(quán)都是以0nice值為參考增加或減少的。
/ *
* delta * =重量/ lw
* /
靜態(tài)無(wú)符號(hào)長(zhǎng)整型
calc_delta_mine (無(wú)符號(hào)長(zhǎng) delta_exec ,無(wú)符號(hào)長(zhǎng)權(quán)重,
結(jié)構(gòu) load_weight * lw )
{
u64 tmp ;
if ( ! lw - > inv_weight ) {
if ( BITS_PER_LONG > 32 & &不太可能( lw - >權(quán)重> = WMULT_CONST ) )
lw - > inv_weight = 1 ;
別的
lw- > inv_weight = 1 + ( WMULT_CONST - lw- >權(quán)重/ 2 ) _
/ (lw->weight+1);//這公式?jīng)]弄明白
}
tmp = ( u64 ) delta_exec *權(quán)重;
/ *
*檢查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);//做四舍五入
返回(無(wú)符號(hào)長(zhǎng))分鐘( tmp , ( u64 )(無(wú)符號(hào)長(zhǎng)) LONG_MAX );
}當(dāng)nice!=0時(shí),實(shí)際是按公式delta *= weight / lw來(lái)計(jì)算的weight=1024是nice0的權(quán)重,lw是當(dāng)前進(jìn)程的權(quán)重,該lw和nice值的換算后面介紹,上面還書的lw計(jì)算公式?jīng)]弄明白,總之這個(gè)函數(shù)就是把實(shí)際運(yùn)行時(shí)間加權(quán)為進(jìn)程調(diào)度里的虛擬運(yùn)行時(shí)間,從而更新vruntime。
更新完vruntime之后,會(huì)檢查是否需要進(jìn)程調(diào)度
返回 static voidentity_tick ( struct cfs_rq * cfs_rq , struct sched_entity * curr , int排隊(duì))
{
update_curr ( cfs_rq ) ;
……
if ( cfs_rq - > nr_running > 1 | | ! sched_feat ( WAKEUP_PREEMPT ) )
check_preempt_tick(cfs_rq, curr); //檢查當(dāng)前進(jìn)程是否需要調(diào)度
}更新完cfs_rq之后,會(huì)檢查當(dāng)前進(jìn)程是否已經(jīng)用完自己的“時(shí)間片”
/ *
*如果需要,用新喚醒的任務(wù)搶占當(dāng)前任務(wù):
* /
靜態(tài)無(wú)效
check_preempt_tick ( struct cfs_rq * cfs_rq , struct sched_entity * curr )
{
無(wú)符號(hào)長(zhǎng)的 Ideal_runtime , delta_exec ;
ideal_runtime = sched_slice(cfs_rq, curr);//ideal_runtime是理論上的處理器運(yùn)行時(shí)間片
delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;//該進(jìn)程本輪調(diào)度累計(jì)運(yùn)行時(shí)間
if (delta_exec > ideal_runtime) {// 假如實(shí)際運(yùn)行超過(guò)調(diào)度器分配的時(shí)間,就標(biāo)記調(diào)度標(biāo)志
resched_task ( rq_of ( cfs_rq ) - > curr ) ;
/ *
*當(dāng)前任務(wù)運(yùn)行足夠長(zhǎng)的時(shí)間,確保它不會(huì)得到
*由于好友的青睞而再次當(dāng)選。
* /
清除好友( cfs_rq , curr ) ;
返回;
}
/ *
*確保錯(cuò)過(guò)喚醒搶占的任務(wù)
*窄邊距不必等待完整的切片。
*這也減少了負(fù)載下好友引起的延遲。
* /
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 (增量>理想運(yùn)行時(shí)間)
resched_task ( rq_of ( cfs_rq ) - > curr ) ;
}
}當(dāng)該進(jìn)程運(yùn)行時(shí)間超過(guò)實(shí)際分配的“時(shí)間片”,就標(biāo)記調(diào)度標(biāo)志resched_task(rq_of(cfs_rq)->curr);,否則本進(jìn)程繼續(xù)執(zhí)行。中斷退出,調(diào)度函數(shù)schedule()會(huì)檢查此標(biāo)記,以選取新的進(jìn)程來(lái)?yè)屨籍?dāng)前進(jìn)程。
4.3如何選擇下一個(gè)可執(zhí)行進(jìn)程
CFS選擇具有最小vruntime值的進(jìn)程作為下一個(gè)可執(zhí)行進(jìn)程,CFS用紅黑樹來(lái)組織調(diào)度實(shí)體,而鍵值就是vruntime。那么CFS只要查找選擇最左葉子節(jié)點(diǎn)作為下一個(gè)可執(zhí)行進(jìn)程即可。實(shí)際上CFS緩存了最左葉子,可以直接選取left_most葉子。
上面代碼跟蹤到timer tick中斷退出,若“ideal_runtime”已經(jīng)用完,就會(huì)調(diào)用schedule()函數(shù)選中新進(jìn)程并且完成切換。
asmlinkage void __sched 時(shí)間表( void )
{
if ( prev - > state && ! ( preempt_count ( ) & PREEMPT_ACTIVE ) ) { _
if (不太可能( signal_pending_state ( prev - > state , prev ) ) )
上一個(gè) - >狀態(tài)= TASK_RUNNING ;
別的
deactivate_task(rq, prev, 1);//如果狀態(tài)已經(jīng)不可運(yùn)行,將其移除運(yùn)行隊(duì)列
switch_count = &上一個(gè)- > nvcsw ;
}
pre_schedule ( rq ,上一個(gè)) ;
if (不太可能( ! rq - > nr_running ) )
空閑平衡( CPU , RQ );
put_prev_task(rq, prev); //處理上一個(gè)進(jìn)程
next = pick_next_task(rq);//選出下一個(gè)進(jìn)程
……
context_switch(rq, prev, next); /* unlocks the rq *///完成進(jìn)程切換
……
}如果進(jìn)程狀態(tài)已經(jīng)不是可運(yùn)行,那么會(huì)將該進(jìn)程移出可運(yùn)行隊(duì)列,如果繼續(xù)可運(yùn)行put_prev_task()會(huì)依次調(diào)用put_prev_task_fair()->put_prev_entity()
靜態(tài)無(wú)效put_prev_entity (結(jié)構(gòu)cfs_rq * cfs_rq ,結(jié)構(gòu)sched_entity * prev )
{
/ *
* 如果仍在運(yùn)行隊(duì)列中,則deactivate_task ( )
*沒有被調(diào)用并且update_curr ( )必須被完成:
* /
if (上一個(gè)- > on_rq )
update_curr ( cfs_rq ) ;
check_spread ( cfs_rq ,上一個(gè)) ;
如果 (上一個(gè)- > on_rq ) {
update_stats_wait_start ( cfs_rq ,上一個(gè)) ;
/ *將“當(dāng)前”放回樹中。 * /
__enqueue_entity ( cfs_rq ,上一個(gè)) ;
}
cfs_rq - > curr = NULL ;
}__enqueue_entity(cfs_rq, prev) 將上一個(gè)進(jìn)程重新插入紅黑樹(注意,當(dāng)前運(yùn)行進(jìn)程是不在紅黑樹中的)pick_next_task()會(huì)依次調(diào)用pick_next_task_fair()
靜態(tài)結(jié)構(gòu)task_struct * pick_next_task_fair (結(jié)構(gòu)rq * rq )
{
結(jié)構(gòu)任務(wù)結(jié)構(gòu)* p ;
struct cfs_rq * cfs_rq = & rq - > cfs ;
結(jié)構(gòu) sched_entity * se ;
if ( ! cfs_rq - > nr_running )
返回空值;
做 {
se = pick_next_entity(cfs_rq);//選出下一個(gè)可執(zhí)行進(jìn)程
set_next_entity(cfs_rq, se); //把選中的進(jìn)程(left_most葉子)從紅黑樹移除,更新紅黑樹
cfs_rq = group_cfs_rq ( se );
} while ( cfs_rq ) ;
p = task_of ( se ) ;
htick_start_fair ( rq , p ) ;
返回 p ;
}set_next_entity()函數(shù)會(huì)調(diào)用__dequeue_entity(cfs_rq, se)把選中的下一個(gè)進(jìn)程即最左葉子移出紅黑樹。最后context_switch()完成進(jìn)程的切換。
4.4何時(shí)更新rbtree
- ①上一個(gè)進(jìn)程執(zhí)行完ideal_time,還可繼續(xù)執(zhí)行時(shí),會(huì)插入紅黑樹;
- ②下一個(gè)進(jìn)程被選中移出rbtree紅黑樹時(shí);
- ③新建進(jìn)程;
- ④進(jìn)程由睡眠態(tài)被激活,變?yōu)榭蛇\(yùn)行態(tài)時(shí);
- ⑤調(diào)整優(yōu)先級(jí)時(shí)也會(huì)更新rbtree;
4.5新建進(jìn)程如何加入紅黑樹
新建進(jìn)程會(huì)做一系列復(fù)雜的工作,這里我們只關(guān)心與紅黑樹有關(guān)部分
Linux使用fork,clone或者vfork等系統(tǒng)調(diào)用創(chuàng)建進(jìn)程,最終都會(huì)到do_fork函數(shù)實(shí)現(xiàn),如果沒有設(shè)置CLONE_STOPPED,do_fork會(huì)執(zhí)行兩個(gè)與紅黑樹相關(guān)的函數(shù): 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為基準(zhǔn)
/ *
* “當(dāng)前”期間已承諾當(dāng)前任務(wù), _
*然而,新任務(wù)的額外重量會(huì)減慢他們的速度
*小,放置新任務(wù),使其適合該插槽
*最后保持打開狀態(tài)。
* /
if (初始&& sched_feat ( START_DEBIT ) ) _
vruntime += sched_vslice(cfs_rq, se);// 加上一個(gè)調(diào)度周期內(nèi)的"時(shí)間片"
/ *休眠至單個(gè)延遲不計(jì)算在內(nèi)。 * /
if ( ! initial & & sched_feat ( FAIR_SLEEPERS ) ) {
無(wú)符號(hào)長(zhǎng)閾值= sysctl_sched_latency ;
/ *
*將休眠閾值轉(zhuǎn)換為虛擬時(shí)間。
* SCHED_IDLE是一個(gè)特殊的子類。我們關(guān)心
*公平性僅相對(duì)于其他 SCHED_IDLE 任務(wù),
*所有這些都具有相同的重量。
* /
if ( sched_feat ( NORMALIZED_SLEEPER ) & & ( ! entity_is_task ( se ) | |
task_of ( se ) - >策略!= SCHED_IDLE ) )
thresh = calc_delta_fair ( thresh , se ) ;
/ *
*將睡眠時(shí)間的影響減半, 以允許
* 為睡眠者帶來(lái)更溫和的效果:
* /
如果 ( sched_feat ( GENTLE_FAIR_SLEEPERS ))
脫粒> > = 1 ;
vruntime - = 閾值;
}
/ *確保我們永遠(yuǎn)不會(huì)因?yàn)楸慌旁诤竺娑A得時(shí)間。 * /
vruntime = max_vruntime ( self - > vruntime , vruntime ) ;
se - > vruntime = vruntime ;
}計(jì)算新進(jìn)程的vruntime值,加上一個(gè)“平均時(shí)間片”表示剛執(zhí)行完,避免新建進(jìn)程立馬搶占CPU。
(2)調(diào)用wake_up_new_task函數(shù)
無(wú)效wake_up_new_task ( struct task_struct * p ,無(wú)符號(hào)長(zhǎng)clone_flags )
{
……
rq = task_rq_lock ( p , & flags ) ;
update_rq_clock ( rq ) ;
activate_task(rq, p, 0);//激活當(dāng)前進(jìn)程,添加入紅黑樹
check_preempt_curr(rq, p, WF_FORK);//確認(rèn)是否需要搶占
……
}更新時(shí)鐘,激活新建的進(jìn)程activate_task()會(huì)調(diào)用
static void enqueue_task ( struct rq * rq , struct task_struct * p , intwakeup , bool head )
{
如果 (喚醒)
p- > se 。_ start_runtime = p - > se 。 sum_exec_運(yùn)行時(shí);
sched_info_queued ( p ) ;
p- > sched_class- > enqueue_task ( rq , p ,喚醒, head ) ; _ _
p- > se 。_ on_rq = 1 ;
}將新建的進(jìn)程加入rbtree;
4.6喚醒進(jìn)程
調(diào)用try_to_wake_up()->activate_task()->enqueue_task_fair()->enqueue_entity()注意enqueue_entity 函數(shù)調(diào)用place_entity對(duì)進(jìn)程vruntime做補(bǔ)償計(jì)算,再次考察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為基準(zhǔn)
/ *
* “當(dāng)前”期間已承諾當(dāng)前任務(wù), _
*然而,新任務(wù)的額外重量會(huì)減慢他們的速度
*小,放置新任務(wù),使其適合該插槽
*最后保持打開狀態(tài)。
* /
if (初始&& sched_feat ( START_DEBIT ) ) _
vruntime += sched_vslice(cfs_rq, se);//一個(gè)調(diào)度周期內(nèi)的"時(shí)間片"
/ *休眠至單個(gè)延遲不計(jì)算在內(nèi)。 * /
if ( ! initial & & sched_feat ( FAIR_SLEEPERS ) ) {
無(wú)符號(hào)長(zhǎng)閾值= sysctl_sched_latency ;
/ *
*將休眠閾值轉(zhuǎn)換為虛擬時(shí)間。
* SCHED_IDLE是一個(gè)特殊的子類。我們關(guān)心
*公平性僅相對(duì)于其他 SCHED_IDLE 任務(wù),
*所有這些都具有相同的重量。
* /
if ( sched_feat ( NORMALIZED_SLEEPER ) & & ( ! entity_is_task ( se ) | |
task_of ( se ) - >策略!= SCHED_IDLE ) )
thresh = calc_delta_fair ( thresh , se ) ;
/ *
*將睡眠時(shí)間的影響減半, 以允許
* 為睡眠者帶來(lái)更溫和的效果:
* /
如果 ( sched_feat ( GENTLE_FAIR_SLEEPERS ))
脫粒> > = 1 ;
vruntime -= thresh;//對(duì)于睡眠進(jìn)程喚醒,給予vruntime補(bǔ)償
}
/ *確保我們永遠(yuǎn)不會(huì)因?yàn)楸慌旁诤竺娑A得時(shí)間。 * /
vruntime = max_vruntime(se->vruntime, vruntime);//避免通過(guò)睡眠來(lái)獲得運(yùn)行時(shí)間
se - > vruntime = vruntime ;
}當(dāng)initial=1時(shí),新建進(jìn)程vruntime=cfs最小vruntime值+時(shí)間片,放入紅黑樹最右端。
當(dāng)initial=0時(shí),表示喚醒進(jìn)程,vruntime要減去一個(gè)thresh.這個(gè)thresh由調(diào)度周期sysctl_sched_latency加權(quán)得到虛擬時(shí)間,這樣做可以對(duì)睡眠進(jìn)程做一個(gè)補(bǔ)償,喚醒時(shí)會(huì)得到一個(gè)較小的vruntime, 使它可以盡快搶占CPU(可以快速響應(yīng)I/O消耗型進(jìn)程)。
注意注釋/* ensure we never gain time by being placed backwards. */這個(gè)設(shè)計(jì)是為了給睡眠較長(zhǎng)時(shí)間的進(jìn)程做時(shí)間補(bǔ)償?shù)模仁蛊淇梢钥焖贀屨?,又避免因太小的vruntime值而長(zhǎng)期占用CPU。但有些進(jìn)程只是短時(shí)間睡眠,這樣喚醒時(shí)自身vruntime還是大于min_vruntime的,為了不讓進(jìn)程通過(guò)睡眠獲得額外運(yùn)行時(shí)間補(bǔ)償,最后vruntime取計(jì)算出的補(bǔ)償時(shí)間和進(jìn)程本身的vruntime較大者。從這可以看出,雖然CFS不再區(qū)分I/O消耗型,CPU消耗型進(jìn)程,但是CFS模型對(duì)IO消耗型天然的提供了快速的響應(yīng)。
4.7改變進(jìn)程優(yōu)先級(jí),如何調(diào)整rbtree
Linux中改變進(jìn)程優(yōu)先級(jí)會(huì)調(diào)用底層的set_user_nice()
void set_user_nice ( struct task_struct * p ,長(zhǎng)nice )
{
……
dequeue_task(rq, p, 0); //把進(jìn)程從紅黑樹中取出
……
p->static_prio = NICE_TO_PRIO(nice);//將nice(-20~19)值映射到100~139,0~99是實(shí)時(shí)進(jìn)程優(yōu)先級(jí)
設(shè)置負(fù)載重量( p );
……
enqueue_task ( rq , p , 0 , false ) ;
}set_user_nice把進(jìn)程從紅黑樹取出,調(diào)整優(yōu)先級(jí)(nice值對(duì)應(yīng)權(quán)重),再重新加入紅黑樹
set_load_weight()函數(shù)是設(shè)置nice值對(duì)應(yīng)的權(quán)重
靜態(tài)無(wú)效set_load_weight (結(jié)構(gòu)task_struct * p )
{
如果 ( task_has_rt_policy ( p )) {
p- > se 。_負(fù)載。重量= 0 ;
p- > se 。_負(fù)載。 inv_weight = WMULT_CONST ;
返回;
}
/ *
* SCHED_IDLE 任務(wù)獲得最小權(quán)重:
* /
如果 ( p - >政策== SCHED_IDLE ){ _
p- > se 。_負(fù)載。重量= WEIGHT_IDLEPRIO ;
p- > se 。_負(fù)載。 inv_weight = WMULT_IDLEPRIO ;
返回;
}
p- > se 。_負(fù)載。權(quán)重= prio_to_weight [ p - > static_prio - MAX_RT_PRIO ] ;
p- > se 。_負(fù)載。 inv_weight = prio_to_wmult [ p - > static_prio - MAX_RT_PRIO ] ;
}數(shù)組prio_to_weight[]是將nice值(-20~19)轉(zhuǎn)化為以nici 0(1024)值為基準(zhǔn)的加權(quán)值,根據(jù)內(nèi)核注釋每一個(gè)nice差值,權(quán)重相差10%,即在負(fù)載一定的條件下,每增加或減少一個(gè)nice值,獲得的CPU時(shí)間相應(yīng)增加或減少10%
靜態(tài)常量 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()函數(shù)用到這個(gè)數(shù)組加權(quán)值,這個(gè)轉(zhuǎn)化過(guò)程還沒弄明白,有明白的朋友,指點(diǎn)一二,不勝感激
/ *
* prio_to_weight [ ]數(shù)組的反( 2^32 / x )值,預(yù)先計(jì)算。
*
* 在重量不經(jīng)常變化的情況下,我們可以使用
*預(yù)先計(jì)算逆數(shù)以通過(guò)轉(zhuǎn)動(dòng)除法來(lái)加速算術(shù)
*轉(zhuǎn)化為乘法:
* /
靜態(tài)常量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 、 _ _ _ _ _ _ _ _
} ;最后,說(shuō)下對(duì)CFS “完全公平” 的理解:
①不再區(qū)分進(jìn)程類型,所有進(jìn)程公平對(duì)待
②對(duì)I/O消耗型進(jìn)程,仍然會(huì)提供快速響應(yīng)(對(duì)睡眠進(jìn)程做時(shí)間補(bǔ)償)
③優(yōu)先級(jí)高的進(jìn)程,獲得CPU時(shí)間更多(vruntime增長(zhǎng)的更慢)
可見CFS的完全公平,并不是說(shuō)所有進(jìn)程絕對(duì)的平等,占用CPU時(shí)間完全相同,而是體現(xiàn)在vruntime數(shù)值上,所有進(jìn)程都用虛擬時(shí)間來(lái)度量,總是讓vruntime最小的進(jìn)程搶占,這樣看起來(lái)是完全公平的,但實(shí)際上vruntime的更新,增長(zhǎng)速度,不同進(jìn)程是不盡一樣的。CFS利用這么個(gè)簡(jiǎn)單的vruntime機(jī)制,實(shí)現(xiàn)了以往需要相當(dāng)復(fù)雜算法實(shí)現(xiàn)的進(jìn)度調(diào)度需求,高明!




















