Linux 是一個(gè)多任務(wù)操作系統(tǒng),它支持遠(yuǎn)大于 CPU 數(shù)量的任務(wù)同時(shí)運(yùn)行。當(dāng)然,這些任務(wù)實(shí)際上并不是真的在同時(shí)運(yùn)行,而是因?yàn)橄到y(tǒng)在很短的時(shí)間內(nèi),將 CPU 輪流分配給它們,造成多任務(wù)同時(shí)運(yùn)行的錯(cuò)覺。而在每個(gè)任務(wù)運(yùn)行前,CPU 都需要知道任務(wù)從哪里加載、又從哪里開始運(yùn)行,也就是說,需要系統(tǒng)事先幫它設(shè)置好CPU 寄存器和程序計(jì)數(shù)器。CPU 寄存器和程序計(jì)數(shù)器就是 CPU 上下文,因?yàn)樗鼈兌际?CPU 在運(yùn)行任何任務(wù)前,必須的依賴環(huán)境。
一、CPU 上下文切換
CPU 上下文切換就是先把前一個(gè)任務(wù)的 CPU 上下文(也就是 CPU 寄存器和程序計(jì)數(shù)器)保存起來,然后加載新任務(wù)的上下文到這些寄存器和程序計(jì)數(shù)器,最后再跳轉(zhuǎn)到程序計(jì)數(shù)器所指的新位置,運(yùn)行新任務(wù)。
而這些保存下來的上下文,會(huì)存儲(chǔ)在系統(tǒng)內(nèi)核中,并在任務(wù)重新調(diào)度執(zhí)行時(shí)再次加載進(jìn)來。這樣就能保證任務(wù)原來的狀態(tài)不受影響,讓任務(wù)看起來還是連續(xù)運(yùn)行。
CPU 上下文切換根據(jù)任務(wù)的不同,可以分為以下三種類型 : 進(jìn)程上下文切換 - 線程上下文切換 - 中斷上下文切換
程序在執(zhí)行過程中通常有用戶態(tài)和內(nèi)核態(tài)兩種狀態(tài),CPU對處于內(nèi)核態(tài)根據(jù)上下文環(huán)境進(jìn)一步細(xì)分,因此有了下面三種狀態(tài):
(1)內(nèi)核態(tài),運(yùn)行于進(jìn)程上下文,內(nèi)核代表進(jìn)程運(yùn)行于內(nèi)核空間。
(2)內(nèi)核態(tài),運(yùn)行于中斷上下文,內(nèi)核代表硬件運(yùn)行于內(nèi)核空間。
(3)用戶態(tài),運(yùn)行于用戶空間
1.1 進(jìn)程上下文切換
Linux按照特權(quán)等級,把進(jìn)程的運(yùn)行空間分為內(nèi)核空間和用戶空間,分別對應(yīng)著下圖中,CPU特權(quán)等級的Ring 0 和Ring 3
- 內(nèi)核空間(Ring 0)具有最高權(quán)限,可以直接訪問所有資源
- 用戶空間(Ring 3) 只能訪問受限資源,不能直接訪問內(nèi)存等硬件設(shè)備,必須通過系統(tǒng)調(diào)用陷入到內(nèi)核中,才能訪問這些特權(quán)資源

換個(gè)角度看,也就是說,進(jìn)程既可以在用戶空間運(yùn)行,又可以在內(nèi)核空間運(yùn)行(從用戶態(tài)到內(nèi)核態(tài)的轉(zhuǎn)變,需要通過系統(tǒng)調(diào)用來完成,系統(tǒng)調(diào)用實(shí)質(zhì)上是一種特殊的中斷,由用戶程序觸發(fā),之前提到的中斷是由硬件觸發(fā))。進(jìn)程在用戶空間運(yùn)行時(shí),被稱為進(jìn)程的用戶態(tài),而陷入內(nèi)核空間的時(shí)候,被稱為進(jìn)程的內(nèi)核態(tài)。
主要注意的是:
- 進(jìn)程上下文切換,是指從一個(gè)進(jìn)程切換到另一個(gè)進(jìn)程運(yùn)行
- 而系統(tǒng)調(diào)用過程中一直是同一個(gè)進(jìn)程在運(yùn)行
系統(tǒng)調(diào)用過程通常稱為特權(quán)模式切換,而不是上下文切換。當(dāng)進(jìn)程調(diào)用系統(tǒng)調(diào)用或者發(fā)生中斷時(shí),CPU從用戶模式(用戶態(tài))切換成內(nèi)核模式(內(nèi)核態(tài)),此時(shí),無論是系統(tǒng)調(diào)用程序還是中斷服務(wù)程序,都處于當(dāng)前進(jìn)程的上下文中,并沒有發(fā)生進(jìn)程上下文切換。當(dāng)系統(tǒng)調(diào)用或中斷處理程序返回時(shí),CPU要從內(nèi)核模式切換回用戶模式,此時(shí)會(huì)執(zhí)行操作系統(tǒng)的調(diào)用程序。如果發(fā)現(xiàn)就需隊(duì)列中有比當(dāng)前進(jìn)程更高的優(yōu)先級的進(jìn)程,則會(huì)發(fā)生進(jìn)程切換:當(dāng)前進(jìn)程信息被保存,切換到就緒隊(duì)列中的那個(gè)高優(yōu)先級進(jìn)程;否則,直接返回當(dāng)前進(jìn)程的用戶模式,不會(huì)發(fā)生上下文切換。

