fork原理--Linux實現

fork的一些特徵是怎麼實現的?
比如:
1. 爲什麼父進程返回子進程pid, 子進程返回0?
2. 子進程是如何做到與父進程“一模一樣的”?
3. 子進程同父進程一樣,都是從調用fork處繼續向下執行,而不是子進程從頭執行?

下面直接看源碼來分析。
注:如果不額外說明,爲了閱讀方便,以下展示源碼都已刪減。

從父進程調用fork開始; 就不寫程序了。
大家都知道,這是一個系統調用,當父進程調用fork()時,大體過程如下:
fork() -> int 0x80 -> 調用system_call這個彙編函數 --> 調用其對應的內核函數sys_fork();
(詳細的系統調用過程可以去另一篇文章瞭解一下)
爲什麼要列出這個過程呢,因爲這個過程反映了壓棧的順序,這裏先不詳述,下面會用到這個調用過程。

好了,現在我們知道,最終是調用了sys_fork, 看一下它的實現,這是一個彙編函數:
在這裏插入圖片描述
kernel/syscall.s

其實sys_fork的實現就分兩個步驟(兩個C函數):

  1. find_empty_process
  2. copy_process

其實從名字上也能看出來這兩個函數要幹嘛了,我們來詳細看一下:
find_empty_process:
在這裏插入圖片描述
kernel/fork.c

Linux把所有的進程(的指針)放在task這個數組裏,所以只需要一個下標就可以表示一個進程了,find_empty_process就是在task數組中找一個空位置,返回這個位置的下標(作爲子進程的pid),失敗返回負數。
注意:C函數的返回值存放在eax寄存器中,也就是說find_empty_process調用完成後,返回值(子進程pid或負數)放在eax中,所以sys_fork那個彙編函數中
在call find_empty_process後,
通過testl eax, eax 判斷find_empty_process的返回值(eax)是否爲負數,是負數則返回(沒有找到空閒位置,子進程沒地方放,自然沒必要進行下面的複製操作)。
如果find_empty_process找到了一個空閒位置(子進程pid),則進行壓棧操作,調用copy_process進行復制操作(這是fork函數的核心)。
在這裏插入圖片描述
kernel/fork.c

首先看參數,這輩子頭一次見這麼長的參數列表,但是這些參數是什麼時候賦值或者說壓棧的呢?
這些參數實際上包含了從中斷開始所有的相關壓棧操作,也就是說從中斷(int 0x80發生那一該)就已經在爲這個函數的參數作準備了。

文章開始的調用順序應該沒忘了,就是它們爲copy_process一個個壓入參數。
從int 0x80: 發生中斷自動壓棧:ip, cs, eflags, sp, ss
然後在system_call:
在這裏插入圖片描述在這裏插入圖片描述
kernel/syscall.s

一直到sys_fork在call coyp_process之前的幾個push, 正好和copy_process的參數是一一對應的。

我們這裏只關注重要的參數:nr, ip.
nr是第一個參數,肯定是call copy_process之前最後一條壓棧指令壓入的(參數從右向左壓棧),對應push eax, 也就是find_empty_process的返回值(task數組的空閒位置)。
ip是發生中斷時壓入的,int 0x80的下一條指令的地址,也就是中斷返回後的地址。
這裏不要認爲是中斷處理程序的地址。
在這裏插入圖片描述
這裏壓入的是instructor1, 而不是system_call, 所以下面把這個ip賦值給子進程ip後,子進程再執行,就是從instrutor1位置(可以認爲是fork()後一條指令了,因爲fork()幾乎 就觸發了一條int 0x80指令)
在這裏插入圖片描述
int 0x80壓入的就是fork()後一條指令地址(這就是爲什麼子進程會和父進程一樣從fork()之後繼續執行)。

我們進入copy_process的函數體,看它是如何複製進程的。

在這裏插入圖片描述
上面代碼再粘一遍。

123行申請一頁內存,這是爲子進程的pcb申請的空間;
125行,task[nr] = p, 直接把子進程放入調度隊列,子進程信息還沒有構建,直接調度不會有問題嗎?
當然不會,進入中斷時會關閉中斷,沒有時鐘中斷,當然不會引起調度,可以放心向下執行下面的操作。
127行,*p = *current, p是指向子進程的pcb, current是指向當前進程(也就是父進程:調用fork的進程)的pcb, 這句是直接把父進程的pcb複製給子進程。
然後下面的大部分代碼都是對子進程pcb進行修改的(父子進程pcb不可能完全一樣,pid, 棧等肯定不一樣)。
129行設置子進程pid
130行設置子進程的內核棧,指向pcb頁末。(進程初始化都會這麼設置)
在這裏插入圖片描述
然後設置ip。
134行設置 eax值爲0, 前邊講彙編函數時講到了函數返回值是放在eax中的,現在把子進程eax的值置爲0, 將來eax被調度,取出eax後就是子進程fork的返回值(0)。

138行調用copy_mem主要是複製頁表(此版本所有進程共享頁目錄表,所以只需要複製頁表)。
在這裏插入圖片描述
71行設置ldt[1], 這是代碼段(子進程的)。
72行設置ldt[2], 這是數據段。
然後調用copy_page_table()複製頁表。由於本文不是講解內存管理,所以對這個函數不做詳細介紹。
這裏只需要知道copy_mem只複製了頁表!!!
然後copy_process最後也只是更新了打開文件數(全都+1),設置tss,ldt,運行狀態。就結束了。也就是說整個fork結束了,那麼只複製了頁表,父子進程不會相互影響嗎?答案是肯定的。
fork只需要頁表,而不復制所有物理內存頁面,父子進程共享!!!。
在這裏插入圖片描述
copy_page_table非常複雜,只需要關注一下177行,this_page &= ~2, 這行的意思是把父進程的物理內存置爲只讀(此時父子進程共享),那麼什麼時候真正進行復制這些物理內存頁面呢?
寫的時候。是的,這就是LInux的寫時複製機制。(見文章

本文主要是筆記加入個人理解,如有錯誤,還望指正。


參考:《Linux 內核完全剖析》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章