fork建立程序過程(底層實現) 和 寫實拷貝

2021-08-09 14:37:18 字數 3708 閱讀 4526

現在我們來總結一下fork的整個處理流程。從c語言中的函式開始,它在glibc庫中會被轉換為int0x80加呼叫號的形式,觸發中斷。該中斷在系統初始化過程中註冊,它的處理函式是system_call,這個函式在system_call.s檔案中,在這裡面它首先壓棧一些引數,然後會根據呼叫號呼叫sys_call_table的相應表項,sys_call_table定義在include/linux/sys.h中,它是乙個函式指標陣列。現在對應的就是sys_fork函式,它仍然是在system_call.s中定義的。我們來看它的處理過程,首先它會呼叫find_empty_process函式來從task陣列中查詢乙個還沒有使用的task,

找到之後把對應的索引返回(儲存到eax中),隨後它又儲存了其他一些暫存器,並且把返回的任務索引壓入棧中,依次作為引數來呼叫copy_process函式。下面我們來看copy_process的處理過程。首先,它會分配乙個記憶體頁,把task_struct結構體儲存到這個記憶體頁的最開始位置(同時需要註冊進task陣列),並把核心棧指標設為該頁的頂端。緊接著為這個新的task_struct複製為之前的task_struct,然後需要修改一些域,這些域都是不能直接從父程序繼承的。這些域包括程序id設定為我們在find_empty_process過程中找到的id,pid設定為呼叫fork的程序id,leader不能繼承父類的,時鐘和訊號也不繼承,設定eip,設定eax為0,設定核心棧指標等等。然後比較重要的現在需要為它設定頁表了。我們再詳細總結一下設定頁表的過程。

為了設定頁表,我們需要知道乙個程序的**段和資料段的起始位址以及占用了多大的空間。由於二者是重疊的,我們設定乙個就可以了。獲取的過程是通過檢視程序ldt表項來獲取基址和限長。由於linux0.11中程序的起始位址為64m×nr,所以前面的基址其實並沒有太大作用,知道了起始位址,現在就可以知道它在頁目錄中的索引了,我們設定它的頁目錄項,現在可以設定它的頁表了,首先獲取父程序的頁表,並對頁表進行拷貝,拷貝的過程中,我們把子程序的頁面屬性設定為唯讀(父程序也被設為唯讀了,核心空間除外)。這樣就完成了頁表的設定,由於是唯讀的,當子程序執行寫操作時會觸發寫保護,在寫保護處理中會拷貝頁面。現在頁表就已經設定好了。接下來該為它設定gdt中的專案了。還需要提一點,之前我們根據程序號nr計算出**段和資料段起始位址,並把它設定到了task_struct的ldt中。現在我們再次根據nr計算出ldt和tss在gdt中的位置,並向其中註冊ldt和tss,通過這個過程我們就把task_struct中的tss和ldt的位址註冊到gdt中對應的表項處。到此,所有的工作都已經完成了,設定其狀態為可執行狀態(剛開始是被設定為不可中斷狀態),返回新建程序號。

1. fork

linux系統中提供了三個系統呼叫可以建立新程序:clone()、fork()、vfork()。實際上,不管是我們比較熟悉的fork()還是剩下的兩個在linux中都是通過clone()實現的。clone()是在c語言庫中定義的乙個封裝函式,它負責建立程序堆疊並且呼叫對程式設計師隱藏的clone()系統呼叫。

進一步觀察發現,linux核心中又是用do_fork()來處理這三個系統呼叫的。

新的程序通過複製父程序而建立。為了建立新程序,首先在系統的物理記憶體中為新程序建立乙個 task_struct 結構,將舊程序的 task_struct 結構內容複製到其中,再修改部分資料。接著,為新程序分配新的堆疊,分配新的程序識別符號 pid。然後,將這個新 task_struct 結構的位址填到 task 陣列中,並調整程序鏈關係,插入執行佇列中。於是,這個新程序便可以在下次排程時被選擇執行。此時,由於父程序的程序上下文 tss 結構複製到了子程序的 tss 結構中,通過改變其中的部分資料,便可以使子程序的執行效果與父程序一致,都是從系統呼叫中退出,而且子程序將得到與父程序不同的返回值(返回父程序的是子程序的 pid,而返回子程序的是 0)

fork()底層流程圖如下:

然後來看看do_fork的具體過程:

p = copy_process(clone_flags, stack_start, regs, stack_size,

child_tidptr, null, trace);

wake_up_new_task(p, clone_flags);