進(jìn)程的上下文不僅包括了虛擬內(nèi)存、棧、全局變量等用戶空間的資源,還包括了內(nèi)核堆棧、寄存器等內(nèi)核空間的狀態(tài)。因此進(jìn)程的上下文切換就比系統(tǒng)調(diào)用時(shí)多了一步:在保存當(dāng)前進(jìn)程的內(nèi)核狀態(tài)和CPU寄存器之前,需要先把該進(jìn)程的虛擬內(nèi)存、棧等保存下來;而加載下一進(jìn)程的內(nèi)核態(tài)后,還需要刷新進(jìn)程的虛擬內(nèi)存和用戶棧。模式切換與進(jìn)程切換比較起來,容易很多,而且節(jié)省時(shí)間,因?yàn)槟J角袚Q最主要的任務(wù)只是切換進(jìn)程寄存器上下文的切換。
1.2 中斷上下文切換
為了快速響應(yīng)硬件的事件,中斷處理會(huì)打斷進(jìn)程的正常調(diào)度和執(zhí)行,轉(zhuǎn)而調(diào)用中斷處理程序,響應(yīng)設(shè)備事件。而在打斷其他進(jìn)程時(shí),就需要將進(jìn)程當(dāng)前的狀態(tài)保存下來,這樣在中斷結(jié)束后,進(jìn)程仍然可以從原來的狀態(tài)恢復(fù)運(yùn)行
跟進(jìn)程上下文不同,中斷上下文切換并不涉及到進(jìn)程的用戶態(tài)。所以,即便中斷過程打斷了一個(gè)正處于用戶態(tài)的進(jìn)程,也不需要保存和恢復(fù)這個(gè)進(jìn)程的虛擬內(nèi)存、全局變量等用戶態(tài)資源。中斷上下文,其實(shí)只包括內(nèi)核態(tài)中斷服務(wù)程序執(zhí)行所必需的狀態(tài),包括CPU寄存器、內(nèi)核堆棧、硬件中斷參數(shù)等。
對同一個(gè)CPU來說,中斷處理比進(jìn)程擁有更高的優(yōu)先級,所以中斷上下文切換并不會(huì)與進(jìn)程上下文切換同時(shí)發(fā)生。同樣道理,由于中斷會(huì)打斷正常進(jìn)程的調(diào)度和執(zhí)行,所以大部分中斷處理程序都短小精悍,以便盡可能快的執(zhí)行結(jié)束。
1.3 線程上下文切換
線程與進(jìn)程最大的區(qū)別在于,線程是調(diào)度的基本單位,而進(jìn)程則是資源擁有的基本單位。說白了,所謂內(nèi)核中的任務(wù)調(diào)度,實(shí)際上的調(diào)度對象是線程;而進(jìn)程只是給線程提供了虛擬內(nèi)存、全局變量等資源。
線程的上下文切換其實(shí)就可以分為兩種情況:
- 第一種:前后兩個(gè)線程屬于不同進(jìn)程。此時(shí),因?yàn)橘Y源不共享,所以切換過程就跟進(jìn)程上下文切換是一樣
- 第二種:前后兩個(gè)線程屬于同一個(gè)進(jìn)程。此時(shí),因?yàn)樘摂M內(nèi)存是共享的,所以在切換時(shí),虛擬內(nèi)存這些資源就保持不動(dòng),只需要切換線程的私有數(shù)據(jù)、寄存器等不共享的數(shù)據(jù)。
到這里你應(yīng)該也發(fā)現(xiàn)了,雖然同為上下文切換,但同進(jìn)程內(nèi)的線程切換,要比多進(jìn)程間的切換消耗更少的資源,而這,也正是多線程代替多進(jìn)程的一個(gè)優(yōu)勢。
二、中斷上下文的切換的一般流程(以系統(tǒng)調(diào)用為例)
中斷分外部中斷(硬件中斷)和內(nèi)部中斷(軟件中斷),內(nèi)部中斷又稱為異常(Exception),異常又分為故障(fault)和陷阱(trap)。 系統(tǒng)調(diào)用就是利用陷阱(trap)這種軟件中斷方式主動(dòng)從用戶態(tài)進(jìn)入內(nèi)核態(tài)的。

