溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何使用分身術(shù)fork和變身術(shù)exec創(chuàng)建新進程

發(fā)布時間:2021-10-11 11:56:40 來源:億速云 閱讀:198 作者:iii 欄目:編程語言

這篇文章主要介紹“如何使用分身術(shù)fork和變身術(shù)exec創(chuàng)建新進程”,在日常操作中,相信很多人在如何使用分身術(shù)fork和變身術(shù)exec創(chuàng)建新進程問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”如何使用分身術(shù)fork和變身術(shù)exec創(chuàng)建新進程”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

一、分身術(shù)fork

準(zhǔn)確的說應(yīng)該是影分身,火影里面普通的分身術(shù)和影分身的區(qū)別知道吧,不知道感興趣的可以去看看火影,咱這就不解釋了,不然變成火影的公眾號了。

不過咱們還是要來百科百科影分身,官方解釋為:使用查克拉造出有實體的分身,具有獨立于本體的意識和一定的抗打擊能力,可應(yīng)用于各種忍術(shù)之上,正常解除后分身的記憶和經(jīng)驗會回歸本體。

而我們的新進程呢,是使用一定物理空間來創(chuàng)建自己的PCB,頁表等結(jié)構(gòu),它是獨立于父進程存在的一個進程,能夠被調(diào)度上CPU,可運行各種新程序,運行完后退出再由父進程回收。這個過程簡直完美契合影分身之術(shù)有沒有,簡直懷疑岸本齊史是不是另有一個計算機兼職。

1. fork 殘卷秘籍

下面我們來具體看看影分身fork這個秘籍,但是呢 fork 大家都應(yīng)該都很熟悉了,就不再做過多的鋪墊介紹,簡單來說就是根據(jù)父進程克隆出一個幾乎一模一樣的子進程出來。在這兒也不舉 fork 那個 if-else 判斷 pid 的經(jīng)典的卻老掉牙的例子了,咱們來談點不一樣的。

首先來看看影分身簡化版的殘卷秘籍(這種方式對于計算機來說是低效的)

如何使用分身術(shù)fork和變身術(shù)exec創(chuàng)建新進程

上述的 fork 只是殘卷,主要是想說明 fork 的一種實現(xiàn)過程思路。雖然這種方式在忍術(shù)中被列為B級,但是在計算機的世界里,將父進程的資源全部拷貝一份的實現(xiàn)方式是非常低效的,后面我們會講另一種高效的方式:寫時復(fù)制?,F(xiàn)在先來看看下面幾個通用的問題:

2. 常見問題

調(diào)用一次,返回兩次

這是CSAPP里面的原話,個人認為單獨說這么一句總結(jié)性的話語是有歧義的,對于初次接觸到fork的朋友來說可能很迷惑。一個函數(shù)只能有一次返回,是不可能返回兩次的。即使我們平時寫程序時可能會使用多個 return 語句,但最終肯定只會從一個 return 中返回。那fork函數(shù)作何解釋呢?

fork 之后一個進程就變成了兩個進程,兩個進程兩個fork 兩個返回,而不是說一個 fork 函數(shù)就返回兩次

父子進程返回的數(shù)不一樣?

fork 函數(shù)有三種返回值:

  • 在父進程中會返回子進程的 pid

  • 子進程中返回0

  • 出錯的話返回-1

子進程是克隆出來的,返回值怎么還不一樣?看清前面說的,根據(jù)父進程克隆出幾乎一模一樣的子進程來,說明并不是完全相同

那返回值是怎么回事呢?在Linux里面系統(tǒng)調(diào)用采用中斷門實現(xiàn),所以調(diào)用 fork 時會觸發(fā)中斷,中斷就會保存上下文,其中包括了eax寄存器的值

據(jù)調(diào)用約定,eax 寄存器里面存放的是返回值,所以據(jù)上面的殘卷可以看出,fork 時會修改子進程中斷上下文里的 eax 為0。如此父進程中的 fork 和子進程中的 fork 便會返回不一樣的值。

而返回 -1,多數(shù)情況是進程數(shù)達到上限或者內(nèi)存不足,這種情況下根本就沒有創(chuàng)建新的進程,也談不上兩次返回和返回不同的值。

