Linux 系統調用 —— fork 內核源碼剖析

系統調用流程簡述

  • fork() 函數是系統調用對應的 API,這個系統調用會觸發一個int 0x80 的中斷;
    當用戶態進程調用 fork() 時,先將 eax(寄存器) 的值置爲 2(即 __NR_fork 系統調用號);
     
  • 執行 int $0x80,cpu 進入內核態;
     
  • 執行 SAVE_ALL,保存全部寄存器到當前進程內核棧中;
     
    node

  • 進入 sys_call,將 eax 的值壓棧,根據系統調用號查找 system_call_table ,調用對應的函數;
     
  • 函數返回,執行 RESTORE_ALL,恢復保存的寄存器;執行 iret,cpu 切換至用戶態;
     
    數據結構

  • 從 eax 中取出返回值,fork() 返回;函數

詳見:系統調用的工做機制atom

 
 
 

fork 在內核中作了什麼

sys_call

當咱們調用 fork()、clone()、vfork() 時,實際上在內核中調用的都是同一個函數 —— do_fork()操作系統

do_fork()

這裏的三個系統調用的區別就在於調用 do_fork() 時傳入的參數不一樣線程

do_fork() 中第一個參數 clone_flags 是一個 32bit 的標誌,其中不一樣的 bit 置 1 表明不一樣的選項,表示新的子進程與父進程之間共享哪些資源3d

clone_flags

其中 sys_fork() 調用 do_fork() 只設置了 SIGCHLD 選項,sys_vfork() 設置了 CLONE_VM | CLONE_VFORK | SIGCHLD 選項,而 sys_clone() 的參數來自上層,經過 ebx 傳入;指針

下面簡述下 do_fork() 的執行過程code

do_fork()

  • 查找 pidmap_array 位圖,爲子進程分配新的 pid;
  • 調用 copy_process() ,將新的 pid 傳入參數,這個函數是建立進程的關鍵步驟,該函數返回新的 task_struct 地址;

copy_process()

copy_process()

  • 建立 task_struct 結構體指針;
  • 檢查參數;
  • 調用 dup_task_struct() ,將父進程 task_struct 傳入參數,爲子進程獲取進程描述符;

dup_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 結構體和內核棧)
    stack

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()orm

  • 檢查當前用戶所擁有的進程數是否超過了限制的值(1024),有root權限除外;若超過了限制則返回錯誤碼,不然增長用戶擁有的進程計數;
atomic_inc(p->user->process);
  • 檢查系統中的進程數量是否超過了 max_threads;
    max_threads的數量由系統內存容量決定,全部的thread_info描述符和內核棧所佔用空間不能超過系統內存的1/8;

  • 拷貝全部的進程信息:

copy all process information

  • 其中最重要的是 copy_mm() ,該函數經過創建新進程全部頁表和內存描述符來建立進程地址空間;
    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()

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 在建立子進程時,主要作了這些工做

  1. 爲子進程分配新的 pid,並經過父進程 PCB(task_struct)建立新的子進程 PCB
  2. 檢查進程數是否達到上限(分別檢查用戶限制和系統限制)
  3. 拷貝全部的進程信息(打開的文件 / 信號處理 / 進程地址空間等),這裏須要拷貝的選項由調用 do_fork() 時傳入的參數 clone_flags 決定
  4. 用父進程的內核棧初始化子進程的內核棧,設置子進程的返回值爲 0(eax = 0)
  5. 設置新進程的狀態(TASK_RUNNING / TASK_STOPPED),調整父子進程調度
  6. 父進程 fork 返回子進程的 pid
相關文章
相關標籤/搜索