系統(tǒng)調(diào)用層次:用戶程序------>C庫(即API):INT 0x80 ----->system_call------->系統(tǒng)調(diào)用服務(wù)例程-------->內(nèi)核程序
系統(tǒng)編程接口API和系統(tǒng)調(diào)用的關(guān)系
- 應(yīng)用程序代碼調(diào)用 xyz(),該函數(shù)是一個(gè)包裝系統(tǒng)調(diào)用的庫函數(shù);
- 庫函數(shù) xyz() 負(fù)責(zé)準(zhǔn)備向內(nèi)核傳遞的參數(shù),并觸發(fā)軟中斷以切換到內(nèi)核;
- CPU 被軟中斷打斷后,執(zhí)行中斷處理函數(shù),即系統(tǒng)調(diào)用處理函數(shù)(system_call);
- 系統(tǒng)調(diào)用處理函數(shù)調(diào)用系統(tǒng)調(diào)用服務(wù)例程(sys_xyz ),真正開始處理該系統(tǒng)調(diào)用。
在Linux中通過執(zhí)行int $0x80或syscall指令來觸發(fā)系統(tǒng)調(diào)用的執(zhí)行,其中這條int $0x80匯編指令是產(chǎn)生中斷向量為128的編程異常(trap)。
當(dāng)用戶態(tài)進(jìn)程調(diào)用一個(gè)系統(tǒng)調(diào)用時(shí),CPU切換到內(nèi)核態(tài)并開始執(zhí)行 system_call(entry_INT80_32或entry_SYSCALL_64)匯編代碼,其 中根據(jù)系統(tǒng)調(diào)用號(hào)調(diào)用對應(yīng)的內(nèi)核處理函數(shù)。
內(nèi)核實(shí)現(xiàn)了很多不同的系統(tǒng)調(diào)用,用戶態(tài)進(jìn)程 必須指明需要執(zhí)行哪個(gè)系統(tǒng)調(diào)用,這需要使用EAX寄存器傳遞一個(gè)名 為系統(tǒng)調(diào)用號(hào)的參數(shù)。除了系統(tǒng)調(diào)用號(hào)外,系統(tǒng)調(diào)用也可能需要傳遞 參數(shù)。在32位x86體系結(jié)構(gòu)下普通的函數(shù)調(diào)用是通過將參數(shù)壓棧的方式傳遞的。
系統(tǒng)調(diào)用從用戶 態(tài)切換到內(nèi)核態(tài),在用戶態(tài)和內(nèi)核態(tài)這兩種執(zhí)行模式下使用的是不同的堆棧,即進(jìn)程的用戶態(tài)堆棧和進(jìn)程的內(nèi)核態(tài)堆棧,傳遞參數(shù)方法無法通過參數(shù)壓棧的方式,而是通過寄存器 傳遞參數(shù)的方式。寄存器傳遞參數(shù)的個(gè)數(shù)是有限制的,而且每個(gè)參數(shù)的長度不能超過寄存 器的長度,32位x86體系結(jié)構(gòu)下寄存器的長度最大32位。除了EAX用于傳遞系統(tǒng)調(diào)用號(hào) 外,參數(shù)按順序賦值給EBX、ECX、EDX、ESI、EDI、EBP,參數(shù)的個(gè)數(shù)不能超過6個(gè), 即上述6個(gè)寄存器。如果超過6個(gè)就把某一個(gè)寄存器作為指針,指向內(nèi)存,就可以通過內(nèi) 存來傳遞更多的參數(shù)。
觸發(fā)系統(tǒng)調(diào)用后的大致流程:
正在運(yùn)行的用戶態(tài)進(jìn)程X
發(fā)生中斷(包括異常、系統(tǒng)調(diào)用等),CPU完成以下動(dòng)作
- save cs:eip/ss:esp/eflags:當(dāng)前CPU上下文壓入進(jìn)程X的內(nèi)核堆棧
- load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack):加載當(dāng)前進(jìn)程內(nèi)核堆棧相關(guān)信息,跳轉(zhuǎn)到中斷處理程序,即中斷執(zhí)行路徑的起點(diǎn)
SAVE_ALL,保存現(xiàn)場,此時(shí)完成了中斷上下文切換,即從進(jìn)程X的用戶態(tài)到進(jìn)程X的內(nèi)核態(tài)
中斷處理過程中或中斷返回前調(diào)用了schedule函數(shù),其中的switch_to做了關(guān)鍵的進(jìn)程上下文切換。將當(dāng)前進(jìn)程X的內(nèi)核堆棧切換到進(jìn)程調(diào)度算法選出來的next進(jìn)程 (本例假定為進(jìn)程Y)的內(nèi)核堆棧,并完成了進(jìn)程上下文所需的EIP等寄存器狀態(tài)切換。詳細(xì)過程見前述內(nèi)容
標(biāo)號(hào)1,即前述3.18.6內(nèi)核的swtich_to代碼第50行“”1:\t“ ”(地址為switch_to中的“$1f”),之后開始運(yùn)行進(jìn)程Y(這里進(jìn)程Y曾經(jīng)通過以上步驟被切換出去,因此可以 從標(biāo)號(hào)1繼續(xù)執(zhí)行)
restore_all,恢復(fù)現(xiàn)場,與(3)中保存現(xiàn)場相對應(yīng)。注意這?是進(jìn)程Y的中斷處理過程中,而(3)中保存現(xiàn)場是在進(jìn)程X的中斷處理過程中,因?yàn)閮?nèi)核堆棧從進(jìn)程X 切換到進(jìn)程Y了
iret - pop cs:eip/ss:esp/eflags,從Y進(jìn)程的內(nèi)核堆棧中彈出(2)中硬件完成的壓棧內(nèi)容。此時(shí)完成了中斷上下文的切換,即從進(jìn)程Y的內(nèi)核態(tài)返回到進(jìn)程Y的用戶態(tài)
繼續(xù)運(yùn)行用戶態(tài)進(jìn)程Y
中斷進(jìn)入詳細(xì)過程:
確定與中斷或者異常關(guān)聯(lián)的向量i(0~255)
讀idtr寄存器指向的IDT表中的第i項(xiàng) 3,從gdtr寄存器獲得GDT的基地址,并在GDT中查找, 以讀取IDT表項(xiàng)中的段選擇符所標(biāo)識(shí)的段描述符
從gdtr寄存器獲得GDT的基地址,并在GDT中查找, 以讀取IDT表項(xiàng)中的段選擇符所標(biāo)識(shí)的段描述符
確定中斷是由授權(quán)的發(fā)生源發(fā)出的
檢查是否發(fā)生了特權(quán)級的變化,一般指是否由 用戶態(tài)陷入了內(nèi)核態(tài)。
如果是由用戶態(tài)陷入了內(nèi)核態(tài),控制單元必須開 始使用與新的特權(quán)級相關(guān)的堆棧
- 讀tr寄存器,訪問運(yùn)行進(jìn)程的tss段
- 用與新特權(quán)級相關(guān)的棧段和棧指針裝載ss和esp寄存器。這些值可以在進(jìn)程的tss段中找到
- 在新的棧中保存ss和esp以前的值,這些值指明了與 舊特權(quán)級相關(guān)的棧的邏輯地址
若發(fā)生的是故障,用引起異常的指令地址修改cs 和eip寄存器的值,以使得這條指令在異常處理結(jié) 束后能被再次執(zhí)行
在棧中保存eflags、cs和eip的內(nèi)容
如果異常產(chǎn)生一個(gè)硬件出錯(cuò)碼,則將它保存在棧 中
裝載cs和eip寄存器,其值分別是IDT表中第i項(xiàng)門 描述符的段選擇符和偏移量字段。這對寄存器值 給出中斷或者異常處理程序的第一條指定的邏輯 地址
此時(shí)的進(jìn)程內(nèi)核態(tài)堆棧中斷返回詳細(xì)過程:
中斷/異常處理完后,相應(yīng)的處理程序會(huì) 執(zhí)行一條iret匯編指令,這條匯編指令讓 CPU控制單元做如下事情:
用保存在棧中的值裝載cs、eip和eflags寄存器。如果一個(gè)硬件出錯(cuò)碼曾被壓入棧中, 那么彈出這個(gè)硬件出錯(cuò)碼
檢查處理程序的特權(quán)級是否等于cs中最低 兩位的值(這意味著進(jìn)程在被中斷的時(shí)候是 運(yùn)行在內(nèi)核態(tài)還是用戶態(tài))。若是,iret終止 執(zhí)行;否則,轉(zhuǎn)入3
從棧中裝載ss和esp寄存器。這步意味著返 回到與舊特權(quán)級相關(guān)的棧
檢查ds、es、fs和gs段寄存器的內(nèi)容,如 果其中一個(gè)寄存器包含的選擇符是一個(gè)段描 述符,并且特權(quán)級比當(dāng)前特權(quán)級高,則清除 相應(yīng)的寄存器。這么做是防止懷有惡意的用 戶程序利用這些寄存器訪問內(nèi)核空間
三、fork子進(jìn)程啟動(dòng)執(zhí)行時(shí)進(jìn)程上下文的特殊之處
首先看一段程序及其執(zhí)行的結(jié)果來了解fork函數(shù)的作用:
#include <unistd.h>
#include <stdio.h>
int main()
{
int pid = fork();
if (pid == -1)
return -1;
if (pid)
{
printf("I am father, my pid is %d\n", getpid());
return 0;
}
else
{
printf("I am child, my pid is %d\n", getpid());
return 0;
}
}