fork 之后接著 fork 后面的代碼運行

這似乎是廢話,但是為什么呢?我看到CSDN上有篇博客是這樣回答的,大概意思是 fork 函數(shù)只是將后面要執(zhí)行的代碼拷貝到新的進程,這篇博客的訪問點贊評論都很高。但是私以為這種說法是不對的,至少在我看的一些系統(tǒng) fork 源碼中沒有這么實現(xiàn)的。

那為什么 fork 之后父進程子進程都是是接著 fork 后面的代碼運行呢?其實很簡單,就是中斷上下文的保存于恢復(fù)。前面說過 fork 系統(tǒng)調(diào)用通過中斷實現(xiàn),中斷時父進程保存了當(dāng)前執(zhí)行流的位置即 cs:eip 的值,然后 fork 函數(shù)復(fù)制了一份給子進程,所以父進程子進程中斷返回時都會繼續(xù)執(zhí)行fork后面的代碼。

因此fork前是一個進程在執(zhí)行,fork 后是兩個進程在執(zhí)行同一塊兒代碼(如果沒調(diào)用 exec 變身的話)

最后來看低效版的 fork 動態(tài)圖,實實在在的將父進程的資源復(fù)制了一份。

(抱歉放不了動圖,可去我的公號查看)

二、變身術(shù)exec

1. exec 函數(shù)

前面我們的分身術(shù) fork 函數(shù)只能克隆出來一個與父進程幾乎相同的子進程,它們執(zhí)行的是同一個程序,但經(jīng)常我們需要的是一個全新的進程,它能運行其他程序。這就需要變身,用到 exec 函數(shù)。exec 函數(shù)總共有6個,其中execve是內(nèi)核的系統(tǒng)調(diào)用,其他5個execl, execv, execle, execlp, execvp都是在execve之上實現(xiàn)的。

execve函數(shù)原型如下:

如何使用分身術(shù)fork和變身術(shù)exec創(chuàng)建新進程

  • const char* filename,可執(zhí)行文件的完整路徑

  • char* const argv[] ,以NULL結(jié)束的字符串指針數(shù)組的地址,每個字符串表示一個命令行參數(shù)

  • char* const envp[],以NULL結(jié)束的字符串指針數(shù)組的地址,每個字符串以NAME=value的形式表示一個環(huán)境變量,通常直接傳參NULL。

2. ELF文件

我們要加載的文件叫做可執(zhí)行目標(biāo)文件,Linux里面可執(zhí)行目標(biāo)文件的格式為ELF,而Windows里面是PE,注意不是 exe,exe 只是后綴名。

ELF格式簡介

ELF 指的是 Executable and Linkable Format,可執(zhí)行可鏈接格式。從命名中也可以看出它有兩種視圖:執(zhí)行和鏈接兩種視圖。

如何使用分身術(shù)fork和變身術(shù)exec創(chuàng)建新進程

上面這圖大家應(yīng)該都很熟悉了吧,后面兩種目標(biāo)文件,可重定位目標(biāo)文件和可執(zhí)行目標(biāo)文件就分別對應(yīng)著ELF格式文件的鏈接視圖和執(zhí)行視圖。

ELF文件格式

細究ELF文件的話,內(nèi)容還是很多的,我們在這兒撿重點,exec用的上的說:

先來從總體上看看兩種視圖的結(jié)構(gòu):

如何使用分身術(shù)fork和變身術(shù)exec創(chuàng)建新進程 鏈接視圖以節(jié)為單位,執(zhí)行視圖以段為單位。這里的段和我們所說的內(nèi)存分段的段的含義是不同的,要區(qū)分開。

實際的ELF文件里面的節(jié)和段很多,這里只是列出了比較重要需要了解的一部分,下面簡要說明一下:

  • .text:代碼部分

  • .rodata: 只讀的數(shù)據(jù),例如 printf 中的格式串,switch-case 中的跳轉(zhuǎn)表

  • .data:已初始化的全局變量

  • .bss:未初始化的全局變量,局部靜態(tài)變量

  • .symtab:symbol table,符號表,程序里面的全局變量名和函數(shù)名都屬于符號,這些符號信息保存到符號表

  • .rel.text,.rel.data:與可重定位相關(guān)的信息

  • .debug,調(diào)試所用的符號表

  • .init,包含可執(zhí)行的指令,進程初始化代碼的一部分,要在執(zhí)行main函數(shù)之前執(zhí)行這些代碼

