系統調用流程簡述
-
fork() 函數是系統調用對應的 API,這個系統調用會觸發一個int 0x80 的中斷;
當用戶態進程調用 fork() 時,先將 eax(寄存器) 的值置為 2(即 __NR_fork 系統調用號);
-
執行 int $0x80,cpu 進入內核態;
-
執行 SAVE_ALL,保存所有寄存器到當前進程內核棧中;
-
進入 sys_call,將 eax 的值壓棧,根據系統調用號查找 system_call_table ,調用對應的函數;
-
函數返回,執行 RESTORE_ALL,恢復保存的寄存器;執行 iret,cpu 切換至用戶態;
-
從 eax 中取出返回值,fork() 返回;
詳見:系統調用的工作機制
fork 在內核中做了什么
當我們調用 fork()、clone()、vfork() 時,實際上在內核中調用的都是同一個函數 —— do_fork()
這里的三個系統調用的區別就在於調用 do_fork() 時傳入的參數不同
do_fork() 中第一個參數 clone_flags 是一個 32bit 的標志,其中不同的 bit 置 1 代表不同的選項,表示新的子進程與父進程之間共享哪些資源
其中 sys_fork() 調用 do_fork() 只設置了 SIGCHLD 選項,sys_vfork() 設置了 CLONE_VM | CLONE_VFORK | SIGCHLD 選項,而 sys_clone() 的參數來自上層,通過 ebx 傳入;
下面簡述下 do_fork() 的執行過程
do_fork()
- 查找 pidmap_array 位圖,為子進程分配新的 pid;
- 調用 copy_process() ,將新的 pid 傳入參數,這個函數是創建進程的關鍵步驟,該函數返回新的 task_struct 地址;
copy_process()
- 創建 task_struct 結構體指針;
- 檢查參數;
- 調用 dup_task_struct() ,將父進程 task_struct 傳入參數,為子進程獲取進程描述符;
dup_task_struct()
- 創建 task_struct 、thread_info 結構體指針;
struct task_struct *tsk;
struct thread_info *ti;
- 調用 alloc_task_struct() 宏為新進程獲取進程描述符,並保存至 tsk 中;
tsk = alloc_task_struct();
if (!tsk)
return NULL;
- 調用 alloc_thread_info() 宏獲取一塊空閑內存區,保存在 ti 中(這塊內存的大小為 8K/4k,用來存放新進程的 thread_info 結構體和內核棧)
struct task_struct
{
struct thread_info * thread_info; // 指向 thread_info 的指針
struct mm_struct * mm; // 進程地址空間
pid_t pid;
struct list_head children; // 子進程鏈表
...
}
struct thread_info
{
struct task_struct task; // 指向 task_struct 的指針
_u32 cpu; // 當前所在的cpu
mm_segment_t addr_limit; // 線程地址空間
// user_thread 0-0xBFFFFFFF
// kernel_thread 0-0xFFFFFFFF
...
}
// thread_info 和 stack 共享一塊內存
union thread_union
{
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
- 復制進程描述符和 thread_info,並將兩者中的互指指針初始化;
*ti = *current->thread_info;
ti->task = tsk;
- 將新進程描述符的使用計數器 usage 置為2,表示描述符正在被使用而其對應的進程處於活動狀態;
新進程的進程描述符創建完成,返回至 copy_process()
- 檢查當前用戶所擁有的進程數是否超過了限制的值(1024),有root權限除外;若超過了限制則返回錯誤碼,否則增加用戶擁有的進程計數;
atomic_inc(p->user->process);
-
檢查系統中的進程數量是否超過了 max_threads;
max_threads的數量由系統內存容量決定,所有的thread_info描述符和內核棧所占用空間不能超過系統內存的1/8; -
拷貝所有的進程信息:
- 其中最重要的是 copy_mm() ,該函數通過建立新進程所有頁表和內存描述符來創建進程地址空間;
struct mm_struct
{
struct vm_area_struct * mmap; // 指向線性區對象的鏈表頭
struct rb_root mm_rb; // 指向線性區對象的紅黑樹的根
pgd_t * pgd; // 指向頁全局目錄
atomic_t mm_users; // 次使用計數器,存放共享 mm_struct 數據結構輕量級進程的個數
atomic_t mm_count; // 主使用計數器,每當 mm_count 遞減,內核就要檢查它是否為0,如果是就要解除這個內存描述符
}
copy_mm()
- 創建 mm_struct * mm, oldmm 結構體指針(內存描述符);
oldmm = current->mm; //oldmm 初始化為父進程的 mm_struct
- 檢查 clone_flags 是否設置了 CLONE_VM 位;
若設置了 CLONE_VM 位,則表示創建線程,與父進程共享地址空間
atomic_inc(&oldmm->mm_users); // 父進程的地址空間引用計數加一
mm = oldmm; // 將父進程地址空間賦給子進程
- 否則,就要創建新的地址空間,並從當前進程復制 mm 的內容
mm = allocate_mm();
memcpy(mm, oldmm, sizeof(*mm));
- 調用 dup_mmap() 復制父進程的線性區和頁表
dup_mmap()
- 復制父進程每個 vm_area_struct 線性區描述符,插入到子進程的線性區鏈表和紅黑樹中;
struct vm_area_struct
{
struct mm_struct * vm_mm; // 指向線性區所在的內存描述符
unsigned long vm_start; // 當前線性區起始地址
unsigned long vm_end; // 線性區尾地址
struct vm_area_struct * vm_next; // 下一個線性區
pgprot_t vm_page_prot; // 線性區訪問權限
struct rb_node vm_rb; // 用於紅黑樹搜索的節點
}
- 用 copy_page_range() 創建新的頁表,在新的 vm_area_struct 中鏈接並復制父進程的頁表條目;
copy_page_range()
- 創建新的頁表;
- 復制父進程的頁表來初始化子進程的新頁表;
私有/可寫的頁( VM_SHARED 標志關閉/ VM_MAYWRITE 標志打開)所對應的權限父子進程都設為只讀,以便於 Copy-on-write 機制處理。
新進程的線性區和頁表復制完成,返回至copy_process()
- 調用 copy_thread() 用父進程的內核棧初始化子進程的內核棧
copy_thread()
- 將eax的值強制設為0(fork / clone 系統調用的返回值)
childregs->eax = 0;
sched_fork()
- 調用 sched_fork() 完成對新進程調度程序數據結構的初始化,將新進程狀態設為 TASK_RUNNING
- 為了公平起見,父子進程共享父進程的時間片
進程創建完成,返回至 do_fork()
- 如果設置 CLONE_STOPPED,就將子進程設置 TASK_STOPPED 狀態並掛起;
否則調用 wake_up_new_task() 調整父子進程的調度參數;
wake_up_new_task()
- 如果父子進程運行在同一個 cpu 上,並且不能共享同一組頁表 (CLONE_VM 位為 0),就把子進程插入運行隊列中的父進程之前;
如果子進程創建之后調用 exec 執行新程序,就可以避免寫時拷貝機制執行不必要的頁面復制;
否則,如果運行在不同的cpu上,或父子共享同一組頁表,就將子進程插入運行隊列的隊尾。
返回至 do_fork()
- 返回子進程的 pid
2017/8/3 補充
-
fork() 和 vfork() 參數是寫死的,而 clone() 是可選的,它可以選擇當前創建的進程哪些部分是共享的,哪些部分是獨立的;
-
vfork() 是歷史的產物,當調用 fork() 的時候,需要將父進程的線性區和頁表都拷貝一份,而調用 exec() 執行新程序后,又要把所有頁表刪除重置新的頁表,建立映射關系,效率很低;
-
所以要有 vfork(),vfork() 的 clone_flags 位置了 CLONE_VM ,表示共享父進程的地址空間,vfork() 中創建的進程沒有分配自己的地址空間,而是通過一個 mm_struct 指針指向父進程的地址空間,這個進程是為了在之后調用 exec() 執行新的程序;
-
而在有了 Copy-on-write 技術后,fork() 出的子進程只創建了自己的地址空間,然后用父進程的地址空間初始化,每個頁表的項置為父進程的頁表項,共享父進程的物理頁面,並將所有 私有/可寫 頁面改為只讀;
-
當我們改變父子進程的數據后,cpu 在運行過程中會發生一個缺頁錯誤,cpu 轉交控制權給操作系統,操作系統查找 VMA 發現該頁權限為只讀,但所在段又是可寫的,產生一個矛盾,這就是識別 Copy-on-write 的方法,接着 OS 給子進程分配一個新的物理頁,並將頁表該頁的地址修改成新的物理頁地址;
-
這樣 fork() 后再調用 exec() 就不用那么麻煩了,可以直接將新的物理頁與子進程的虛擬空間建立映射
小結
綜上,fork 在創建子進程時,主要做了這些工作
- 為子進程分配新的 pid,並通過父進程 PCB(task_struct)創建新的子進程 PCB
- 檢查進程數是否達到上限(分別檢查用戶限制和系統限制)
- 拷貝所有的進程信息(打開的文件 / 信號處理 / 進程地址空間等),這里需要拷貝的選項由調用 do_fork() 時傳入的參數 clone_flags 決定
- 用父進程的內核棧初始化子進程的內核棧,設置子進程的返回值為 0(eax = 0)
- 設置新進程的狀態(TASK_RUNNING / TASK_STOPPED),調整父子進程調度
- 父進程 fork 返回子進程的 pid