看到這個(gè)結(jié)果是不是很奇怪,為什么if的分支執(zhí)行到了,else的分支也執(zhí)行到了。這明顯不符合程序執(zhí)行最基本的原理。這個(gè)放到后面再來解釋,先來了解一下fork這個(gè)函數(shù):
pid_t fork();上面是fork函數(shù)的原型,它有三個(gè)返回值
- 該進(jìn)程為父進(jìn)程時(shí),返回子進(jìn)程的pid
- 該進(jìn)程為子進(jìn)程時(shí),返回0
- fork執(zhí)行失敗,返回-1
fork的作用是克隆進(jìn)程,也就是將原先的一個(gè)進(jìn)程再克隆出一個(gè)來,克隆出的這個(gè)進(jìn)程就是原進(jìn)程的子進(jìn)程,這個(gè)子進(jìn)程和其他的進(jìn)程沒有什么區(qū)別,同樣擁有自己的獨(dú)立的地址空間。不同的是子進(jìn)程是在fork返回之后才開始執(zhí)行的,就像一把叉子一樣,執(zhí)行fork之后,父子進(jìn)程就分道揚(yáng)鑣了,所以fork這個(gè)名字就很形象,叉子的意思。fork給父進(jìn)程返回子進(jìn)程pid,給其拷貝出來的子進(jìn)程返回0,這也是他的特點(diǎn)之一,一次調(diào)用,兩次返回,所以與一般的系統(tǒng)調(diào)用處理流程也必定不同。
Linux下用于創(chuàng)建進(jìn)程的API有三個(gè)fork,vfork和clone,這三個(gè)函數(shù)分別是通過系統(tǒng)調(diào)用sys_fork,sys_vfork以及sys_clone實(shí)現(xiàn)的(目前討論的都是基于x86架構(gòu)的)。而且這三個(gè)系統(tǒng)調(diào)用,都是通過do_fork來實(shí)現(xiàn)的,只是傳入了不同的參數(shù)。所以我們可以得出結(jié)論:所有的子進(jìn)程是在do_fork實(shí)現(xiàn)創(chuàng)建和調(diào)用的。下面我們就來整理一下整個(gè)進(jìn)程的在用戶態(tài)到內(nèi)核態(tài)的過程是怎么樣的。fork系統(tǒng)調(diào)用如下:
do_fork的代碼如下(linux3.18.6):
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
long nr;
/*
* Determine whether and which event to report to ptracer. When
* called from kernel_thread or CLONE_UNTRACED is explicitly
* requested, no event is reported; otherwise, report if the event
* for the type of forking is enabled.
*/
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if ((clone_flags & CSIGNAL) != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p)) {
struct completion vfork;
struct pid *pid;
trace_sched_process_fork(current, p);
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
wake_up_new_task(p);
/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event_pid(trace, pid);
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}
put_pid(pid);
} else {
nr = PTR_ERR(p);
}
return nr;
}
整個(gè)創(chuàng)建新進(jìn)程是由copy_process()這個(gè)函數(shù)實(shí)現(xiàn)的,copy_process()做的主要工作如下:
1. 復(fù)制一個(gè)PCB——task_struct
p = dup_task_struct(current);
復(fù)制當(dāng)前進(jìn)程的PCB描述符task_struct。我們在進(jìn)入到該函數(shù)dup_task_struct體內(nèi)就可以看到這個(gè)pcb是如何復(fù)制的。主要的賦值函數(shù)是
err = arch_dup_task_struct(tsk, orig); //賦值操作
...
tsk = alloc_task_struct_node(node);
ti = alloc_thread_info_node(tsk, node);
tsk->stack = ti;
setup_thread_stack(tsk, orig); //這里只是復(fù)制thread_info,而非復(fù)制內(nèi)核堆棧
然而我們再 往dup_task_struct(current)函數(shù)往下看,后面是大量的修改進(jìn)程的內(nèi)容,也就是對復(fù)制過來的東西修改為子進(jìn)程所擁有的數(shù)據(jù)。也就是初始化一個(gè)子進(jìn)程,在copy_process()函數(shù)有一個(gè)非常重要的函數(shù)copy_thread,部分代碼如下:
struct pt_regs *childregs = task_pt_regs(p);
struct task_struct *tsk;
int err;
p->thread.sp = (unsigned long) childregs;
p->thread.sp0 = (unsigned long) (childregs+1);
memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));
if (unlikely(p->flags & PF_KTHREAD)) {
/* kernel thread */
memset(childregs, 0, sizeof(struct pt_regs));
p->thread.ip = (unsigned long) ret_from_kernel_thread;
task_user_gs(p) = __KERNEL_STACK_CANARY;
childregs->ds = __USER_DS;
childregs->es = __USER_DS;
childregs->fs = __KERNEL_PERCPU;
childregs->bx = sp; /* function */
childregs->bp = arg;
childregs->orig_ax = -1;
childregs->cs = __KERNEL_CS | get_kernel_rpl();
childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED;
p->thread.io_bitmap_ptr = NULL;
return 0;
}
*childregs = *current_pt_regs(); //拷貝父進(jìn)程的內(nèi)核堆棧棧底,也就是已有的內(nèi)核堆棧數(shù)據(jù)的拷貝
childregs->ax = 0; //給eax賦值為0,因?yàn)樽舆M(jìn)程返回的是0,系統(tǒng)調(diào)用是通過eax返回的,
if (sp)
childregs->sp = sp; //修改棧頂
p->thread.ip = (unsigned long) ret_from_fork; //給ip賦值,這就是子進(jìn)程執(zhí)行的起點(diǎn)
從用戶態(tài)的代碼看fork();函數(shù)返回了兩次,即在父子進(jìn)程中各返回一次,父進(jìn)程從系統(tǒng)調(diào)用中返回比較容易理解,子進(jìn)程從系統(tǒng)調(diào)用中返回,那它在系統(tǒng)調(diào)用處理過程中的哪里開始執(zhí)行的呢?這就涉及子進(jìn)程的內(nèi)核堆棧數(shù)據(jù)狀態(tài)和task_struct中thread記錄的sp和ip的一致性問題,這是在哪里設(shè)定的?copy_thread in copy_process
struct pt_regs *childregs = task_pt_regs(p);
*childregs = *current_pt_regs(); //復(fù)制內(nèi)核堆棧棧底
childregs->ax = 0; //為什么子進(jìn)程的fork返回0,這里就是原因!
p->thread.sp = (unsigned long) childregs; //調(diào)度到子進(jìn)程時(shí)的內(nèi)核棧頂
p->thread.ip = (unsigned long) ret_from_fork; //調(diào)度到子進(jìn)程時(shí)的第一條指令地址
上面賦值復(fù)制的內(nèi)核堆棧并不是父進(jìn)程的所有內(nèi)核堆棧的內(nèi)容,那復(fù)制的是哪些部分呢?我們可以看上面代碼的第一句,其中復(fù)制的內(nèi)容就是pt_regs里面的內(nèi)容。里面的代碼如下:
struct pt_regs {
long ebx;
long ecx;
long edx;
long esi;
long edi;
long ebp;
long eax;
int xds;
int xes;
int xfs;
int xgs;
long orig_eax;
long eip;
int xcs;
long eflags;
long esp;
int xss;
};
父進(jìn)程堆棧復(fù)制給子進(jìn)程的就是上面那些參數(shù)。從copy_thread中我們就已經(jīng)得出堆棧復(fù)制和子進(jìn)程開始執(zhí)行的起始地方??偨Y(jié)一下:linux創(chuàng)建一個(gè)新的進(jìn)程是從復(fù)制開始的,在系統(tǒng)內(nèi)核里首先是將父進(jìn)程的進(jìn)程控制塊PCB進(jìn)行拷貝,然后再根據(jù)自己的情況修改相應(yīng)的參數(shù),獲取自己的進(jìn)程號(hào),再開始執(zhí)行。內(nèi)核中主要通過do_fork()實(shí)現(xiàn),復(fù)制進(jìn)程主要是靠copy_process()完成的,整個(gè)過程實(shí)現(xiàn)如下:
-
p = dup_task_struct(current);為新進(jìn)程創(chuàng)建一個(gè)內(nèi)核棧、thread_iofo和task_struct,這里完全copy父進(jìn)程的內(nèi)容,所以到目前為止,父進(jìn)程和子進(jìn)程是沒有任何區(qū)別的 - 為新進(jìn)程在其內(nèi)存上建立內(nèi)核堆棧
- 對子進(jìn)程task_struct任務(wù)結(jié)構(gòu)體中部分變量進(jìn)行初始化設(shè)置,檢查所有的進(jìn)程數(shù)目是否已經(jīng)超出了系統(tǒng)規(guī)定的最大進(jìn)程數(shù),如果沒有的話,那么就開始設(shè)置進(jìn)程描訴符中的初始值,從這開始,父進(jìn)程和子進(jìn)程就開始區(qū)別開了。
- 父進(jìn)程的有關(guān)信息復(fù)制給子進(jìn)程,建立共享關(guān)系
- 設(shè)置子進(jìn)程的狀態(tài)為不可被TASK_UNINTERRUPTIBLE,從而保證這個(gè)進(jìn)程現(xiàn)在不能被投入運(yùn)行,因?yàn)檫€有很多的標(biāo)志位、數(shù)據(jù)等沒有被設(shè)置
- 復(fù)制標(biāo)志位(falgs成員)以及權(quán)限位(PE_SUPERPRIV)和其他的一些標(biāo)志
- 調(diào)用get_pid()給子進(jìn)程獲取一個(gè)有效的并且是唯一的進(jìn)程標(biāo)識(shí)符PID
-
return ret_from_fork;返回一個(gè)指向子進(jìn)程的指針,開始執(zhí)行
總結(jié)來說,進(jìn)程的創(chuàng)建過程大致是父進(jìn)程通過fork系統(tǒng)調(diào)用進(jìn)入內(nèi)核_ do_fork函數(shù),如下圖所示復(fù)制進(jìn)程描述符及相關(guān)進(jìn)程資源(采用寫時(shí)復(fù)制技術(shù))、分配子進(jìn)程的內(nèi)核堆棧并對內(nèi)核堆棧和 thread等進(jìn)程關(guān)鍵上下文進(jìn)行初始化,最后將子進(jìn)程放入就緒隊(duì)列,fork系統(tǒng)調(diào)用返回;而子進(jìn)程則在被調(diào)度執(zhí)行時(shí)根據(jù)設(shè)置的內(nèi)核堆棧和thread等進(jìn)程關(guān)鍵上下文開始執(zhí)行。