ELF Header

如何使用分身術(shù)fork和變身術(shù)exec創(chuàng)建新進程

各元素表示的意思大都已經(jīng)說明,根據(jù)命名應(yīng)該還是很好記住各元素所代表的意義,下面再重點說幾點:

  • e_ident前4位是固定的魔數(shù),e_ident[0] = 0x7f,e_ident[1] = 'E', e_ident[2] = 'L', e_ident[3] = 'F',表明這是一個 ELF 文件

  • e_ident[5]用來指定大端還是小端字節(jié)序,1表小端,2表大端,0表非法編碼格式

  • e_type,ELF 目標(biāo)文件類型,如可重定位,可執(zhí)行,動態(tài)共享目標(biāo)文件

  • e_entry,這個可執(zhí)行文件的入口地址,exec 加載完程序之后就從這兒開始運行

Program Header

如何使用分身術(shù)fork和變身術(shù)exec創(chuàng)建新進程

同上簡單解釋幾點:

  1. 程序段的類型有很多,我們只需要了解可裝載段,顧名思義,需要裝載到內(nèi)存里面的段,比如代碼段,數(shù)據(jù)段。

  2. 這里涉及了多種段,程序段類型里面的段,數(shù)據(jù)段代碼段等里面的段,還有內(nèi)存的分段,都是段不要混淆了。

  3. 一般說來 p_filesz <= p_memsz,這是因為bss節(jié)的存在,它并不存在與文件中,僅存在與運行時的內(nèi)存當(dāng)中。這是因為 bss 節(jié)中存放的是未初始化的全局變量,它們的值是無意義的,如果我們在文件中分配空間將這些變量的值存儲下來也就無意義。所以我們的目標(biāo)文件中其實并不需要 bss 的實體,只需要記錄bss 的大小位置等相關(guān)信息即可。

  4. 雖然在文件中存儲變量沒有意義,但是人家好歹也是未初始化的全局變量,需要在內(nèi)存中專門為它們開辟空間存儲它們。

3. exec殘卷秘籍

從上面的 ELF Header 和 Program Header 中可以看出程序各段的大小位置都已經(jīng)確定好了了,我們只需要將它們加載到相應(yīng)位置即可,來看看 exec 變身術(shù)的殘卷秘籍:

如何使用分身術(shù)fork和變身術(shù)exec創(chuàng)建新進程

同 fork 那本秘籍,主要是想展現(xiàn) exec 實現(xiàn)的一個大致過程思路,每個步驟寫的應(yīng)該還是比較清晰,照例下面說幾點重點:

Load裝載映射

裝載映射的段都是可裝載段,具體的裝載過程可以用讀取文件 read 和 lseek 系統(tǒng)調(diào)用來實現(xiàn)。

read 的作用就是讀取文件到內(nèi)存的一個緩沖區(qū),而 read,lseek 兩函數(shù)需要的參數(shù)在程序頭中都有記錄,所以理論上來講實現(xiàn)起來應(yīng)該是很容易的。

入口地址e_entry

exec 需要修改原進程內(nèi)核棧中的一些信息,最主要的就是將中斷上下文里面的 eip 改為 ELF 文件中的入口地址。

ELF頭中的 p_entry 入口地址是什么?是main函數(shù)的地址嗎?非也。那是什么呢?這要牽扯一個概念,運行庫,運行庫涉及的知識很多,在這就長話短說,講講與本文有關(guān)的。

簡單說來,運行庫就是標(biāo)準(zhǔn)庫的擴展,會在 main 函數(shù)運行之前準(zhǔn)備好環(huán)境,運行完之后再進行收尾的工作。

本文就只說說準(zhǔn)備運行環(huán)境的部分,這部分可以看做是一個函數(shù),全局符號為_start,也就是函數(shù)名為_start。_start才是我們運行的第一個函數(shù),ELF 頭中的入口地址 p_entry 就是它