第一步是呼叫copy_process函式來複製乙個程序,並對相應的標誌位等進行設定,接下來,如果copy_process呼叫成功的話,那麼系統會有意讓新開闢的程序執行,這是因為子程序一般都會馬上呼叫exec()函式來執行其他的任務,這樣就可以避免寫是複製造成的開銷,或者從另乙個角度說,如果其首先執行父程序,而父程序在執行的過程中,可能會向位址空間中寫入資料,那麼這個時候,系統就會為子程序拷貝父程序原來的資料,而當子程序呼叫的時候,其緊接著執行拉exec()操作,那麼此時,系統又會為子程序拷貝新的資料,這樣的話,相比優先執行子程式,就進行了一次「多餘」的拷貝。

從上面的分析中可以看出,do_fork()的實現,主要是靠copy_process()完成的,這就是一環套一環,所以在看核心的時候,會覺得一下子跳到這,一下子又跳到那,一下子就看暈了的乙個很大的原因。不過我覺得這也是linux的一大好處,因為其提高了函式的可重用行,比如本文一開始提到的幾個函式的實現,歸根到底,都是通過do_fork()實現的。

接著再來看看copy_process()的實現:

p = dup_task_struct(current); 為新程序建立乙個核心棧、thread_iofo和task_struct,這裡完全copy父程序的內容,所以到目前為止,父程序和子程序是沒有任何區別的。

檢查所有的程序數目是否已經超出了系統規定的最大程序數,如果沒有的話,那麼就開始設定程序描訴符中的初始值,從這開始,父程序和子程序就開始區別開了。

設定子程序的狀態為不可被task_uninterruptible,從而保證這個程序現在不能被投入執行,因為還有很多的標誌位、資料等沒有被設定。

複製標誌位(falgs成員)以及許可權位(pe_superpriv)和其他的一些標誌。

呼叫get_pid()給子程序獲取乙個有效的並且是唯一的程序識別符號pid。

根據傳入的cloning flags(具體表示上面有)對相應的內容進行copy。比如說開啟的檔案符號、訊號等。

父子程序平分父程序剩餘的時間片。

return p;返回乙個指向子程序的指標。

至此,do_fork的工作就基本結束了

寫時拷貝

技術:傳統的fork()系統呼叫直接把所有的資源複製給新建立的程序。這種實現過於簡單並且效率低下,因為它拷貝的資料也許並不共享,更糟的情況是,如果新程序打算立即執行乙個新的映像,那麼所有的拷貝都將前功盡棄。

linux

的fork()

使用寫時拷貝(

copy-on-write

)頁實現。寫時拷貝是一種可以推遲甚至免除拷貝資料的技術。核心此時並不複製整個程序位址空間,而是讓父程序和子程序共享同乙個拷貝。只有在需要寫入的時候,資料才會被複製,從而使各個程序擁有各自的拷貝。也就是說,

資源的複製只有在需要寫入的時候才進行

,在此之前,只是以唯讀方式共享。這種技術使位址空間上的頁的拷貝被推遲到實際發生寫入的時候。在

頁根本不會被寫入的情況下—舉例來說,

fork()

後立即呼叫

exec()—

它們就無需複製了。

fork()

的實際開銷就是複製父程序的頁表以及給子程序建立惟一的程序描述符。

在一般情況下,程序建立後都會馬上執行乙個可執行的檔案,這種優化可以避免拷貝大量根本就不會被使用的資料(位址空間裡常常包含數十兆的資料)。由於unix強調程序快速執行的能力,所以這個優化是很重要的。

程序建立fork 和vfork

乙個現有的程序可以通過兩種方式建立乙個新的程序,下面詳細介紹兩種fork vfork 函式原型 man 手冊 include pid t fork void 描述 fork 以當前的程序為副本建立乙個新的程序,新建立的程序被稱為子程序,當前的程序被稱為父程序,父程序和子程序執行在各自的位址空間。返回...

fork程序建立

fork建立子程序,fork函式返回兩個值,當為0時,則認為是子程序 塊執行區域,而不為0則是父程序 塊執行區域。我們需要知道的是,fork子程序可以與父程序共享部分程序上下文,而與此不同的是execl函式,一旦開始執行到execl函式時,啟動被呼叫的函式,後面的 則不再執行,而是直接執行呼叫的程式...

fork建立程序

1.程序的建立 fork 函式是建立子程序的函式,在主函式中呼叫fork會產生乙個子程序 列印出來的結果是 if語句是條件語句但卻兩個都列印了,兩個條件都滿足,也就是說兩個都執行,但卻不是一條執行流,那麼可以得出肯定還有乙個程序在列印另乙個。都記得fork 之後有兩個程序,乙個父程序,乙個子程序,父...