fork()函數的底層實現原理

在之前的博客 進程控制【創建、等待、終止和替換】 - CSDN博客 https://blog.csdn.net/qq_37964547/article/details/79720027中只是簡單的講到了fork函數,但並沒有深入,今天在複習知識的時候,就把fork函數重新學習了一遍,做了一下總結。
在Linux中,fork函數是非常重要的函數,他從一個已存在的進程中創建一個新進程;新進程爲子進程,而原進程稱爲父進程。接下來我們一起來系統的解析一下fork函數的調用原理:

1、fork函數原型

#include<unistd.h>
pid_t fork(void)

返回值:
pid_t 是進程描述符,實質就是一個int,如果fork函數調用失敗,返回一個負數,調用成功則返回兩個值:0和子進程ID。
函數功能:
以當前進程作爲父進程創建出一個新的子進程,並且將父進程的所有資源拷貝給子進程,這樣子進程作爲父進程的一個副本存在。父子進程幾乎時完全相同的,但也有不同的如父子進程ID不同。
需要注意的是:
當fork系統調用成功時,它會返回兩個值:一個是0,另一個是所創建的新的子進程的ID(>0)。當fork成功調用後此時有兩個數據相同的父子進程,我們可以通過fork的返回值來判斷接下來程序是在執行父進程還是子進程。

  • id==0:執行子進程
  • id>0:在父進程中執行
  • id<0:fork函數調用失敗

2、fork函數的底層實現原理

  • fork()系統調用通過複製一個現有進程來創建一個全新的進程。進程被存放在一個叫做任務隊列的雙向循環鏈表當中,鏈表當中的每一項都是類型爲task_struct稱爲進程描述符的結構,也就是我們寫過的進程PCB.

  • Tips:內核通過一個位置的進程標識值或PID來標識每一個進程。//最大值默認爲32768,short int短整型的最大值.,他就是系統中允許同時存在的進程最大的數目。
    可以到目錄 /proc/sys/kernel中查看pid_max:
    這裏寫圖片描述
    當進程調用fork後,當控制轉移到內核中的fork代碼後,內核會做4件事情:
    1、分配新的內存塊和內核數據結構給子進程

    2、將父進程部分數據結構內容拷貝至子進程

    3、添加子進程到系統進程列表當中

    4、fork返回,開始調度器調度
    在這裏有一個疑問,那麼fork函數在底層到底做了什麼呢?
    Linux平臺通過clone()系統調用實現fork()。 fork(),vfork()和clone()庫函數都根據各自需要的參數標誌去調用clone(),然後由clone()去調用do_fork(), 再然後do_fork()完成了創建中的大部分工作,該函數調用copy_process().做最後的那部分工作。具體的圖解是借用一位寫的非常好的大神的博客https://blog.csdn.net/Dawn_sf/article/details/78709839
    這裏寫圖片描述
    那麼fork函數爲什麼是一次調用,卻返回了兩次呢?

    • 當程序執行到下面的語句: pid=fork();
    • 由於在複製時複製了父進程的堆棧段,所以兩個進程都停留在fork函數中,等待返回。因此fork函數會返回兩次,一次是在父進程中返回,另一次是在子進程中返回,這兩次的返回值是不一樣的。
      fork調用的一個奇妙之處就是它僅僅被調用一次,卻能夠返回兩次,它可能有三種不同的返回值:

      1)在父進程中,fork返回新創建子進程的進程ID;
      2)在子進程中,fork返回0;
      3)如果出現錯誤,fork返回一個負值。

    • 我們可以通過fork返回的值來判斷當前進程是子進程還是父進程。通俗的解釋,可以這樣看待:“其實就相當於鏈表,進程形成了鏈表,父進程的fork函數返回的值指向子進程的進程id, 因爲子進程沒有子進程,所以其fork函數返回的值爲0.

    • 調用fork之後,數據、堆、棧有兩份,代碼仍然爲一份但是這個代碼段成爲兩個進程的共享代碼段都從fork函數中返回。當父子進程有一個想要修改數據或者堆棧時,兩個進程真正分裂。

    • 而且我們還需要注意的是:子進程的代碼是從fork處執行的,fork底層實現採用了COW(copy_on_write)技術—-寫入時拷貝
    • 寫時拷貝思想:父進程和子進程共享頁幀而不是複製頁幀。然而,只要頁幀被共享,它們就不能被修改,即頁幀被保護。無論父進程還是子進程何時試圖寫一個共享的頁幀,就產生一個異常,這時內核就把這個頁複製到一個新的頁幀中並標記爲可寫。原來的頁幀仍然是寫保護的:當其他進程試圖寫入時,內核檢查寫進程是否是這個頁幀的唯一屬主,如果是,就把這個頁幀標記爲對這個進程是可寫的。

3、vfork函數

既然講到了fork函數,那麼我們很容易想到另一個創建進程的函數vfork:
vfork最初是因爲fork沒有實現COW機制,而很多情況下fork之後會緊接着exec,而exec的執行相當於之前fork複製的空間全部變成了無用功,所以設計了vfork。
函數原型

#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);

函數解釋:
(1)vfork函數用於創建一個子進程,而子進程和父進程共享地址空間,fork的子進程具有獨立的地址空間
(2)vfork保證子進程先運行,在它調用exec或(exit)之後父進程纔可能被調度運行
注意的是:
vfork創建的子進程結束時需要調用exit()函數退出,如果沒有調用該函數時會出現短錯誤的;這是因爲在函數棧上面,子進程運行結束了,main的函數棧被子進程釋放了,然後父進程在使用的時候,就訪問不到了,所以一旦vfork出子進程,退出的時候需要使用exit來結束。

4、fork和vfork的區別

(1)fork:子進程拷貝父進程的代碼段和數據段
vfork:子進程和父進程共享代碼段和數據段
(2)fork中父子進程的先後運行次序不定
vfork:保證子進程先運行,子進程exit後父進程纔開始被調度運行
(3) vfork ()保證子進程先運行,在她調用exec 或exit 之後父進程纔可能被調度運行。如果在 調用這兩個函數之前子進程依賴於父進程的進一步動作,則會導致死鎖。
(4)就算fork實現了寫時拷貝,但其效率仍然沒有vfork高,但是vfork在一般平臺上都存在問題,所以一般不推薦使用

發佈了115 篇原創文章 · 獲贊 74 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章