Linux进程的创建函数fork()及其fork内核实现解析
生活随笔
收集整理的這篇文章主要介紹了
Linux进程的创建函数fork()及其fork内核实现解析
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
進程的創建之fork()Linux系統下,進程可以調用fork函數來創建新的進程。調用進程為父進程,被創建的進程為子進程。fork函數的接口定義如下:#include <unistd.h> pid_t fork(void); 與普通函數不同,fork函數會返回兩次。一般說來,創建兩個完全相同的進程并沒有太多的價值。大部分情況下,父子進程會執行不同的代碼分支。fork函數的返回值就成了區分父子進程的關鍵。fork函數向子進程返回0,并將子進程的進程ID返給父進程。當然了,如果fork失敗,該函數則返回-1,并設置errno。
從2.6.24起,Linux采用完全公平調度(Completely Fair Scheduler,CFS)。用戶創建的普通進程,都采用CFS調度策略。對于CFS調度策略,procfs提供了如下控制選項:
/proc/sys/kernel/sched_child_runs_first 該值默認是0,表示父進程優先獲得調度。如果將該值改成1,那么子進程會優先獲得調度。
fork之后父子進程的內存關系fork之后的子進程完全拷貝了父進程的地址空間,包括棧、堆、代碼段等。通過下面的示例代碼,我們一起來查看父子進程的內存關系:#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <errno.h> #include <sys/types.h> #include <wait.h> int g_int = 1;//數據段的全局變量 int main() { int local_int = 1;//棧上的局部變量 int *malloc_int = malloc(sizeof(int));//通過malloc動態分配在堆上的變量 *malloc_int = 1; pid_t pid = fork(); if(pid == 0) /*子進程*/ { local_int = 0; g_int = 0; *malloc_int = 0; fprintf(stderr,"[CHILD ] child change local global malloc value to 0\n"); free(malloc_int); sleep(10); fprintf(stderr,"[CHILD ] child exit\n"); exit(0); } else if(pid < 0) { printf("fork failed (%s)",strerror(errno)); return 1; } fprintf(stderr,"[PARENT] wait child exit\n"); waitpid(pid,NULL,0); fprintf(stderr,"[PARENT] child have exit\n"); printf("[PARENT] g_int = %d\n",g_int); printf("[PARENT] local_int = %d\n",local_int); printf("[PARENT] malloc_int = %d\n",local_int); free(malloc_int); return 0; } 這里刻意定義了三個變量,一個是位于數據段的全局變量,一個是位于棧上的局部變量,還有一個是通過malloc動態分配位于堆上的變量,三者的初始值都是1。然后調用fork創建子進程,子進程將三個變量的值都改成了0。按照fork的語義,子進程完全拷貝了父進程的數據段、棧和堆上的內存,如果父子進程對相應的數據進行修改,那么兩個進程是并行不悖、互不影響的。因此,在上面示例代碼中,盡管子進程將三個變量的值都改成了0,對父進程而言這三個值都沒有變化,仍然是1,代碼的輸出也證實了這一點。[PARENT] wait child exit [CHILD ] child change local global malloc value to 0 [CHILD ] child exit [PARENT] child have exit [PARENT] g_int = 1 [PARENT] local_int = 1 [PARENT] malloc_int = 1
前文提到過,子進程和父進程執行一模一樣的代碼的情形比較少見。Linux提供了execve系統調用,構建在該系統調用之上,glibc提供了exec系列函數。這個系列函數會丟棄現存的程序代碼段,并構建新的數據段、棧及堆。調用fork之后,子進程幾乎總是通過調用exec系列函數,來執行新的程序。在這種背景下,fork時子進程完全拷貝父進程的數據段、棧和堆的做法是不明智的,因為接下來的exec系列函數會毫不留情地拋棄剛剛辛苦拷貝的內存。為了解決這個問題,Linux引入了寫時拷貝(copy-on-write)的技術。寫時拷貝是指子進程的頁表項指向與父進程相同的物理內存頁,這樣只拷貝父進程的頁表項就可以了,當然要把這些頁面標記成只讀(如圖4-4所示)。如果父子進程都不修改內存的內容,大家便相安無事,共用一份物理內存頁。但是一旦父子進程中有任何一方嘗試修改,就會引發缺頁異常(page fault)。此時,內核會嘗試為該頁面創建一個新的物理頁面,并將內容真正地復制到新的物理頁面中,讓父子進程真正地各自擁有自己的物理內存頁,然后將頁表中相應的表項標記為可寫。從上面的描述可以看出,對于沒有修改的頁面,內核并沒有真正地復制物理內存頁,僅僅是復制了父進程的頁表。這種機制的引入提升了fork的性能,從而使內核可以快速地創建一個新的進程。查看下copy_one_pte函數中有如下代碼:/*如果是寫時拷貝, 那么無論是初始頁表, 還是拷貝的頁表, 都設置了寫保護 *后面無論父子進程, 修改頁表對應位置的內存時, 都會觸發page fault */ if (is_cow_mapping(vm_flags)) { ptep_set_wrprotect(src_mm, addr, src_pte);//設置為寫保護 pte = pte_wrprotect(pte); } 該代碼將頁表設置成寫保護,父子進程中任意一個進程嘗試修改寫保護的頁面時,都會引發缺頁中斷,內核會走向do_wp_page函數,該函數會負責創建副本,即真正的拷貝。寫時拷貝技術極大地提升了fork的性能,在一定程度上讓vfork成為了雞肋。
父子進程共用了一套文件偏移量
文件描述符還有一個文件描述符標志(file descriptor flag)。目前只定義了一個標志位:FD_CLOSEXEC,這是close_on_exec標志位。細心閱讀open函數手冊也會發現,open函數也有一個類似的標志位,即O_CLOSEXEC,該標志位也是用于設置文件描述符標志的。那么這個標志位到底有什么作用呢?如果文件描述符中將這個標志位置位,那么調用exec時會自動關閉對應的文件??墒菫槭裁葱枰@個標志位呢?主要是出于安全的考慮。對于fork之后子進程執行exec這種場景,如果子進程可以操作父進程打開的文件,就會帶來嚴重的安全隱患。一般來講,調用exec的子進程時,因為它.會另起爐灶,因此父進程打開的文件描述符也應該一并關閉,但事實上內核并沒有主動這樣做。試想如下場景,Webserver首先以root權限啟動,打開只有擁有root權限才能打開的端口和日志等文件,再降到普通用戶,fork出一些worker進程,在進程中進行解析腳本、寫日志、輸出結果等操作。由于子進程完全可以操作父進程打開的文件,因此子進程中的腳本只要繼續操作這些文件描述符,就能越權操作root用戶才能操作的文件。為了解決這個問題,Linux引入了close on exec機制。設置了FD_CLOSEXEC標志位的文件,在子進程調用exec家族函數時會將相應的文件關閉。而設置該標志位的方法有兩種:
·open時,帶上O_CLOSEXEC標志位?!pen時如果未設置,那就在后面調用fcntl函數的F_SETFD操作來設置。建議使用第一種方法。原因是第二種方法在某些時序條件下并不那么絕對的安全??紤]圖4-7的場景:Thread 1還沒來得及將FD_CLOSEXEC置位,由于Thread 2已經執行過fork,這時候fork出來的子進程就不會關閉相應的文件。盡管Thread1后來調用了fcntl的F_SETFD操作,但是為時已晚,文件已經泄露了。注意 圖4-7中,多線程程序執行了fork,僅僅是為了示意,實際中并不鼓勵這種做法。正相反,這種做法是十分危險的。多線程程序不應該調用fork來創建子進程,第8章會分析具體原因。前面提到,執行fork時,子進程會獲取父進程所有文件描述符的副本,但是測試結果表明,父子進程共享了文件的很多屬性。這到底是怎么回事?讓我們深入內核一探究竟。在內核的進程描述符task_struct結構體中,與打開文件相關的變量如下所示:struct task_struct { ...struct files_struct *files;... } 調用fork時,內核會在copy_files函數中處理拷貝父進程打開的文件的相關事宜:
static int copy_files(unsigned long clone_flags, struct task_struct *tsk) { struct files_struct *oldf, *newf; int error = 0; oldf = current->files;//獲取父進程的文件結構體 if (!oldf) goto out; /*創建線程和vfork, 都不用復制父進程的文件描述符, 增加引用計數即可*/ if (clone_flags & CLONE_FILES) { atomic_inc(&oldf->count); goto out; } /*對于fork而言, 需要復制父進程的文件描述符*/ newf = dup_fd(oldf, &error); //復制一份文件描述符 if (!newf) goto out; tsk->files = newf; error = 0; out: return error; } CLONE_FILES標志位用來控制是否共享父進程的文件描述符。如果該標志位置位,則表示不必費勁復制一份父進程的文件描述符了,增加引用計數,直接共用一份就可以了。對于vfork函數和創建線程的pthread_create函數來說都是如此。但是fork函數卻不同,調用fork函數時,該標志位為0,表示需要為子進程拷貝一份父進程的文件描述符。文件描述符的拷貝是通過內核的dup_fd函數來完成的。
struct files_struct *dup_fd(struct files_struct *oldf, int *errorp) { struct files_struct *newf; struct file **old_fds, **new_fds; int open_files, size, i; struct fdtable *old_fdt, *new_fdt; *errorp = -ENOMEM; newf = kmem_cache_alloc(files_cachep, GFP_KERNEL); if (!newf) goto out; dup_fd函數首先會給子進程分配一個file_struct結構體,然后做一些賦值操作。這個結構體是進程描述符中與打開文件相關的數據結構,每一個打開的文件都會記錄在該結構體中。其定義代碼如下:
struct files_struct { atomic_t count; struct fdtable __rcu *fdt; struct fdtable fdtab; spinlock_t file_lock ____cacheline_aligned_in_smp; int next_fd; struct embedded_fd_set close_on_exec_init; struct embedded_fd_set open_fds_init; struct file __rcu * fd_array[NR_OPEN_DEFAULT]; }; struct fdtable //文件描述符表 { unsigned int max_fds; struct file __rcu **fd; /* current fd array */ fd_set *close_on_exec; fd_set *open_fds; struct rcu_head rcu; struct fdtable *next; }; struct embedded_fd_set { unsigned long fds_bits[1]; }; 初看之下struct fdtable的內容與struct files_struct的內容有頗多重復之處,包括close_on_exec文件描述符位圖、打開文件描述符位圖及file指針數組等,但事實上并非如此。struct files_struct中的成員是相應數據結構的實例,而struct fdtable中的成員是相應的指針。
Linux系統假設大多數的進程打開的文件不會太多。于是Linux選擇了一個long類型的位數(32位系統下為32位,64位系統下為64位)作為經驗值。以64位系統為例,file_struct結構體自帶了可以容納64個struct file類型指針的數組fd_array,也自帶了兩個大小為64的位圖,其中open_fds_init位圖用于記錄文件的打開情況,close_on_exec_init位圖用于記錄文件描述符的FD_CLOSEXCE標志位是否置位。只要進程打開的文件個數小于64,file_struct結構體自帶的指針數組和兩個位圖就足以滿足需要。因此在分配了file_struct結構體后,內核會初始化file_struct自帶的fdtable,代碼如下所示:atomic_set(&newf->count, 1); spin_lock_init(&newf->file_lock); newf->next_fd = 0; new_fdt = &newf->fdtab; new_fdt->max_fds = NR_OPEN_DEFAULT; new_fdt->close_on_exec = (fd_set *)&newf->close_on_exec_init; new_fdt->open_fds = (fd_set *)&newf->open_fds_init; new_fdt->fd = &newf->fd_array[0]; new_fdt->next = NULL; 初始化之后,子進程的file_struct的情況如圖4-8所示。注意,此時file_struct結構體中的fdt指針并未指向file_struct自帶的struct fdtable類型的fdtab變量。原因很簡單,因為此時內核還沒有檢查父進程打開文件的個數,因此并不確定自帶的結構體能否滿足需要。
接下來,內核會檢查父進程打開文件的個數。如果父進程打開的文件超過了64個,struct files_struct中自帶的數組和位圖就不能滿足需要了。這種情況下內核會分配一個新的struct fdtable,代媽如下:
spin_lock(&oldf->file_lock); old_fdt = files_fdtable(oldf); open_files = count_open_files(old_fdt); /*如果父進程打開文件的個數超過NR_OPEN_DEFAULT*/ while (unlikely(open_files > new_fdt->max_fds)) { spin_unlock(&oldf->file_lock); /* 如果不是自帶的fdtable而是曾經分配的fdtable, 則需要先釋放*/ if (new_fdt != &newf->fdtab) __free_fdtable(new_fdt); /*創建新的fdtable*/ new_fdt = alloc_fdtable(open_files - 1); if (!new_fdt) { *errorp = -ENOMEM; goto out_release; } /*如果超出了系統限制, 則返回EMFILE*/ if (unlikely(new_fdt->max_fds < open_files)) { __free_fdtable(new_fdt); *errorp = -EMFILE; goto out_release; } spin_lock(&oldf->file_lock); old_fdt = files_fdtable(oldf); open_files = count_open_files(old_fdt); } alloc_fdtable所做的事情,不過是分配fdtable結構體本身,以及分配一個指針數組和兩個位圖。分配之前會根據父進程打開文件的數目,計算出一個合理的值nr,以確保分配的數組和位圖能夠滿足需要。
無論是使用file_struct結構體自帶的fdtable,還是使用alloc_fdtable分配的fdtable,接下來要做的事情都一樣,即將父進程的兩個位圖信息和打開文件的struct file類型指針拷貝到子進程的對應數據結構中,代碼如下:
old_fds = old_fdt->fd; /*父進程的struct file 指針數組*/ new_fds = new_fdt->fd; /*子進程的struct file 指針數組*/ /* 拷貝打開文件位圖 */ memcpy(new_fdt->open_fds->fds_bits,old_fdt->open_fds->fds_bits, open_files/8); /* 拷貝 close_on_exec位圖 */ memcpy(new_fdt->close_on_exec->fds_bits,old_fdt->close_on_exec->fds_bits, open_files/8); for (i = open_files; i != 0; i--) { ?struct file *f = *old_fds++; if (f) { ?get_file(f); /* f對應的文件的引用計數加1 */ ?}?else { FD_CLR(open_files - i, new_fdt->open_fds); } /* 子進程的struct file類型指針, *指向和父進程相同的struct file 結構體*/ rcu_assign_pointer(*new_fds++, f); ? } spin_unlock(&oldf->file_lock);/* compute the remainder to be cleared */ size = (new_fdt->max_fds - open_files) * sizeof(struct file *); /*將尚未分配到的struct file結構的指針清零*/ ????memset(new_fds, 0, size);/*將尚未分配到的位圖區域清零*/ ????if (new_fdt->max_fds > open_files) { int left = (new_fdt->max_fds-open_files)/8; int start = open_files / (8 * sizeof(unsigned long)); memset(&new_fdt->open_fds->fds_bits[start], 0, left); memset(&new_fdt->close_on_exec->fds_bits[start], 0, left); } ????rcu_assign_pointer(newf->fdt, new_fdt); ????return newf; out_release: ????kmem_cache_free(files_cachep, newf); out: ????return NULL; } 通過對上述流程的梳理,不難看出,父子進程之間拷貝的是struct file的指針,而不是struct file的實例,父子進程的struct file類型指針,都指向同一個struct file實例。fork之后,父子進程的文件描述符關系如圖4-10所示。
進程的創建之vfork()在早期的實現中,fork沒有實現寫時拷貝機制,而是直接對父進程的數據段、堆和棧進行完全拷貝,效率十分低下。很多程序在fork一個子進程后,會緊接著執行exec家族函數,這更是一種浪費。所以BSD引入了vfork。既然fork之后會執行exec函數,拷貝父進程的內存數據就變成了一種無意義的行為,所以引入的vfork壓根就不會拷貝父進程的內存數據,而是直接共享。再后來Linux引入了寫時拷貝的機制,其效率提高了很多,這樣一來,vfork其實就可以退出歷史舞臺了。除了一些需要將性能優化到極致的場景,大部分情況下不需要再使用vfork函數了。vfork會創建一個子進程,該子進程會共享父進程的內存數據,而且系統將保證子進程先于父進程獲得調度。子進程也會共享父進程的地址空間,而父進程將被一直掛起,直到子進程退出或執行exec。
注意,vfork之后,子進程如果返回,則不要調用return,而應該使用_exit函數。如果使用return,就會出現詭異的錯誤。請看下面的示例代碼:#include<stdio.h> #include <stdlib.h> #include <unistd.h> int glob = 88 ; int main(void) { int var; var = 88; pid_t pid; if ((pid = vfork()) < 0) { printf("vfork error"); exit(-1); } else if (pid == 0) { /* 子進程 */ var++; glob++; return 0; }printf("pid=%d, glob=%d, var=%d\n",getpid(), glob, var); return 0; } 調用子進程,如果使用return返回,就意味著main函數返回了,因為棧是父子進程共享的,所以程序的函數棧發生了變化。main函數return之后,通常會調用exit系的函數,父進程收到子進程的exit之后,就會開始從vfork返回,但是這時整個main函數的棧都已經不復存在了,所以父進程壓根無法執行。于是會返回一個詭異的棧地址,對于在某些內核版本中,進程會直接報棧錯誤然后退出,但是在某些內核版本中,有可能就會再次進出main,于是進入一個無限循環,直到vfork返回錯誤。筆者的Ubuntu版本就是后者。返回。一般來說,vfork創建的子進程會執行exec,執行完exec后應該調用_exit,注意是_exit而不是exit。因為exit會導致父進程stdio緩沖區的沖刷和關閉。我們會在后面講述exit和_exit的區別。
來自為知筆記(Wiz)
從2.6.24起,Linux采用完全公平調度(Completely Fair Scheduler,CFS)。用戶創建的普通進程,都采用CFS調度策略。對于CFS調度策略,procfs提供了如下控制選項:
fork之后父子進程的內存關系fork之后的子進程完全拷貝了父進程的地址空間,包括棧、堆、代碼段等。通過下面的示例代碼,我們一起來查看父子進程的內存關系:
前文提到過,子進程和父進程執行一模一樣的代碼的情形比較少見。Linux提供了execve系統調用,構建在該系統調用之上,glibc提供了exec系列函數。這個系列函數會丟棄現存的程序代碼段,并構建新的數據段、棧及堆。調用fork之后,子進程幾乎總是通過調用exec系列函數,來執行新的程序。在這種背景下,fork時子進程完全拷貝父進程的數據段、棧和堆的做法是不明智的,因為接下來的exec系列函數會毫不留情地拋棄剛剛辛苦拷貝的內存。為了解決這個問題,Linux引入了寫時拷貝(copy-on-write)的技術。寫時拷貝是指子進程的頁表項指向與父進程相同的物理內存頁,這樣只拷貝父進程的頁表項就可以了,當然要把這些頁面標記成只讀(如圖4-4所示)。如果父子進程都不修改內存的內容,大家便相安無事,共用一份物理內存頁。但是一旦父子進程中有任何一方嘗試修改,就會引發缺頁異常(page fault)。此時,內核會嘗試為該頁面創建一個新的物理頁面,并將內容真正地復制到新的物理頁面中,讓父子進程真正地各自擁有自己的物理內存頁,然后將頁表中相應的表項標記為可寫。從上面的描述可以看出,對于沒有修改的頁面,內核并沒有真正地復制物理內存頁,僅僅是復制了父進程的頁表。這種機制的引入提升了fork的性能,從而使內核可以快速地創建一個新的進程。查看下copy_one_pte函數中有如下代碼:
父子進程共用了一套文件偏移量
文件描述符還有一個文件描述符標志(file descriptor flag)。目前只定義了一個標志位:FD_CLOSEXEC,這是close_on_exec標志位。細心閱讀open函數手冊也會發現,open函數也有一個類似的標志位,即O_CLOSEXEC,該標志位也是用于設置文件描述符標志的。那么這個標志位到底有什么作用呢?如果文件描述符中將這個標志位置位,那么調用exec時會自動關閉對應的文件??墒菫槭裁葱枰@個標志位呢?主要是出于安全的考慮。對于fork之后子進程執行exec這種場景,如果子進程可以操作父進程打開的文件,就會帶來嚴重的安全隱患。一般來講,調用exec的子進程時,因為它.會另起爐灶,因此父進程打開的文件描述符也應該一并關閉,但事實上內核并沒有主動這樣做。試想如下場景,Webserver首先以root權限啟動,打開只有擁有root權限才能打開的端口和日志等文件,再降到普通用戶,fork出一些worker進程,在進程中進行解析腳本、寫日志、輸出結果等操作。由于子進程完全可以操作父進程打開的文件,因此子進程中的腳本只要繼續操作這些文件描述符,就能越權操作root用戶才能操作的文件。為了解決這個問題,Linux引入了close on exec機制。設置了FD_CLOSEXEC標志位的文件,在子進程調用exec家族函數時會將相應的文件關閉。而設置該標志位的方法有兩種:
·open時,帶上O_CLOSEXEC標志位?!pen時如果未設置,那就在后面調用fcntl函數的F_SETFD操作來設置。建議使用第一種方法。原因是第二種方法在某些時序條件下并不那么絕對的安全??紤]圖4-7的場景:Thread 1還沒來得及將FD_CLOSEXEC置位,由于Thread 2已經執行過fork,這時候fork出來的子進程就不會關閉相應的文件。盡管Thread1后來調用了fcntl的F_SETFD操作,但是為時已晚,文件已經泄露了。注意 圖4-7中,多線程程序執行了fork,僅僅是為了示意,實際中并不鼓勵這種做法。正相反,這種做法是十分危險的。多線程程序不應該調用fork來創建子進程,第8章會分析具體原因。前面提到,執行fork時,子進程會獲取父進程所有文件描述符的副本,但是測試結果表明,父子進程共享了文件的很多屬性。這到底是怎么回事?讓我們深入內核一探究竟。在內核的進程描述符task_struct結構體中,與打開文件相關的變量如下所示:
Linux系統假設大多數的進程打開的文件不會太多。于是Linux選擇了一個long類型的位數(32位系統下為32位,64位系統下為64位)作為經驗值。以64位系統為例,file_struct結構體自帶了可以容納64個struct file類型指針的數組fd_array,也自帶了兩個大小為64的位圖,其中open_fds_init位圖用于記錄文件的打開情況,close_on_exec_init位圖用于記錄文件描述符的FD_CLOSEXCE標志位是否置位。只要進程打開的文件個數小于64,file_struct結構體自帶的指針數組和兩個位圖就足以滿足需要。因此在分配了file_struct結構體后,內核會初始化file_struct自帶的fdtable,代碼如下所示:
接下來,內核會檢查父進程打開文件的個數。如果父進程打開的文件超過了64個,struct files_struct中自帶的數組和位圖就不能滿足需要了。這種情況下內核會分配一個新的struct fdtable,代媽如下:
無論是使用file_struct結構體自帶的fdtable,還是使用alloc_fdtable分配的fdtable,接下來要做的事情都一樣,即將父進程的兩個位圖信息和打開文件的struct file類型指針拷貝到子進程的對應數據結構中,代碼如下:
進程的創建之vfork()在早期的實現中,fork沒有實現寫時拷貝機制,而是直接對父進程的數據段、堆和棧進行完全拷貝,效率十分低下。很多程序在fork一個子進程后,會緊接著執行exec家族函數,這更是一種浪費。所以BSD引入了vfork。既然fork之后會執行exec函數,拷貝父進程的內存數據就變成了一種無意義的行為,所以引入的vfork壓根就不會拷貝父進程的內存數據,而是直接共享。再后來Linux引入了寫時拷貝的機制,其效率提高了很多,這樣一來,vfork其實就可以退出歷史舞臺了。除了一些需要將性能優化到極致的場景,大部分情況下不需要再使用vfork函數了。vfork會創建一個子進程,該子進程會共享父進程的內存數據,而且系統將保證子進程先于父進程獲得調度。子進程也會共享父進程的地址空間,而父進程將被一直掛起,直到子進程退出或執行exec。
注意,vfork之后,子進程如果返回,則不要調用return,而應該使用_exit函數。如果使用return,就會出現詭異的錯誤。請看下面的示例代碼:
來自為知筆記(Wiz)
轉載于:https://www.cnblogs.com/zengyiwen/p/5755193.html
總結
以上是生活随笔為你收集整理的Linux进程的创建函数fork()及其fork内核实现解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: hdu 2191 多重背包
- 下一篇: iOS中正则表达式的使用