四、execve系統(tǒng)調(diào)用中斷上下文的特殊之處
首先看兩段程序及其執(zhí)行的結(jié)果來了解execve函數(shù)的作用:
/* execve.c */
#include <unistd.h>
#include <stdlib.h>
char *buf [] = {"/bin/sh", NULL};
void main(){
execve("/bin/sh", buf, 0);
exit(0);
}

/* execve2.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
int pid;
/* fork another process */
pid = fork();
if (pid < 0)
{
/* error occurred */
fprintf(stderr, "Fork Failed!");
exit(-1);
}
else if (pid == 0)
{
/* child process */
execlp("/bin/ls", "ls", NULL);
}
else
{
/* parent process */
/* parent will wait for the child to complete*/
wait(NULL);
printf("Child Complete!\n");
exit(0);
}
}

execve2.c簡化了的Shell程 序執(zhí)行l(wèi)s命令的過程。首先fork一個(gè)子進(jìn)程,pid為0的分支是將來的子進(jìn)程要執(zhí)行的,在子進(jìn)程里調(diào)用 execlp來加載可執(zhí)行程序ls。子進(jìn)程通過execlp加載可執(zhí)行程序時(shí)按如圖所示的結(jié)構(gòu)重新布局用戶態(tài)堆棧,可以看到用戶態(tài)堆棧的棧頂就是main函數(shù)調(diào)用堆??蚣?,這就是程序的main函數(shù)起點(diǎn)的執(zhí)行環(huán)境。