_start 函數(shù)的工作之一就是壓入 main 函數(shù)的參數(shù)。

前面我們的偽碼中是把實際的命令行參數(shù)傳到了用戶態(tài)的線性空間中,但是要清楚 main 函數(shù)的參數(shù)可不是實際的命令行參數(shù),而是命令行參數(shù)的個數(shù)和字符串指針數(shù)組的地址。這兩個參數(shù)壓棧操作就在_start 中進行。畢竟 main 函數(shù)也是一個被調(diào)用的函數(shù),在調(diào)用之前需要傳參。

exec返回

exec函數(shù)如果發(fā)生錯誤會返回-1,正確則不返回。

exec 函數(shù)里面還調(diào)用了許多其他函數(shù),這些函數(shù)出錯,exec 沒能繼續(xù)運行下去的話是會直接返回-1的,只是上面?zhèn)未a沒體現(xiàn)出來。

要知道 exec 這個函數(shù)就像是推到原進程然后重來,改變了很多信息,中斷的執(zhí)行上下文被大幅度改變,調(diào)用 exec 的代碼也是不復(fù)存在的。從這個角度看exec從未成功返回,取而代之的是執(zhí)行的新程序被映射大進程的地址空間。

以上來自深入理解 Linux 內(nèi)核的解釋,感覺聽抽象模糊?也可以嘗試這樣理解,來自于操作系統(tǒng)真相還原的一個系統(tǒng)設(shè)計。

在這個OS設(shè)計里,exec 如果成功運行到最后,直接使用 jmp 語句跳到中斷退出點。jmp 語句不像 call,它是有去無回的,所以沒有不會再返回到 exec 函數(shù)里,而是直接彈出中斷上下文的 eip 入口地址去運行新程序了。

再來看看exec的動態(tài)圖,想要表達的意思很簡單,就是在原進程上推到重來:

(抱歉放不了動圖,可去我的公號查看)

好了,關(guān)于分身術(shù)和變身術(shù)咱們就傳授到這,下面我們要來學(xué)以致用,推陳出新,創(chuàng)造一門新忍術(shù)。

三、寫時復(fù)制COW

前面說過,影分身之術(shù)雖然等級很高但是有弊端的,特別是在施展多重影分身之術(shù)時可能會因為查克拉消耗太過劇烈而傷及自身,所以被列為禁術(shù)。而同樣的,咱們最初版的 fork 因為復(fù)制了父進程的全部資源而浪費了太多時間空間,也不再使用。

現(xiàn)在的 fork 都是用了寫時復(fù)制技術(shù),這項技術(shù)可了不得,面試中經(jīng)常提到。岸本齊史肯定不會這個,不然的話肯定再創(chuàng)一門 S級忍術(shù),沒有實體的假分身可以變成真的,真的可以變成假的。真真假假,虛虛實實,補不足而損有余,這樣就可以減少不必要的查克拉消耗,還能達到兵者詭道也的效果。

扯遠了扯遠了,寫時復(fù)制這項技術(shù)可沒有那么強大,但也有類似的機制和目的,減少空間時間消耗,高效的完成進程創(chuàng)建任務(wù),來具體看看:

1. 初代版本的fork具體弊端

前面我們的fork是傻瓜性的,真的將父進程的所有資源全部復(fù)制了一份,但實際上是不必要的。

如果我們不調(diào)用 exec 運行新程序,那么實際上父子倆進程很多的資源是可以共用的,比如代碼部分

而如果調(diào)用 exec 來執(zhí)行新程序,exec 要刪除掉已存在的用戶區(qū)域,復(fù)制父進程的資源也無意義。所以這樣的fork有很大弊端,不適用。

2. 具體的COW

顧名思義的簡單解釋就是,fork 時不會真的分配新的物理頁復(fù)制資源,子進程直接引用共享父進程的物理空間。只有一個進程要寫數(shù)據(jù),改變共享內(nèi)容時,才單獨復(fù)制一份出來。

