vfork掛掉的一個問題

在知乎上,有我的問了這樣的一個問題——爲何vfork的子進程裏用return,整個程序會掛掉,並且exit()不會?並給出了以下的代碼,下面的代碼一運行就掛掉了,但若是把子進程的return改爲exit(0)就沒事。html

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

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

#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的差異:shell

  • fork 是 建立一個子進程,並把父進程的內存數據copy到子進程中。less

  • vfork是 建立一個子進程,並和父進程的內存數據share一塊兒用。函數

這兩個的差異是,一個是copy,一個是share。(關於fork,能夠參看酷殼以前的《一道fork的面試題》)性能

你 man vfork 一下,你能夠看到,vfork是這樣的工做的,優化

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

那麼,爲何要幹出一個vfork這個玩意? 緣由在man page也講得很清楚了:this

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 memory 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() 沒有修改函數棧,因此,父進程得以順利執行

關於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,除非你很是關注性能。

(全文完)

相關文章
相關標籤/搜索