在布局一個(gè)新的用戶態(tài)堆棧時(shí),實(shí)際上是把命令行參數(shù)內(nèi)容和環(huán)境變量的內(nèi)容通過指針的方式傳到系統(tǒng)調(diào)用內(nèi)核處理函數(shù),再創(chuàng)建一個(gè)新的用戶態(tài)堆棧時(shí)會(huì)把這些char *argcv[]和char *envp[]等復(fù)制到用戶態(tài)堆棧中,來初始化這個(gè)新的可執(zhí)行程序的執(zhí)行上下文環(huán)境。所以新 的程序可以從main函數(shù)開始把對應(yīng)的參數(shù)接收過來,然后執(zhí)行
在調(diào)用execve系統(tǒng)調(diào)用時(shí),當(dāng)前的執(zhí)行環(huán)境是從父進(jìn)程復(fù)制過來的, execve系統(tǒng)調(diào)用加載完新的可執(zhí)行程序之后已經(jīng)覆蓋了原來父進(jìn)程的上下文環(huán)境。execve 系統(tǒng)調(diào)用在內(nèi)核中幫我們重新布局了新的用戶態(tài)執(zhí)行環(huán)境。
正常的一個(gè)系統(tǒng)調(diào)用都是陷入內(nèi)核態(tài),再返回到用戶態(tài),然后繼續(xù)執(zhí)行系統(tǒng)調(diào)用后的下一條指令。上文講到,fork和其他系統(tǒng)調(diào)用不同之處是它在陷入內(nèi)核態(tài)之后有兩次返回,第一次返回到原來的父進(jìn)程的位 置繼續(xù)向下執(zhí)行,這和其他的系統(tǒng)調(diào)用是一樣的。在子進(jìn)程中fork也返回了一次,會(huì)返回到一個(gè)特定的點(diǎn)——ret_from_fork,通過內(nèi)核構(gòu)造的堆棧環(huán)境,它可以正常系統(tǒng)調(diào)用返回到用戶態(tài),所以它 稍微特殊一點(diǎn)。同樣,execve也比較特殊。當(dāng)前的可執(zhí)行程序在執(zhí)行,執(zhí)行到execve系統(tǒng)調(diào)用時(shí)陷入內(nèi)核態(tài),在內(nèi)核里面用do_execve加載可執(zhí)行文件,把當(dāng)前進(jìn)程的可執(zhí)行程序給覆蓋掉。當(dāng)execve系統(tǒng)調(diào)用返回時(shí),返回的已經(jīng)不是原來的那個(gè)可執(zhí)行程序了,而是新的可執(zhí)行程序。execve返回的是新的可執(zhí)行程序執(zhí)行的起點(diǎn),靜態(tài)鏈接的可執(zhí)行文件也就是main函數(shù)的大致位置,動(dòng)態(tài)鏈接的可執(zhí)行文件還需 要ld鏈接好動(dòng)態(tài)鏈接庫再從main函數(shù)開始執(zhí)行。
Linux系統(tǒng)一般會(huì)提供了execl、execlp、execle、execv、execvp和execve等6個(gè)用以加載執(zhí)行 一個(gè)可執(zhí)行文件的庫函數(shù),這些庫函數(shù)統(tǒng)稱為exec函數(shù),差異在于對命令行參數(shù)和環(huán)境變量參數(shù) 的傳遞方式不同。exec函數(shù)都是通過execve系統(tǒng)調(diào)用進(jìn)入內(nèi)核,對應(yīng)的系統(tǒng)調(diào)用內(nèi)核處理函數(shù)為 sys_execve或__x64_sys_execve,它們都是通過調(diào)用do_execve來具體執(zhí)行加載可執(zhí)行文件的 ?作。
整體的調(diào)用關(guān)系為sys_execve() / x64_sys_execve ==> do_execve() ==> do_execveat_common() ==> do_execve_file ==> exec_binprm() ==> search_binary_handler() ==> load_elf_binary() ==> start_thread()。

