date: 2014-10-22 17:40linux
前文已大體講過fork、vfork與clone的區別,先來看看它們的原型:數組
pid_t fork(void); pid_t vfork(void); int clone(int (*fn)(void *), void *child_stack, int flags, void* arg, ...);
fork是全面的複製。併發
vfork建立一個線程,但主要用做建立進程的中間步驟。既然新的進程最終要調用execve執行新的目標程序,並與父進程分道揚鑣,那麼複製父進程的資源徹底是多餘的,因而vfork便應運而生,其建立一個線程並經過指針拷貝共享父進程的資源(這些資源會在execve中被替換)。 vfork與fork的另外一個不一樣是,vfork會保證子進程先運行,在子進程執行execve或者exit以後,父進程纔可能被調度運行。函數
clone能夠用來建立一個線程,並能夠指定子線程的用戶空間堆棧的起始位置(child_stack參數)以及自線程的入口(參數fn)。同時clone也能夠用來建立一個進程,有選擇性的複製父進程的資源(由參數flag來指定)。atom
這幾個系統調用在內核中的實現以下(2.4.0內核)線程
<arch/kernel/process.c> asmlinkage int sys_fork(struct pt_regs regs) { return do_fork(SIGCHLD, regs.esp, ®s, 0); } asmlinkage int sys_clone(struct pt_regs regs) { unsigned long clone_flags; unsigned long newsp; clone_flags = regs.ebx; newsp = regs.ecx; if (!newsp) newsp = regs.esp; return do_fork(clone_flags, newsp, ®s, 0); } asmlinkage int sys_vfork(struct pt_regs regs) { return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0); }
可見這幾個系統調用最終都是以不一樣的參數調用do_fork。這三個函數參數中的pt_regs結構是系統調用陷入內核空間後,對用戶空間中CPU各通用寄存器的備份。對sys_clone來講,regs.ebx保存的是clone的參數flag(flag是第三個參數,不是應該保存在edx中嗎?),regs.ecx保存的就是clone的參數child_task。指針
do_fork函數的原型爲:code
int do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size);
第一個參數clone_flags包含兩部分,第一部分爲其最低的字節(8bit)表示信號類型,表示子進程去世時內核應該給其父進程發什麼信號。fork和vfork設置的信號爲SIGCHILD,而clone則由調用者決定發什麼信號。blog
第二部分是一些表示資源或者特性的標誌位,這些標誌位定義在<include/linux/sched.h>文件中:進程
/* * cloning flags: */ #define CSIGNAL 0x000000ff /* signal mask to be sent at exit */ #define CLONE_VM 0x00000100 /* set if VM shared between processes */ #define CLONE_FS 0x00000200 /* set if fs info shared between processes */ #define CLONE_FILES 0x00000400 /* set if open files shared between processes */ #define CLONE_SIGHAND 0x00000800 /* set if signal handlers and blocked signals shared */ #define CLONE_PID 0x00001000 /* set if pid shared */ #define CLONE_PTRACE 0x00002000 /* set if we want to let tracing continue on the child too */ #define CLONE_VFORK 0x00004000 /* set if the parent wants the child to wake it up on mm_release */ #define CLONE_PARENT 0x00008000 /* set if we want to have the same parent as the cloner */ #define CLONE_THREAD 0x00010000 /* Same thread group? */ #define CLONE_SIGNAL (CLONE_SIGHAND | CLONE_THREAD)
這裏的標誌位若是爲1則表示對應的資源經過指針拷貝(「淺拷貝」)的方式與父進程共享,反之若是爲0則要進行深度拷貝了。其中CLONE_PID有特殊的用途,它表示父子進程共用同一進程號PID,也就是說子進程雖然有其本身的task_struct結構,卻使用父進程的PID,可是隻有0號進程(0號進程不是init進程,init進程的進程號爲1,0號進程空轉進程即idle進程)被容許如此調用clone。
對於fork,這部分全爲0,表示全部的資源都要複製(「深拷貝」);而對vfork,其設置了CLONE_VFORK和CLONE_VM標誌,表示子進程(線程)共用父進程(用戶空間的)虛存空間,而且當子進程釋放其虛存區間時,要通知父進程;至於clone,這一部分也由調用者設定。
下面分析下do_fork的流程,若是讀者理解淺拷貝和深拷貝,那麼複製的過程很好理解。
下面對流程中的重點函數進行詳細介紹。
通常來說,將上一個進程的PID加1便可獲得新進程的PID,但系統中規定PID不能≥PID_MAX(PID_MAX 定義爲0x8000,可見最大的PID號爲0x7fff即32767)。若是PID≥PID_MAX,則要回過頭去「撿漏」:某些進程已經去世了,以前分配給他們的PID能夠從新利用了。注意撿漏要從PID爲300開始掃描,由於300之內(0~299)的PID保留給系統進程使用(包括內核線程和各「守護神」進程)。若是掃描只爲找到一個可用的PID,顯然不划算,最好是能找到一個可用PID的區間(而不只僅是找到第一個可用的PID)。這樣下一次再建立進程時,直接從可用PID區間中取下一個PID便可。
task_struct結構有一個files成員,其爲struct files_struct類型的指針,指向進程打開的文件的信息,copy_files函數完成該指針的深度拷貝。files_struct結構體的定義以下:
/* * Open file table structure */ struct files_struct { atomic_t count; rwlock_t file_lock; int max_fds; int max_fdset; int next_fd; struct file ** fd; /* current fd array */ fd_set *close_on_exec; fd_set *open_fds; fd_set close_on_exec_init; fd_set open_fds_init; struct file * fd_array[NR_OPEN_DEFAULT]; };
files_struct結構主要有三個部件:其一是個位圖(指針)close_on_exec,表示在進程執行exce時須要close的文件描述符,初始指向結構體中另外一個成員close_on_exec_init的地址;其二也是一個位圖(指針)open_fds,初始指向結構體中另外一成員open_fds_init的地址;其三爲指向文件表項(struct file)指針數組的指針fd,指向進程打開的文件表項數組,初始指向結構體中另外一成員fd_array的地址。因爲位圖(fd_set)可以表示最大文件描述符爲1024(fd_set共有1024個bit),而fd_array數組的大小固定爲NR_OPEN_DEFAULT,隨着程序的運行,進程打開的文件數可能超過NR_OPEN_DEFAULT,或者fd_set(1024個bit)已經沒法容納最大的文件描述符,這個時候就須要對這個三個部件進行擴展,而這三個指針則指向結構體外的某個地址。
若有須要(clone_flag中對應的標誌位爲0,下同),copy_files將執行「深度拷貝」,這裏「深度拷貝」主要是這三個部件的拷貝,在拷貝時要判斷父進程的三個部件是否已進行過擴展,若是是則子進程的三個部件也要進行擴展。
注意,這裏所謂的「深度拷貝」只是中間層次的「深度拷貝」。以fd爲例,fd爲指向指針數組的指針,拷貝以後子進程有本身的一塊存儲空間,用來存放全部的文件表項(struct file)的指針,可是並未執行更深層次的拷貝,並無對每一個文件表項指針所指向的文件表項(struct file)進行拷貝。父子進程每一個相同的文件描述符共享同一個文件表項 ,以下圖所示:
可見,fork以後,當子進程經過lseek移動某個文件的「當前文件偏移量」時,因爲這個偏移量記錄在文件表項file中,父進程中對應文件的「當前文件偏移量」也會被更新,這就是在父子進程中同時往標準輸出中打印信息時不會重疊的緣由。罕見的,《情景分析》關於這一點的描述有誤。
task_struct結構中有一sig成員,其爲struct signal_struct類型,後者的定義以下:
struct signal_struct { atomic_t count; struct k_sigaction action[_NSIG]; spinlock_t siglock; };
其核心成員action爲一數組,指定了各個信號的處理方式。_NSIG即爲信號的最大值64。copy_sighand所執行的深度拷貝就是將action數組拷貝一份到子進程。
task_struct結構中有一個指針mm,相信你們已經很熟悉了,其爲mm_struct結構類型,表明了進程的用戶空間。mm_struct中用兩個重要的成員,其一爲vm_area_struct類型的指針mmap,表明着用戶空間的全部「虛存空間」;其二爲pgd,領銜着「虛存區間」的頁面映射。相應的,copy_mm所執行的深度拷貝也包括兩部分:「虛存區間」的拷貝和頁面表的拷貝。
注意,在拷貝頁表時,只是爲子進程的頁表分配了存儲空間,頁面表項(pte_t)的內容與父進程一致。這意味着針對每個頁面表項,子進程並無像咱們想象的那樣,從新申請一個page,從父進程那裏拷貝一個page的內容,而後將新page的地址和保護屬性寫入頁面表項。取而代之的作法是:子進程的頁面表項仍然指向父進程中的page,只是將這個page的保護屬性設置成「只讀」,並將保護屬性同時設置進父、子進程的頁面表項中。這樣的話,當無論父進程仍是子進程企圖寫入這個頁面時,都會觸發異常頁面異常,這種狀況下,頁面異常處理程序會從新分配一個page,並把內容複製過來,並更新父子進程的頁面表項(將保護屬性設置成可寫),讓父子進程擁有各自的物理page。這就是copy_on_write技術。linux之因此能快速的複製出一個進程,徹底歸功於copy on write技術(不然的話,在fork時,就得老老實實的分配一個一個的物理頁面,並將頁面的內容拷貝過來)。
咱們以前講過進程的系統空間爲兩個頁面即8K的空間,底端爲task_struct結構,task_struct結構之上爲系統空間堆棧。其實,在系統空間堆棧之上即8K空間的頂端,存放的是pt_regs結構,以下圖所示:
pt_regs結構保存着進入內核前夕CPU各個寄存器的內容,這但是系統調用返回到用戶空間的重要「現場」,對於剛剛出生的子進程,這些信息只能從父進程拷貝而來,也正因如此,父子進程才能夠返回到用戶空間的同一個地方。
copy_thread的代碼比較有趣,咱們來看看:
<arch/kernel/process.c> 529 int copy_thread(int nr, unsigned long clone_flags, unsigned long esp, 530 unsigned long unused, 531 struct task_struct * p, struct pt_regs * regs) 532 { 533 struct pt_regs * childregs; 534 535 childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) - 1; 536 struct_cpy(childregs, regs); 537 childregs->eax = 0; 538 childregs->esp = esp; 539 540 p->thread.esp = (unsigned long) childregs; 541 p->thread.esp0 = (unsigned long) (childregs+1); 542 543 p->thread.eip = (unsigned long) ret_from_fork; 544 545 savesegment(fs,p->thread.fs); 546 savesegment(gs,p->thread.gs); 547 548 unlazy_fpu(current); 549 struct_cpy(&p->thread.i387, ¤t->thread.i387); 550 551 return 0; 552 }
參數中的p指向新建立的子進程;而regs則來自父進程;esp指向進程用戶空間的堆棧的棧頂,對fork與vfork而言,esp來自regs.esp即父進程的用戶空間地址的棧頂,對clone而言,棧頂esp可有調用者指定。
首先,要肯定在進程的系統空間中pt_regs結構的起始地址,這是535行的做用。逐步拆解以下:
(THREAD_SIZE + (unsigned long) p) 定位到子進程系統空間(8K)的上邊界;
((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) 將系統空間的上邊界強轉成struct pt_regs類型的指針;
((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) – 1,在struct pt_regs類型的指針類型的指針上執行減1操做,指針後移sizeof(struct pt_regs)個存儲單元,因而定位到pt_regs結構的起始地址。
其次,子進程的pt_regs拷貝自父進程(第536行),但也要進行些「修補」。第537行修改子進程在用戶空間的返回值爲0;第538行修改進程用戶空間的棧頂esp。
另外,task_struct結構中有一個thread成員,其爲struct thread_struct類型,裏面存放着進程在切換時系統空間堆棧的棧頂esp、下一條指令eip(進程再次被切換運行時,將從這裏開始運行)等關鍵信息。在複製task_struct結構時,這些內容原封不動從父進程拷貝過來,如今子進程有本身的系統空間堆棧了,因此要適當的加以調整。第540行將p->thread.esp設置成pt_regs結構的起始地址(注意堆棧是向下擴展的),從調度器的角度來看,就好像這個子進程之前曾經進入內核運行過,而在內核中的任務處理完畢(所以進程系統空間堆棧恢復平衡,變成「空」堆棧)準備返回用戶空間時被切換了;而p->thread.esp0則應指向系統空間堆棧的頂端,表示這個進程進入0級(內核空間)運行時,其堆棧的位置。第543行,p->thread.eip被賦值爲ret_from_fork,當子進程調度運行時(確定先從系統空間運行),將從ret_from_fork處開始運行。
前文提到,vfork會保證子進程先運行,在子進程執行execve或者exit以後,父進程纔可能被調度運行。爲了實現這個需求,task_struct結構中有一個信號量(struct semaphore)類型的指針vfork_sem。在do_fork函數開始位置,初始化了一個局部信號量sem,並將sem的地址賦給父進程的task_struct.vfork_sem(流程圖中黃色標註的部分)。信號量的初始計數爲0,意味着此時對信號量執行down操做,進程將被阻塞。do_fork在完成子進程的「複製」之後,調用wake_up_process喚醒子進程,而本身則調用down(&sem)進入睡眠。
在子進程調用exec或者exit時,都會調用mm_release,在該函數中,up(tsk->p_opptr->vfork_sem)增長了父進程vfork_sem的計數,因而父進程被喚醒。
void mm_release(void) { struct task_struct *tsk = current; /* notify parent sleeping on vfork() */ if (tsk->flags & PF_VFORK) { tsk->flags &= ~PF_VFORK; up(tsk->p_opptr->vfork_sem); } }
仔細想來,vfork保證子進程先運行,不見得是內核「精心」爲linux用戶實現的特性,倒像是內核的無奈之舉。咱們知道vfork系統調用,父子進程共享進程用戶空間,子進程對用戶空間的修改會反過來影響父進程,反之亦然。若是說父子進程各自修改數據區的數據是危險的話,那麼修改堆棧那就是致命的了。而恰巧子進程緊接着就會調用exec(或者exit),而函數調用涉及到傳參,涉及到設置返回地址這些都會修改堆棧。因此決不能讓兩個進程都回到用戶空間併發的運行,必須扣留其中的一個,而只讓一個返回到用戶空間去運行,直到兩個進程再也不共享用戶空間(子進程執行exec)或者其中之一消亡(必然是先返回用戶空間的子進程)爲止。
即便如此,也仍是有危險,子進程絕對不能從調用vfork的那個函數(caller)中返回(好比vfork以後子進程調用return),由於函數返回時,棧會被恢復平衡,以前用來存放caller的函數參數和函數返回地址的內存單元被回收,以後,這些內存單元可能被用來存放其餘的內容。等到子進程去世父進程開始運行時,雖然父進程雖然有本身的esp(保存在pt_regs中),但棧中保存的caller的返回地址被破壞了(esp之上的內容都被破壞),這是很危險的。因此vfork其實是創建在子進程在建立後當即就會調用execve的前提之上的。