重慶分公司,新征程啟航
為企業提供網站建設、域名注冊、服務器等服務
為企業提供網站建設、域名注冊、服務器等服務
這篇文章主要介紹“如何使用分身術fork和變身術exec創建新進程”,在日常操作中,相信很多人在如何使用分身術fork和變身術exec創建新進程問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”如何使用分身術fork和變身術exec創建新進程”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
讓客戶滿意是我們工作的目標,不斷超越客戶的期望值來自于我們對這個行業的熱愛。我們立志把好的技術通過有效、簡單的方式提供給客戶,將通過不懈努力成為客戶在信息化領域值得信任、有價值的長期合作伙伴,公司提供的服務項目有:域名注冊、網絡空間、營銷軟件、網站建設、任丘網站維護、網站推廣。
準確的說應該是影分身,火影里面普通的分身術和影分身的區別知道吧,不知道感興趣的可以去看看火影,咱這就不解釋了,不然變成火影的公眾號了。
不過咱們還是要來百科百科影分身,官方解釋為:使用查克拉造出有實體的分身,具有獨立于本體的意識和一定的抗打擊能力,可應用于各種忍術之上,正常解除后分身的記憶和經驗會回歸本體。
而我們的新進程呢,是使用一定物理空間來創建自己的PCB,頁表等結構,它是獨立于父進程存在的一個進程,能夠被調度上CPU,可運行各種新程序,運行完后退出再由父進程回收。這個過程簡直完美契合影分身之術有沒有,簡直懷疑岸本齊史是不是另有一個計算機兼職。
下面我們來具體看看影分身fork這個秘籍,但是呢 fork大家都應該都很熟悉了,就不再做過多的鋪墊介紹,簡單來說就是根據父進程克隆出一個幾乎一模一樣的子進程出來。在這兒也不舉 fork 那個 if-else 判斷 pid 的經典的卻老掉牙的例子了,咱們來談點不一樣的。
首先來看看影分身簡化版的殘卷秘籍(這種方式對于計算機來說是低效的)
上述的 fork 只是殘卷,主要是想說明 fork 的一種實現過程思路。雖然這種方式在忍術中被列為B級,但是在計算機的世界里,將父進程的資源全部拷貝一份的實現方式是非常低效的,后面我們會講另一種高效的方式:寫時復制。現在先來看看下面幾個通用的問題:
這是CSAPP里面的原話,個人認為單獨說這么一句總結性的話語是有歧義的,對于初次接觸到fork的朋友來說可能很迷惑。一個函數只能有一次返回,是不可能返回兩次的。即使我們平時寫程序時可能會使用多個 return 語句,但最終肯定只會從一個 return 中返回。那fork函數作何解釋呢?
fork 之后一個進程就變成了兩個進程,兩個進程兩個fork 兩個返回,而不是說一個 fork 函數就返回兩次
fork 函數有三種返回值:
在父進程中會返回子進程的 pid
子進程中返回0
出錯的話返回-1
子進程是克隆出來的,返回值怎么還不一樣?看清前面說的,根據父進程克隆出幾乎一模一樣的子進程來,說明并不是完全相同。
那返回值是怎么回事呢?在Linux里面系統調用采用中斷門實現,所以調用 fork 時會觸發中斷,中斷就會保存上下文,其中包括了eax寄存器的值。
據調用約定,eax 寄存器里面存放的是返回值,所以據上面的殘卷可以看出,fork 時會修改子進程中斷上下文里的 eax 為0。如此父進程中的 fork 和子進程中的 fork 便會返回不一樣的值。
而返回 -1,多數情況是進程數達到上限或者內存不足,這種情況下根本就沒有創建新的進程,也談不上兩次返回和返回不同的值。
這似乎是廢話,但是為什么呢?我看到CSDN上有篇博客是這樣回答的,大概意思是 fork 函數只是將后面要執行的代碼拷貝到新的進程,這篇博客的訪問點贊評論都很高。但是私以為這種說法是不對的,至少在我看的一些系統 fork 源碼中沒有這么實現的。
那為什么 fork 之后父進程子進程都是是接著 fork 后面的代碼運行呢?其實很簡單,就是中斷上下文的保存于恢復。前面說過 fork 系統調用通過中斷實現,中斷時父進程保存了當前執行流的位置即 cs:eip 的值,然后 fork 函數復制了一份給子進程,所以父進程子進程中斷返回時都會繼續執行fork后面的代碼。
因此fork前是一個進程在執行,fork 后是兩個進程在執行同一塊兒代碼(如果沒調用 exec 變身的話)
最后來看低效版的 fork 動態圖,實實在在的將父進程的資源復制了一份。
(抱歉放不了動圖,可去我的公號查看)
前面我們的分身術 fork 函數只能克隆出來一個與父進程幾乎相同的子進程,它們執行的是同一個程序,但經常我們需要的是一個全新的進程,它能運行其他程序。這就需要變身,用到 exec 函數。exec 函數總共有6個,其中execve是內核的系統調用,其他5個execl, execv, execle, execlp, execvp都是在execve之上實現的。
execve函數原型如下:
const char* filename,可執行文件的完整路徑
char* const argv[] ,以NULL結束的字符串指針數組的地址,每個字符串表示一個命令行參數
char* const envp[],以NULL結束的字符串指針數組的地址,每個字符串以NAME=value的形式表示一個環境變量,通常直接傳參NULL。
我們要加載的文件叫做可執行目標文件,Linux里面可執行目標文件的格式為ELF,而Windows里面是PE,注意不是 exe,exe 只是后綴名。
ELF 指的是 Executable and Linkable Format,可執行可鏈接格式。從命名中也可以看出它有兩種視圖:執行和鏈接兩種視圖。
上面這圖大家應該都很熟悉了吧,后面兩種目標文件,可重定位目標文件和可執行目標文件就分別對應著ELF格式文件的鏈接視圖和執行視圖。
細究ELF文件的話,內容還是很多的,我們在這兒撿重點,exec用的上的說:
先來從總體上看看兩種視圖的結構:
鏈接視圖以節為單位,執行視圖以段為單位。這里的段和我們所說的內存分段的段的含義是不同的,要區分開。
實際的ELF文件里面的節和段很多,這里只是列出了比較重要需要了解的一部分,下面簡要說明一下:
.text:代碼部分
.rodata: 只讀的數據,例如 printf 中的格式串,switch-case 中的跳轉表
.data:已初始化的全局變量
.bss:未初始化的全局變量,局部靜態變量
.symtab:symbol table,符號表,程序里面的全局變量名和函數名都屬于符號,這些符號信息保存到符號表
.rel.text,.rel.data:與可重定位相關的信息
.debug,調試所用的符號表
.init,包含可執行的指令,進程初始化代碼的一部分,要在執行main函數之前執行這些代碼
各元素表示的意思大都已經說明,根據命名應該還是很好記住各元素所代表的意義,下面再重點說幾點:
e_ident前4位是固定的魔數,e_ident[0] = 0x7f,e_ident[1] = 'E', e_ident[2] = 'L', e_ident[3] = 'F',表明這是一個 ELF 文件
e_ident[5]用來指定大端還是小端字節序,1表小端,2表大端,0表非法編碼格式
e_type,ELF 目標文件類型,如可重定位,可執行,動態共享目標文件
e_entry,這個可執行文件的入口地址,exec 加載完程序之后就從這兒開始運行
同上簡單解釋幾點:
程序段的類型有很多,我們只需要了解可裝載段,顧名思義,需要裝載到內存里面的段,比如代碼段,數據段。
這里涉及了多種段,程序段類型里面的段,數據段代碼段等里面的段,還有內存的分段,都是段不要混淆了。
一般說來 p_filesz <= p_memsz,這是因為bss節的存在,它并不存在與文件中,僅存在與運行時的內存當中。這是因為 bss 節中存放的是未初始化的全局變量,它們的值是無意義的,如果我們在文件中分配空間將這些變量的值存儲下來也就無意義。所以我們的目標文件中其實并不需要 bss 的實體,只需要記錄bss 的大小位置等相關信息即可。
雖然在文件中存儲變量沒有意義,但是人家好歹也是未初始化的全局變量,需要在內存中專門為它們開辟空間存儲它們。
從上面的 ELF Header 和 Program Header 中可以看出程序各段的大小位置都已經確定好了了,我們只需要將它們加載到相應位置即可,來看看 exec 變身術的殘卷秘籍:
同 fork 那本秘籍,主要是想展現 exec 實現的一個大致過程思路,每個步驟寫的應該還是比較清晰,照例下面說幾點重點:
裝載映射的段都是可裝載段,具體的裝載過程可以用讀取文件 read 和 lseek 系統調用來實現。
read 的作用就是讀取文件到內存的一個緩沖區,而 read,lseek 兩函數需要的參數在程序頭中都有記錄,所以理論上來講實現起來應該是很容易的。
exec 需要修改原進程內核棧中的一些信息,最主要的就是將中斷上下文里面的 eip 改為 ELF 文件中的入口地址。
ELF頭中的 p_entry 入口地址是什么?是main函數的地址嗎?非也。那是什么呢?這要牽扯一個概念,運行庫,運行庫涉及的知識很多,在這就長話短說,講講與本文有關的。
簡單說來,運行庫就是標準庫的擴展,會在 main 函數運行之前準備好環境,運行完之后再進行收尾的工作。
本文就只說說準備運行環境的部分,這部分可以看做是一個函數,全局符號為_start,也就是函數名為_start。_start才是我們運行的第一個函數,ELF 頭中的入口地址 p_entry 就是它。
_start 函數的工作之一就是壓入 main 函數的參數。
前面我們的偽碼中是把實際的命令行參數傳到了用戶態的線性空間中,但是要清楚 main 函數的參數可不是實際的命令行參數,而是命令行參數的個數和字符串指針數組的地址。這兩個參數壓棧操作就在_start 中進行。畢竟 main 函數也是一個被調用的函數,在調用之前需要傳參。
exec函數如果發生錯誤會返回-1,正確則不返回。
exec 函數里面還調用了許多其他函數,這些函數出錯,exec 沒能繼續運行下去的話是會直接返回-1的,只是上面偽碼沒體現出來。
要知道 exec 這個函數就像是推到原進程然后重來,改變了很多信息,中斷的執行上下文被大幅度改變,調用 exec 的代碼也是不復存在的。從這個角度看exec從未成功返回,取而代之的是執行的新程序被映射大進程的地址空間。
以上來自深入理解 Linux 內核的解釋,感覺聽抽象模糊?也可以嘗試這樣理解,來自于操作系統真相還原的一個系統設計。
在這個OS設計里,exec 如果成功運行到最后,直接使用 jmp 語句跳到中斷退出點。jmp 語句不像 call,它是有去無回的,所以沒有不會再返回到 exec 函數里,而是直接彈出中斷上下文的 eip 入口地址去運行新程序了。
再來看看exec的動態圖,想要表達的意思很簡單,就是在原進程上推到重來:
(抱歉放不了動圖,可去我的公號查看)
好了,關于分身術和變身術咱們就傳授到這,下面我們要來學以致用,推陳出新,創造一門新忍術。
前面說過,影分身之術雖然等級很高但是有弊端的,特別是在施展多重影分身之術時可能會因為查克拉消耗太過劇烈而傷及自身,所以被列為禁術。而同樣的,咱們最初版的 fork 因為復制了父進程的全部資源而浪費了太多時間空間,也不再使用。
現在的 fork 都是用了寫時復制技術,這項技術可了不得,面試中經常提到。岸本齊史肯定不會這個,不然的話肯定再創一門 S級忍術,沒有實體的假分身可以變成真的,真的可以變成假的。真真假假,虛虛實實,補不足而損有余,這樣就可以減少不必要的查克拉消耗,還能達到兵者詭道也的效果。
扯遠了扯遠了,寫時復制這項技術可沒有那么強大,但也有類似的機制和目的,減少空間時間消耗,高效的完成進程創建任務,來具體看看:
前面我們的fork是傻瓜性的,真的將父進程的所有資源全部復制了一份,但實際上是不必要的。
如果我們不調用 exec 運行新程序,那么實際上父子倆進程很多的資源是可以共用的,比如代碼部分。
而如果調用 exec 來執行新程序,exec 要刪除掉已存在的用戶區域,復制父進程的資源也無意義。所以這樣的fork有很大弊端,不適用。
顧名思義的簡單解釋就是,fork 時不會真的分配新的物理頁復制資源,子進程直接引用共享父進程的物理空間。只有一個進程要寫數據,改變共享內容時,才單獨復制一份出來。
這樣就避免了不必要的資源復制。在面試中肯定不能只回答這么一點內容,那是過不了關的,咱們還需細剖注意幾個問題,就直接已干貨的形式羅列出來了,如下所示。
即使利用寫時復制技術,fork時也還是為子進程創建一些單獨的資源,比如PCB,頁表。也就是說要為其分配新的物理空間來存儲這些資源,這些東西是不會在物理上共享的。
父子進程各自擁有一套頁表,子進程的頁表從父進程哪兒復制過來的,內容是相同,所以父子進程映射到了同一個物理空間。但因為是兩套頁表,所以父子進程的虛擬地址空間是不同的,只是說兩個虛擬地址空間對應的是同一個物理空間。或者按照CSAPP里面的話來說,相同但獨立的地址空間。表述的可能不太一樣,但實際表達的意思是一樣的,能清楚明白是指就好。
寫時復制的實現原理:
將兩個進程的頁面標記為只讀,這是通過設置頁表項里面的存取權限位來實現的。
將兩個進程的區域結構都標記為私有的寫時復制,這是通過設置進程的vm_area_struct結構體里面的vm_flags字段來實現的
如果父子進程都是讀取相同的物理頁,那么父子之間是相安無事的。但是只要有一個寫就會起沖突,內核就會把這個頁的內容拷貝到一個新分配的物理頁,并更新寫進程的頁表項使其指向新分配的這個物理頁。最后再回復頁面的可寫屬性。
再來看看動圖直觀感受一下:
(抱歉放不了動圖,可去我的公號查看)
所以啊,fork之后,你以為現在是父子兩個進程實體,但實際上內存里面只有父進程一個完整的實體。你又以為子進程在內存里面沒多少自己單獨的資源時,過了一會兒,說不定又因為寫操作給分配了。
所以吧,真不是我生搬硬套,這虛虛實實的感覺與我那創造的S級忍術還是有些相似的對吧。到現在這個S級術法還沒取名字呢,為了紀念寫時復制技術,而且這個術法這么厲害,干脆就叫做牛影分身吧。(為啥叫這名兒能懂吧,沒看明白的看看寫時復制的英文簡寫?)
最后這一部分簡單談談上述三個函數的區別,同樣的不多說直接以干貨的形式羅列出來:
vfork就是為了避免fork時的大量無用復制而設計的。
vfork創建進程時連父進程的頁表都不會復制,完全使用父進程的資源,運行在父進程的地址空間中。子進程對數據的任何修改也就是對父進程的數據修改。
vfork會保證子進程先運行,而fork不會,要看調度情況。
父進程則一直被阻塞,直到子進程調用exec有了自己的地址空間或者退出時,父進程才會被重新調度。
由上可以看出vfork的系統開銷很小,似乎很有競爭力,但是由于現在的fork采用了寫時復制技術,相比之下vfork的競爭力也不是那么強了,所以現在已經漸漸淡出內核
clone這個函數功能很齊全,參數也多,使用其他比較復雜。我們可以使用不同的參數組合來選擇性的復制父進程的資源。
傳統的fork函數還有vfork函數就是依據clone來實現的。
clone函數的主要用處還是來創建線程,也就是輕量級進程。
關于這部分就先說這么多吧,了解了解即可,最常用的還是fork函數。
到此,關于“如何使用分身術fork和變身術exec創建新進程”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注創新互聯網站,小編會繼續努力為大家帶來更多實用的文章!