日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

Linux进程的创建函数fork()及其fork内核实现解析

發布時間:2025/4/14 linux 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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標志位。·open時如果未設置,那就在后面調用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)

    轉載于:https://www.cnblogs.com/zengyiwen/p/5755193.html

    總結

    以上是生活随笔為你收集整理的Linux进程的创建函数fork()及其fork内核实现解析的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。