VFORK 掛掉的一個問題

在知乎上,有個人問了這樣的一個問題——爲什麼vfork的子進程裏用return,整個程序會掛掉,而且exit()不會?並給出瞭如下的代碼,下面的代碼一運行就掛掉了,但如果把子進程的return改成exit(0)就沒事。

我受邀後本來不想回答這個問題的,因爲這個問題明顯就是RTFM的事,後來,發現這個問題放在那裏好長時間,而掛在下面的幾個答案又跑偏得比較嚴重,我覺得可能有些朋友看到那樣的答案會被誤導,所以就上去回答了一下這個問題。

下面我把問題和我的回答發佈在這裏,也供更多的人查看。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
    int var;
    var = 88;
    if ((pid = vfork()) < 0) {
        printf("vfork error");
        exit(-1);
    } else if (pid == 0) { /* 子進程 */
        var++;
        return 0;
    }
    printf("pid=%d, glob=%d, var=%d\n", getpid(), glob, var);
    return 0;
}

基礎知識

首先說一下fork和vfork的差別:

  • fork 是 創建一個子進程,並把父進程的內存數據copy到子進程中。
  • vfork是 創建一個子進程,並和父進程的內存數據share一起用。

這兩個的差別是,一個是copy,一個是share。(關於fork,可以參看酷殼之前的《一道fork的面試題》

你 man vfork 一下,你可以看到,vfork是這樣的工作的:

  • 保證子進程先執行。
  • 當子進程調用exit()或exec()後,父進程往下執行。

那麼,爲什麼要幹出一個vfork這個玩意? 原因在man page也講得很清楚了:

Historic Description

Under Linux, fork(2) is implemented using copy-on-write pages, so the only penalty incurred by fork(2) is the time and memory required to duplicate the parent’s page tables, and to create a unique task structure for the child. However, in the bad old days a fork(2) would require making ****a complete copy of the caller’s data space, often needlessly, since usually immediately afterwards an exec(3) is done. Thus, for greater efficiency, BSD introduced the vfork() system call, which did not fully copy the address space of the parent process, but borrowed the parent’s mem****ory and thread of control until a call to execve(2) or an exit occurred. The parent process was suspended while the child was using its resources. The use of vfork() was tricky: for example, not modifying data in the parent process depended on knowing which variables are held in a register.

意思是這樣的—— 起初只有fork,但是很多程序在fork一個子進程後就exec一個外部程序,於是fork需要copy父進程的數據這個動作就變得毫無意了,而且這樣幹還很重(注:後來,fork做了優化,詳見本文後面),所以,BSD搞出了個父子進程共享的 vfork,這樣成本比較低。因此,vfork本就是爲了exec而生。

爲什麼return會掛掉,exit()不會?

從上面我們知道,結束子進程的調用是exit()而不是return,如果你在vfork中return了,那麼,這就意味main()函數return了,注意因爲函數棧父子進程共享,所以整個程序的棧就跪了。

如果你在子進程中return,那麼基本是下面的過程:

  1. 子進程的main() 函數 return了,於是程序的函數棧發生了變化。
  2. 而main()函數return後,通常會調用 exit()或相似的函數(如:_exit(),exitgroup())
  3. 這時,父進程收到子進程exit(),開始從vfork返回,但是尼瑪,老子的棧都被你子進程給return幹廢掉了,你讓我怎麼執行?(注:棧會返回一個詭異一個棧地址,對於某些內核版本的實現,直接報“棧錯誤”就給跪了,然而,對於某些內核版本的實現,於是有可能會再次調用main(),於是進入了一個無限循環的結果,直到vfork 調用返回 error)

好了,現在再回到 return 和 exit,return會釋放局部變量,並彈棧,回到上級函數執行。exit直接退掉。如果你用c++ 你就知道,return會調用局部對象的析構函數,exit不會。(注:exit不是系統調用,是glibc對系統調用 _exit()或_exitgroup()的封裝)

可見,子進程調用exit() 沒有修改函數棧,所以,父進程得以順利執行

但是!注意!如果你調用 exit() 函數,還是會有問題的,正確的方法應該是調用 _exit() 函數,因爲 exit() 函數 會 flush 並 close 所有的 標準 I/O ,這樣會導致父進程受到影響。(這個情況在fork下也會受到影響,會導致一些被buffer的數據被flush兩次,這裏可以參看《一個fork的面試題》

關於fork的優化

很明顯,fork太重,而vfork又太危險,所以,就有人開始優化fork這個系統調用。優化的技術用到了著名的寫時拷貝(COW)

也就是說,對於fork後並不是馬上拷貝內存,而是隻有你在需要改變的時候,纔會從父進程中拷貝到子進程中,這樣fork後立馬執行exec的成本就非常小了。所以,Linux的Man Page中並不鼓勵使用vfork() ——

“ It is rather unfortunate that Linux revived this specter from the past. The BSD man page states: “This system call will be eliminated when proper system sharing mechanisms are implemented. Users should not depend on the memory sharing semantics of vfork() as it will, in that case, be made synonymous to fork(2).””

於是,從BSD4.4開始,他們讓vfork和fork變成一樣的了

但在後來,NetBSD 1.3 又把傳統的vfork給撿了回來,說是vfork的性能在 Pentium Pro 200MHz 的機器(這機器好古董啊)上有可以提高几秒鐘的性能。詳情見——“NetBSD Documentation: Why implement traditional vfork()

今天的Linux下,fork和vfork還是各是各的,不過,還是建議你不要用vfork,除非你非常關注性能。

作者:陳皓
鏈接:https://coolshell.cn/articles/12103.html
來源:酷 殼 – COOLSHELL
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章