進程的建立 —— do_fork()函數詳解

來源:互聯網
上載者:User

在講進程管理專題最核心的課題——進程建立之前,我們先簡單地回顧一下上一篇博文的sys_clone()系統調用:
asmlinkage int sys_clone(struct pt_regs regs)
{
    unsigned long clone_flags;
    unsigned long newsp;
    int __user *parent_tidptr, *child_tidptr;

    clone_flags = regs.ebx;
    newsp = regs.ecx;
    parent_tidptr = (int __user *)regs.edx;
    child_tidptr = (int __user *)regs.edi;
    if (!newsp)
        newsp = regs.esp;
    return do_fork(clone_flags, newsp, &regs, 0, parent_tidptr, child_tidptr);
}

那麼,在同一個檔案中(針對80x86平台是/arch/i386/kernel/Process.c),有:
asmlinkage int sys_fork(struct pt_regs regs)
{
    return do_fork(SIGCHLD, regs.esp, &regs, 0, NULL, NULL);
}

asmlinkage int sys_vfork(struct pt_regs regs)
{
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, &regs, 0, NULL, NULL);
}

我們看到,不管是clone、fork還是vfork系統調用,他們的實現函數sys_clone、sys_fork和sys_vfork都指向了位於/kernel/Fork.c中的do_fork函數,唯一的不同就是clone_flags的不同,具體含義請參考前一博文。

這裡,我們就來詳細分析進程建立的實務函數 —— do_fork()

long do_fork(unsigned long clone_flags,
          unsigned long stack_start,
          struct pt_regs *regs,
          unsigned long stack_size,
          int __user *parent_tidptr,
          int __user *child_tidptr)
先介紹一下它執行時使用的參數:
clone_flags:與clone()參數flags相同。
stack_start:與clone()參數stack_start相同。
regs:指向核心態堆棧通用寄存器值的指標,通用寄存器的值是在從使用者態切換到核心態時被儲存到核心態堆棧中的。
stack_size:未使用,總被設定為0。
parent_tidptr,child_tidptr:與clone系統調用中對應參數ptid和ctid相同。

do_fork()函數利用輔助函數copy_process()來建立進程描述符以及子進程所需要的其他所有資料結構。下面是do_fork()函數執行的主要步驟:

1. 通過尋找pidmap_array位元影像,為子進程分配新的PID
2. 檢查父進程的ptrace欄位:如果它的值不等於0,說明有另外一個進程正在跟蹤父進程,因而,do_fork()函數檢查debugger程式是否自己想跟蹤子進程。在這種情況下,如果子進程不是核心線程(CLONE_UNTRACED標誌被清0),則do_fork()函數設定CLONE_PTRACE標誌。
3. 調用copy_process()函數複製進程描述符。如果所有必須的資源都是可用的,則該函數返回剛建立的task_struct描述符的地址。
4. 如果設定了CLONE_STOPPED標誌,或者必須跟蹤子進程,即在p->ptrace 中設定 PT_PTRACED標誌,那麼子進程的狀態被設定成TASK_STOPPED狀態,並且為子進程增加掛起的SIGSTOP訊號。在另一個進程把子進程狀態恢複成TASK_RUNNING之前,一直保持該狀態。

5. 如果沒有設定CLONE_STOPPED標誌,則調用wake_up_new_task(p, clone_flags)函數以執行以下操作:

a.調整父進程和子進程的調度參數
b.如果子進程和父進程運行在同一個CPU上,而且父進程和子進程不能共用同一組頁表(CLONE_VM標誌被清0),那麼,就把子進程插入到父進程的運行隊列,插入時讓子進程恰好在父進程前面,因此迫使子進程優於父進程先運行。如果子進程重新整理其地址空間,並且在建立之後執行新程式,那麼這種簡單的處理會產生較好的效能。而如果我們讓父進程先運行,那麼寫時複製機制將會執行一些不必要的頁面複製。
c.否則,如果子進程與父進程運行在不同CPU上,或者父進程和子進程共用同一組頁表(CLONE_VM標誌被設定),就把子進程插入父進程所在運行隊列的隊尾。

6. 如果設定了CLONE_STOPPED標誌,則子進程的狀態被設定成TASK_STOPPED狀態。
7. 如果父進程被跟蹤,則把子進程的PID存入current的ptrace_message欄位並調用ptrace_notify函數使當前進程停止運行,並向當前進程的父進程發送SIGCHLD訊號。子進程的祖父進程是跟蹤父進程的debugger進程。SIGCHLD訊號通知debugger進程:當前進程current已經建立了一個子進程,可以通過current->ptrace_message欄位獲得該子進程的PID。
8. 如果設定了CLONE_VFORK標誌,則把父進程插入等待隊列,並掛起父進程直到子進程釋放自己的記憶體位址空間(也就是說,直到子進程結束或執行新的程式)。
9. 結束並返回子進程的PID。

下邊,我們重點關注一下do_fork()函數的copy_process過程,其同樣來自於/kernel/Fork.c中:copy_process()函數建立進程描述符以及子進程執行所需要的其他資料結構。他的參數與do_fork()相同,外加子進程PID:
p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, nr);

下面描述其最重要的處理步驟:
1. 檢查參數clone_flags所傳遞標誌的一致性。尤其是在下列情況下,函數返回錯誤代號:

a. CLONE_NEWNS和CLONE_FS標誌都被設定。
b. CLONE_THREAD標誌被設定,但CLONE_SIGHAND標誌被清0(同一線程組中的輕量級進程必須共用訊號)。

2. 通過調用security_task_create(clone_flags)函數以及稍後的security_task_alloc(p)函數執行所有附加的安全檢查。
3. 調用dup_task_struct(current)函數(也是來自/kernel/Fork.c)為子進程獲得進程描述符。該函數執行以下操作:

a. 如果需要,則在當前進程中調用__unlazy_fpu()宏,把FPU、MMX和SSE/SSE2寄存器內容儲存到thread_info結構中。稍後,dup_task_struct函數將把這些複製到子進程的thread_info結構中。
b. 執行alloc_task_struct()宏,用slab分配器task_struct_cachep為新進程擷取進程描述符,並將描述符地址儲存在tsk局部變數中。
c. 執行alloc_thread_info宏以擷取一塊空閑記憶體區(#define alloc_thread_info(tsk) kmalloc(THREAD_SIZE, GFP_KERNEL)),用來存放新進程的thread_info結構和核心棧,並將這些塊記憶體區欄位的地址存在局部變數ti中。正如前面博文說的,這塊記憶體區的大小是8KB或4KB。
d. 將current進程描述符的內容複寫到tsk所指向的task_struct結構中,然後把tsk->thread_info置為ti。注意,這裡的代碼是簡簡單單的*tsk = *orig:讓建立立的tsk指向的task_struct和orig指向的父進程的task_struct中的每個欄位的值完全相等(完全複製)。
e. 把current進程的thread_info描述符的內容複寫到ti所指向的結構中,然後把ti->task置為tsk。
f. 把新進程描述符使用計數器(tsk->usage)置為2,用來表示進程描述符正在被使用而且其相應的進程狀態處於活動狀態(進程狀態既不是EXIT_ZOMBIE,也不是EXIT_DEAD)。
g. 返回新進程的進程描述符指標tsk。

4. 檢查存放在p->signal->rlim[RLIMIT_NPROC].rlim_cur變數中的值是否小於或等於使用者所擁有的進程數。如果是,則返回錯誤碼,除非進程沒有root許可權。該函數從每個使用者資料結構user_struct中擷取使用者擁有的進程數。通過進程描述符user欄位指標可以找到這個資料結構。
5. 遞增user_struct結構中使用計數器(tsk->user->__count)和使用者所擁有的進程計數器(tsk->user->processes)。
6. 查系統中的進程數量(存放在nr_threads變數中)是否超過max_threads變數的值。這個變數的預設值取決於系統記憶體容量的大小。總的原則是:所有thread_info描述符和核心棧所佔用的空間不能超過實體記憶體大小的1/8。不過,系統管理員可以通過寫/proc/sys/kernel/threads-max檔案來改變這個值。
7. 如果實現新進程的執行域和可執行格式的核心功能都包含在核心模組中,則遞增它們的使用計數器。
8. 設定與進程狀態相關的幾個關鍵字段:

a. 把大核心鎖計數器tsk->lock_depth初始化為-1。
b. 把tsk->did_exec欄位初始化為0:它記錄了進程發出的execve()系統調用的次數。
c. 通過copy_flags函數更新從父進程複製到tsk->flags欄位中的一些標誌:首先清除PF_SUPERPRIV標誌,該標誌表示進程是否使用了某種超級使用者權限。然後設定PF_FORKNOEXEC標誌,它表示子進程還沒有發出execve()系統調用。

9. 把新進程的PID存入tsk_pid欄位。
10. 如果clone_flags參數中的CLONE_PARENT_SETTID標誌被設定,就把子進程的PID複製到參數parent_tidptr指向的使用者態變數中。
11. 初始化子進程描述符中的list_head資料結構和自旋鎖,並為與掛起訊號、定時器及時間統計表相關的若干欄位賦初值。
12. 調用copy_semundo、copy_files、copy_fs、copy_sighand、copy_signal、copy_mm和copy_namespace來建立新的資料結構,並把父進程的相應資料結構的值複製到新資料結構中,除非clone_flags參數指出它們有不同的值。
13. 調用copy_thread(0, clone_flags, stack_start, stack_size, p, regs),用發出clone()系統調用時CPU寄存器的值來初始化子進程的核心棧。不過,copy_thread把eax寄存器對應的欄位(這也是fork和clone系統調用在子進程中的傳回值)欄位強行置為0。進程描述符的thread.esp欄位初始化為子進程核心棧的基地址,組合語言函數ret_from_fork()的地址存放在thread.eip欄位中。如果父進程使用I/O許可權位元影像,則子進程擷取該位元影像的一個拷貝。最後,如果CLONE_SETTLS標誌被設定,則子進程擷取由clone系統調用的參數tls指向的使用者態資料結構所表示的TLS段。
14. 如果clone_flags參數的值被置為CLONE_CHILD_SETTID或CLONE_CHILD_CLEARTID,就把child_tidptr參數的值分別複製到tsk->set_child_tid或tsk->clear_child_tid欄位。這些標誌說明:必須改變子進程使用者態地址空間的child_tidptr所指向的變數的值,不過實際的寫操作要稍後再執行。
15. 清除子進程thread_info結構的TIF_SYSCALL_TRACE標誌,以使ret_from_fork()函數不會把系統調用結束的訊息通知給調試進程。
16. 用clone_flags參數低位的訊號數字編碼初始化tsk-> exit_signal欄位,如果CLONE_THREAD標誌被設定,就把tsk-> exit_signal欄位初始化為-1。正如我們將在下一章進程終止所看見的,只有當線程組的最後一個成員(通常是現在組的頭兒)死亡,才會產生一個訊號,以通知領頭進程的父進程。
17. 調用sched_fork(p)完成對新進程發送器資料結構的初始化。該函數將新進程的狀態設定為TASK_RUNNING,並把thread_info結構的preempt_count欄位設定為1,從而禁止核心搶佔。此外,為了保證公平的進程調度,該函數在父子進程之間共用父進程的時間片。
18. 把新進程的thread_info結構的cpu欄位設定為由smp_processor_id()所返回的本地CPU號。
19. 初始化表示親子關係的欄位。尤其是,如果CLONE_PARENT或CLONE_THREAD被設定,就用current->real_parent的值初始化tsk->real_parent和tskp->parent,因此,子進程的父進程似乎是當前進程的父進程。否則tsk->real_parent和tskp->parent置為當前進程。
20. 如果不需要跟蹤子進程(沒有設定CLONE_PTRACE標誌),就把tsk->ptrrace欄位設定為0。 tsk->ptrrace欄位會存放一些標誌,而這些標誌是在一個進程被另外一個進程跟蹤時才會用到的。採用這種方式,即使當前進程被跟蹤,子進程也不會被跟蹤。
21. 執行SET_LINKS宏,把新進程描述符插入進程鏈表。
22. 如果子進程必須被跟蹤(tsk->ptrrace欄位的PT_PTRACED標誌被設定),就把current->parent賦給tsk->parent,並將子進程插入偵錯工具的跟蹤鏈表中。
23. 調用attach_pid把新進程描述符PID插入pidhash[PIDTYPE_PID]散列表。
24. 如果子進程是領頭進程(CLONE_THREAD標誌被清0):

a. 把tsk->tgid的初值置為tsk->pid。
b. 把tsk->group_leader的初值設定為tsk。
c. 調用三次attach_pid()函數,把子進程分別插入PIDTYPE_TGID、PIDTYPE_PGID和PIDTYPE_SID類型的散列表。

25. 否則,如果子進程屬於它的父進程的線程組(CLONE_THREAD標誌被設定):

a. 把tsk->tgid的初值置為tsk->current->tgid。
b. 把tsk->group_leader的初值置為current->group_leader。
c. 調用attach_pid(),把子進程描述符插入PIDTYPE_TGID類型的散列表中。

26. 現在,新進程已經被加入進程集合:遞增nr_threads變數的值。
27. 遞增total_forks變數以記錄被建立的進程的數量。
28. 終止並返回子進程描述符指標(p,等價於tsk)。

好了,讓我們再回頭看看在do_fork()結束之後都發生了什麼。現在,我們有了處於可運行狀態的完整的子進程。但是,它還沒有實際運行,發送器要決定何時把CPU交給這個子進程。在以後的進程切換中,發送器繼續完善子進程:把子進程描述符thread欄位的值(TSS值)裝入幾個CPU寄存器。特別是把thread.esp裝入esp寄存器,把函數ret_from_fork()的地址裝入eip寄存器。這個組合語言函數調用schedule_tail()函數,用存放在棧中的值再裝入所有寄存器,並強迫CPU返回到使用者態。這樣,eax寄存器就裝過兩個值,一個是子進程的值0,一個是父進程的值——子進程的PID。然後在fork()、vfork()或clone()返回時,新進程將開始執行,這也是為啥這幾個函數會魔幻般地返回兩個值的原因。

除非fork系統調用返回0,否則,子進程將與父進程執行相同的代碼(參見copy_process的第13步)。應用程式的開發人員可以按照UNIX編程者熟悉的方式利用這一事實,在基於PID值的程式中插入一條if語句便可以使子進程與父進程有不同的行為。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.