中斷上下文切換的一般流程及fork、execve系統(tǒng)調(diào)用的特殊之處

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)資源
Linux特權(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)程上下文切換

進(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)系
  1. 應(yīng)用程序代碼調(diào)用 xyz(),該函數(shù)是一個(gè)包裝系統(tǒng)調(diào)用的庫函數(shù);
  2. 庫函數(shù) xyz() 負(fù)責(zé)準(zhǔn)備向內(nèi)核傳遞的參數(shù),并觸發(fā)軟中斷以切換到內(nèi)核;
  3. CPU 被軟中斷打斷后,執(zhí)行中斷處理函數(shù),即系統(tǒng)調(diào)用處理函數(shù)(system_call);
  4. 系統(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)用后的大致流程:

  1. 正在運(yùn)行的用戶態(tài)進(jìn)程X

  2. 發(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)
  3. SAVE_ALL,保存現(xiàn)場,此時(shí)完成了中斷上下文切換,即從進(jìn)程X的用戶態(tài)到進(jìn)程X的內(nèi)核態(tài)

  4. 中斷處理過程中或中斷返回前調(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)容

  5. 標(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í)行)

  6. 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了

  7. 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)

  8. 繼續(xù)運(yùn)行用戶態(tài)進(jìn)程Y

中斷進(jìn)入詳細(xì)過程:

  1. 確定與中斷或者異常關(guān)聯(lián)的向量i(0~255)

  2. 讀idtr寄存器指向的IDT表中的第i項(xiàng) 3,從gdtr寄存器獲得GDT的基地址,并在GDT中查找, 以讀取IDT表項(xiàng)中的段選擇符所標(biāo)識(shí)的段描述符

  3. 從gdtr寄存器獲得GDT的基地址,并在GDT中查找, 以讀取IDT表項(xiàng)中的段選擇符所標(biāo)識(shí)的段描述符

  4. 確定中斷是由授權(quán)的發(fā)生源發(fā)出的

  5. 檢查是否發(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)的棧的邏輯地址
  6. 若發(fā)生的是故障,用引起異常的指令地址修改cs 和eip寄存器的值,以使得這條指令在異常處理結(jié) 束后能被再次執(zhí)行

  7. 在棧中保存eflags、cs和eip的內(nèi)容

  8. 如果異常產(chǎn)生一個(gè)硬件出錯(cuò)碼,則將它保存在棧 中

  9. 裝載cs和eip寄存器,其值分別是IDT表中第i項(xiàng)門 描述符的段選擇符和偏移量字段。這對寄存器值 給出中斷或者異常處理程序的第一條指定的邏輯 地址

此時(shí)的進(jìn)程內(nèi)核態(tài)堆棧

中斷返回詳細(xì)過程:

中斷/異常處理完后,相應(yīng)的處理程序會(huì) 執(zhí)行一條iret匯編指令,這條匯編指令讓 CPU控制單元做如下事情:

  1. 用保存在棧中的值裝載cs、eip和eflags寄存器。如果一個(gè)硬件出錯(cuò)碼曾被壓入棧中, 那么彈出這個(gè)硬件出錯(cuò)碼

  2. 檢查處理程序的特權(quán)級是否等于cs中最低 兩位的值(這意味著進(jìn)程在被中斷的時(shí)候是 運(yùn)行在內(nèi)核態(tài)還是用戶態(tài))。若是,iret終止 執(zhí)行;否則,轉(zhuǎn)入3

  3. 從棧中裝載ss和esp寄存器。這步意味著返 回到與舊特權(quán)級相關(guān)的棧

  4. 檢查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;
    }
}
執(zhí)行結(jié)果

看到這個(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)用如下:

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)如下:

  1. 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ū)別的
  2. 為新進(jìn)程在其內(nèi)存上建立內(nèi)核堆棧
  3. 對子進(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ū)別開了。
  4. 父進(jìn)程的有關(guān)信息復(fù)制給子進(jìn)程,建立共享關(guān)系
  5. 設(shè)置子進(jìn)程的狀態(tài)為不可被TASK_UNINTERRUPTIBLE,從而保證這個(gè)進(jìn)程現(xiàn)在不能被投入運(yùn)行,因?yàn)檫€有很多的標(biāo)志位、數(shù)據(jù)等沒有被設(shè)置
  6. 復(fù)制標(biāo)志位(falgs成員)以及權(quán)限位(PE_SUPERPRIV)和其他的一些標(biāo)志
  7. 調(diào)用get_pid()給子進(jìn)程獲取一個(gè)有效的并且是唯一的進(jìn)程標(biāo)識(shí)符PID
  8. 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);
}
execve.c執(zhí)行結(jié)果
/*  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執(zhí)行結(jié)果

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()。

do_execve流程

execve執(zhí)行后具體處理過程:

  1. 檢查看可執(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.outJava 程序、#! 開頭的腳本程序。do_execve()通過讀取前 128 個(gè)字節(jié)來判斷文件的格式。每種可執(zhí)行文件格式的開頭幾個(gè)字節(jié)都是很特殊的,尤其是前4個(gè)字節(jié),被稱為 魔數(shù)(Magic Number)。

  1. 搜索匹配裝載處理過程:當(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()
  2. 裝載執(zhí)行可執(zhí)行文件
    以 ELF 的裝載處理過程 load_elf_binary() 為例,其所包含的步驟如下圖所示:

    load_elf_binary裝載過程

    1. 操作系統(tǒng)讀取可執(zhí)行文件 ELF 的 Header,檢查文件的有效性。
    2. 操作系統(tǒng)讀取可執(zhí)行文件 ELF的 Program Header Table 中讀取每個(gè) Segment 的虛擬地址、文件地址、屬性等。
    3. 操作系統(tǒng)根據(jù) Program Header Table 將可執(zhí)行文件 ELF 映射至內(nèi)存。
    4. 如果是靜態(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)鏈接器的入口。
    5. 動(dòng)態(tài)鏈接器獲得控制權(quán)后,開始執(zhí)行一系列初始化操作。
    6. 動(dòng)態(tài)鏈接器根據(jù)當(dāng)前的環(huán)境參數(shù),對可執(zhí)行文件進(jìn)行動(dòng)態(tài)鏈接工作。
    7. 控制權(quán)被轉(zhuǎn)交到可執(zhí)行文件的入口地址,程序開始正式執(zhí)行。

參考文章:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容