execve執(zhí)行后具體處理過程:
檢查看可執(zhí)行文件的類型:當(dāng)進(jìn)入
execve()系統(tǒng)調(diào)用之后,Linux 內(nèi)核就開始進(jìn)行真正的裝載工作。在內(nèi)核中,execve()系統(tǒng)調(diào)用相應(yīng)的入口是sys_execve()。sys_execve()進(jìn)行一些參數(shù)的檢查復(fù)制之后,調(diào)用do_execve()。do_execve()會(huì)首先查找被執(zhí)行的文件,如果找到文件,則讀取文件的前 128 個(gè)字節(jié)。為什么要先讀取文件的前 128 個(gè)字節(jié)?這是因?yàn)長inux支持的可執(zhí)行文件不止 ELF 一種,還包括 a.out、Java 程序、以
#!開頭的腳本程序。do_execve()通過讀取前 128 個(gè)字節(jié)來判斷文件的格式。每種可執(zhí)行文件格式的開頭幾個(gè)字節(jié)都是很特殊的,尤其是前4個(gè)字節(jié),被稱為 魔數(shù)(Magic Number)。
搜索匹配裝載處理過程:當(dāng)
do_execve()讀取了128個(gè)字節(jié)的文件頭部之后,調(diào)用search_binary_handle()去搜索和匹配合適的可執(zhí)行文件裝載處理過程。Linux 中所有被支持的可執(zhí)行文件格式都有相應(yīng)的裝載處理過程,search_binary_handler()會(huì)通過判斷頭部的魔術(shù)確定文件的格式,并且調(diào)用相應(yīng)的裝載處理過程。常見的可執(zhí)行程序及其裝載處理過程的對應(yīng)關(guān)系如下所示.
- ELF 可執(zhí)行文件:
load_elf_binary()- a.out 可執(zhí)行文件:
load_aout_binary()- 可執(zhí)行腳本程序:
load_script()裝載執(zhí)行可執(zhí)行文件
以 ELF 的裝載處理過程load_elf_binary()為例,其所包含的步驟如下圖所示:
load_elf_binary裝載過程
- 操作系統(tǒng)讀取可執(zhí)行文件 ELF 的
Header,檢查文件的有效性。- 操作系統(tǒng)讀取可執(zhí)行文件 ELF的
Program Header Table中讀取每個(gè)Segment的虛擬地址、文件地址、屬性等。- 操作系統(tǒng)根據(jù)
Program Header Table將可執(zhí)行文件 ELF 映射至內(nèi)存。- 如果是靜態(tài)鏈接的情況,則直接跳轉(zhuǎn)至第 7 步;如果是動(dòng)態(tài)鏈接的情況,操作系統(tǒng)將查找
.interp節(jié),找到 動(dòng)態(tài)鏈接器(Dynamic Linker) 的位置,并啟動(dòng)動(dòng)態(tài)鏈接器。在 Linux 下,動(dòng)態(tài)鏈接器ld.so是一個(gè)共享對象,操作系統(tǒng)同樣通過映射的方式將它加載到進(jìn)程的地址空間。操作系統(tǒng)在加載完后,將控制權(quán)交給動(dòng)態(tài)鏈接器的入口。- 動(dòng)態(tài)鏈接器獲得控制權(quán)后,開始執(zhí)行一系列初始化操作。
- 動(dòng)態(tài)鏈接器根據(jù)當(dāng)前的環(huán)境參數(shù),對可執(zhí)行文件進(jìn)行動(dòng)態(tài)鏈接工作。
- 控制權(quán)被轉(zhuǎn)交到可執(zhí)行文件的入口地址,程序開始正式執(zhí)行。
參考文章:
- https://zhuanlan.zhihu.com/p/52845869
- https://www.cnblogs.com/anyux/articles/10585301.html
- https://blog.csdn.net/u010820757/article/details/48344991
- https://blog.csdn.net/hustyangju/article/details/40340633
- https://blog.csdn.net/Always2015/article/details/45008785
- https://blog.csdn.net/Always2015/article/details/45008785
- https://www.dazhuanlan.com/2019/10/13/5da214a7b0483/