這樣就避免了不必要的資源復(fù)制。在面試中肯定不能只回答這么一點內(nèi)容,那是過不了關(guān)的,咱們還需細剖注意幾個問題,就直接已干貨的形式羅列出來了,如下所示。

  1. 即使利用寫時復(fù)制技術(shù),fork時也還是為子進程創(chuàng)建一些單獨的資源,比如PCB,頁表。也就是說要為其分配新的物理空間來存儲這些資源,這些東西是不會在物理上共享的。

  2. 父子進程各自擁有一套頁表,子進程的頁表從父進程哪兒復(fù)制過來的,內(nèi)容是相同,所以父子進程映射到了同一個物理空間。但因為是兩套頁表,所以父子進程的虛擬地址空間是不同的,只是說兩個虛擬地址空間對應(yīng)的是同一個物理空間?;蛘甙凑誄SAPP里面的話來說,相同但獨立的地址空間。表述的可能不太一樣,但實際表達的意思是一樣的,能清楚明白是指就好。

  3. 寫時復(fù)制的實現(xiàn)原理:

  • 將兩個進程的頁面標(biāo)記為只讀,這是通過設(shè)置頁表項里面的存取權(quán)限位來實現(xiàn)的。

  • 將兩個進程的區(qū)域結(jié)構(gòu)都標(biāo)記為私有的寫時復(fù)制,這是通過設(shè)置進程的vm_area_struct結(jié)構(gòu)體里面的vm_flags字段來實現(xiàn)的

  • 如果父子進程都是讀取相同的物理頁,那么父子之間是相安無事的。但是只要有一個寫就會起沖突,內(nèi)核就會把這個頁的內(nèi)容拷貝到一個新分配的物理頁,并更新寫進程的頁表項使其指向新分配的這個物理頁。最后再回復(fù)頁面的可寫屬性。

再來看看動圖直觀感受一下:

(抱歉放不了動圖,可去我的公號查看)

所以啊,fork之后,你以為現(xiàn)在是父子兩個進程實體,但實際上內(nèi)存里面只有父進程一個完整的實體。你又以為子進程在內(nèi)存里面沒多少自己單獨的資源時,過了一會兒,說不定又因為寫操作給分配了。

所以吧,真不是我生搬硬套,這虛虛實實的感覺與我那創(chuàng)造的S級忍術(shù)還是有些相似的對吧。到現(xiàn)在這個S級術(shù)法還沒取名字呢,為了紀(jì)念寫時復(fù)制技術(shù),而且這個術(shù)法這么厲害,干脆就叫做牛影分身吧。(為啥叫這名兒能懂吧,沒看明白的看看寫時復(fù)制的英文簡寫?)

四、fork、vfork、clone

最后這一部分簡單談?wù)勆鲜鋈齻€函數(shù)的區(qū)別,同樣的不多說直接以干貨的形式羅列出來:

1. vfork

  • vfork就是為了避免fork時的大量無用復(fù)制而設(shè)計的。

  • vfork創(chuàng)建進程時連父進程的頁表都不會復(fù)制,完全使用父進程的資源,運行在父進程的地址空間中。子進程對數(shù)據(jù)的任何修改也就是對父進程的數(shù)據(jù)修改。

  • vfork會保證子進程先運行,而fork不會,要看調(diào)度情況。

  • 父進程則一直被阻塞,直到子進程調(diào)用exec有了自己的地址空間或者退出時,父進程才會被重新調(diào)度。

由上可以看出vfork的系統(tǒng)開銷很小,似乎很有競爭力,但是由于現(xiàn)在的fork采用了寫時復(fù)制技術(shù),相比之下vfork的競爭力也不是那么強了,所以現(xiàn)在已經(jīng)漸漸淡出內(nèi)核

2. clone

  • clone這個函數(shù)功能很齊全,參數(shù)也多,使用其他比較復(fù)雜。我們可以使用不同的參數(shù)組合來選擇性的復(fù)制父進程的資源。

  • 傳統(tǒng)的fork函數(shù)還有vfork函數(shù)就是依據(jù)clone來實現(xiàn)的。

  • clone函數(shù)的主要用處還是來創(chuàng)建線程,也就是輕量級進程。

關(guān)于這部分就先說這么多吧,了解了解即可,最常用的還是fork函數(shù)。

到此,關(guān)于“如何使用分身術(shù)fork和變身術(shù)exec創(chuàng)建新進程”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI