一、原理分析
1.進程的描述
進程控制塊PCB——task_struct,為了管理進程,內核必須對每個進程進行清晰的描述,進程描述符提供了內核所需了解的進程信息。
struct task_struct{ volatile long state; //進程狀態,-1表示不可執行,0表示可執行,大於1表示停止 void *stack; //內核堆棧 atomic_t usage; unsigned int flags; //進程標識符 unsigned int ptrace; …… }
2.進程的創建
道生一(start_ kernel...cpu_ idle),一生二(kernel_ init和kthreadd),二生三(即前面的0、1、2三個進程),三生萬物(1號進程是所有用戶態進程的祖先,2號進程是所有內核線程的祖先)
start_ kernel創建了cpu_ idle,也就是0號進程。而0號進程又創建了兩個線程,一個是kernel_ init,也就是1號進程,這個進程最終啟動了用戶態;另一個是kthreadd。0號進程是固定的代碼,1號進程是通過復制0號進程PCB之后在此基礎上做修改得到的。
iret與int 0x80指令對應,一個是彈出寄存器值,一個是壓入寄存器的值。
如果將系統調用類比於fork();那么就相當於系統調用創建了一個子進程,然后子進程返回之后將在內核態運行,而返回到父進程后仍然在用戶態運行。
Linux中創建進程一共有三個函數:fork,創建子進程 vfork,與fork類似,但是父子進程共享地址空間,而且子進程先於父進程運行。 clone,主要用於創建線程。Linux中所有的進程創建都是基於復制的方式,Linux通過復制父進程來創建一個新進程,通過調用do_ fork來實現。然后對子進程做一些特殊的處理。而Linux中的線程,又是一種特殊的進程。根據代碼的分析,do_ fork中,copy_ process管子進程運行的准備,wake_ up_ new_ task作為子進程forking的完成。fork()函數最大的特點就是被調用一次,返回兩次。
追蹤do_fork的代碼
long do_fork(unsigned long clone_flags, unsigned long stack_start, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr) { struct task_struct *p; int trace = 0; long nr; // ... // 復制進程描述符,返回創建的task_struct的指針 p = copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace); if (!IS_ERR(p)) { struct completion vfork; struct pid *pid; trace_sched_process_fork(current, p); // 取出task結構體內的pid pid = get_task_pid(p, PIDTYPE_PID); nr = pid_vnr(pid); if (clone_flags & CLONE_PARENT_SETTID) put_user(nr, parent_tidptr); // 如果使用的是vfork,那么必須采用某種完成機制,確保父進程后運行 if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); get_task_struct(p); } // 將子進程添加到調度器的隊列,使得子進程有機會獲得CPU wake_up_new_task(p); // ... // 如果設置了 CLONE_VFORK 則將父進程插入等待隊列,並掛起父進程直到子進程釋放自己的內存空間 // 保證子進程優先於父進程運行 if (clone_flags & CLONE_VFORK) { if (!wait_for_vfork_done(p, &vfork)) ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid); } put_pid(pid); } else { nr = PTR_ERR(p); } return nr; }
通過上面的代碼,可以看出,do_fork大概做了這么幾件事情:
(1)調用copy_process,將當期進程復制一份出來為子進程,並且為子進程設置相應地上下文信息。
(2)初始化vfork的完成處理信息(如果是vfork調用)
(3)調用wake_up_new_task,將子進程放入調度器的隊列中,此時的子進程就可以被調度進程選中,得以運行。
(4)如果是vfork調用,需要阻塞父進程,知道子進程執行exec。
3.創建的新進程從哪里開始執行?
kernel中是可以指定新進程開始的位置(也就是通過eip寄存器指定代碼行)。fork中也有相似的機制 這涉及子進程的內核堆棧數據狀態和task_ struct中thread記錄的sp和ip的一致性問題,這是在copy_ thread in copy_ process設定的。
copy_thread的流程如下:
(1) 獲取子進程寄存器信息的存放位置
(2) 對子進程的thread.sp賦值,將來子進程運行,這就是子進程的esp寄存器的值。
(3)如果是創建內核線程,那么它的運行位置是ret_from_kernel_thread,將這段代碼的地址賦給thread.ip,之后准備其他寄存器信息,退出 。
(4)將父進程的寄存器信息復制給子進程。
(5)將子進程的eax寄存器值設置為0,所以fork調用在子進程中的返回值為0。
(6)子進程從ret_from_fork開始執行,所以它的地址賦給thread.ip,也就是將來的eip寄存器。 從上面的流程中,我們看出,子進程復制了父進程的上下文信息,僅僅對某些地方做了改動,運行邏輯和父進程完全一致。
另外,子進程從ret_from_fork處開始執行,子進程的運行是由這幾處保證的:
(1)dup_task_struct中為其分配了新的堆棧 。
(2)copy_process中調用了sched_fork,將其置為TASK_RUNNING 。
(3)copy_thread中將父進程的寄存器上下文復制給子進程,這是非常關鍵的一步,這里保證了父子進程的堆棧信息是一致的。
(4)將ret_from_fork的地址設置為eip寄存器的值,這是子進程的第一條指令。
二、實驗內容
更新menu,刪除test_fork.c和test.c文件,重新執行make rootfs。首先還是將QEMU啟動,然后在sys_clone處設置一個斷點。執行以下代碼:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char * argv[]) { int pid; /* fork another process */ pid = fork(); if (pid < 0) { /* error occurred */ fprintf(stderr,"Fork Failed!"); exit(-1); } else if (pid == 0) { /* child process */ printf("This is Child Process!\n"); } else { /* parent process */ printf("This is Parent Process!\n"); /* parent will wait for the child to complete*/ wait(NULL); printf("Child Complete!\n"); } }
執行后,來到了斷點處
sys_clone實際上調用的還是do_fork,也就是說真正的進程創建是都是最終調用do_fork,然后進入該函數,里面有一個函數copy_process,這里面就是開始了詳細的創建。首先是異常判斷,然后開始了dup_task_struct,也就是復制父進程,產生子進程。 具體函數為
int __weak arch_dup_task_struct(struct task_struct *dst, struct task_struct *src) { *dst = *src; return 0; }
復制結束后,子進程和父進程是一樣的,復制過程中內核堆棧也被復制了,接下來是對子進程進行賦值
復制文件,復制文件系統,復制信號等。 然后單步進入函數copy_thread。
p->thread.sp = (unsigned long) childregs; //子進程棧地址 *childregs = *current_pt_regs(); //子進程的寄存器 childregs->ax = 0; //ax保存返回值,子進程執行fork的返回值是0 p->thread.ip = (unsigned long) ret_from_fork; //子進程的開始執行地址
再往下繼續單步
編譯內核查看fork命令,得到子進程與父進程結果。
啟動gdb調試,並對主要的函數設置斷點。
三、總結
對於“特別關注新進程是從哪里開始執行的?為什么從哪里能順利執行下去?即執行起點與內核堆棧如何保證一致”,由於ret_ from_ fork決定新進程的第一條指令地址。子進程從ret_ from_ fork處開始執行。因為在ret_ from_ fork之前,也就是在copy_ thread()函數中* childregs = * current_ pt_ regs();該句將父進程的regs參數賦值到子進程的內核堆棧。* childregs的類型為pt_ regs,里面存放了SAVE_ ALL中壓入棧的參數,因此在之后的RESTORE ALL中能順利執行下去。
新的進程通過克隆舊的程序(當前進程)而建立。fork() 和 clone()(對於線程)系統調用可用來建立新的進程。這兩個系統調用結束時,內核在系統的物理內存中為新的進程分配新的 task_struct 結構,同時為新進程要使用的堆棧分配物理頁。Linux 還會為新的進程分配新的進程標識符。然后,新 task_struct 結構的地址保存在鏈表中,而舊進程的 task_struct 結構內容被復制到新進程的 task_struct 結構中。 在克隆進程時,Linux 允許兩個進程共享相同的資源。可共享的資源包括文件、信號處理程序和虛擬內存等。
劉帥
原創作品轉載請注明出處
《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000