vfork使用return導(dǎo)致進(jìn)程掛掉引發(fā)的思考

本文轉(zhuǎn)載自#:https://coolshell.cn/articles/12103.html
代碼如下

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
    int var;
    var = 88; 
    pid_t pid;

    if ((pid = vfork()) < 0) {
        printf("vfork error");
        exit(-1);
    } else if (pid == 0) { /* 子進(jìn)程 */
        var++;
        return 0;
    }   
    printf("pid=%d, var=%d\n", getpid(), var);
    return 0;
}

運(yùn)行結(jié)果

tangsx@:~/code/unix/src$ ./fork4
pid=6255, var=-1636802491
fork4: cxa_atexit.c:100: __new_exitfn: Assertion `l != NULL' failed.
已放棄 (核心已轉(zhuǎn)儲)

基礎(chǔ)知識:

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

  • fork 是 創(chuàng)建一個(gè)子進(jìn)程,并把父進(jìn)程的內(nèi)存數(shù)據(jù)copy到子進(jìn)程中。
  • vfork是 創(chuàng)建一個(gè)子進(jìn)程,并和父進(jìn)程的內(nèi)存數(shù)據(jù)share一起用。

這兩個(gè)的差別是,一個(gè)是copy,一個(gè)是share。(關(guān)于fork,可以參看酷殼之前的《一道fork的面試題》)

你 man vfork 一下,你可以看到,vfork是這樣的工作的 :
1)保證子進(jìn)程先執(zhí)行。
2)當(dāng)子進(jìn)程調(diào)用exit()或exec()后,父進(jìn)程往下執(zhí)行。

vfork() differs from fork(2) in that the calling thread is suspended until the child terminates (either normally, by calling _exit(2), or abnormally, after delivery of a fatal signal), or it makes a call to execve(2). Until that point, the child shares all memory with its parent, including the stack. The child must not return from the current function or call exit(3), but may call _exit(2).

那么,為什么要干出一個(gè)vfork這個(gè)玩意? 原因在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 afterward 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 were held in a register.

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

為什么return會掛掉,exit()不會?

從上面我們知道,結(jié)束子進(jìn)程的調(diào)用是exit()而不是return,如果你在vfork中return了,那么,這就意味main()函數(shù)return了,注意因?yàn)楹瘮?shù)棧父子進(jìn)程共享,所以整個(gè)程序的棧就跪了。

如果你在子進(jìn)程中return,那么基本是下面的過程:

1)子進(jìn)程的main() 函數(shù) return了,于是程序的函數(shù)棧發(fā)生了變化。
2)而main()函數(shù)return后,通常會調(diào)用 exit()或相似的函數(shù)(如:_exit(),exitgroup())
3)這時(shí),父進(jìn)程收到子進(jìn)程exit(),開始從vfork返回,但是尼瑪,老子的棧都被你子進(jìn)程給return干廢掉了,你讓我怎么執(zhí)行?(注:棧會返回一個(gè)詭異一個(gè)棧地址,對于某些內(nèi)核版本的實(shí)現(xiàn),直接報(bào)“棧錯(cuò)誤”就給跪了,然而,對于某些內(nèi)核版本的實(shí)現(xiàn),于是有可能會再次調(diào)用main(),于是進(jìn)入了一個(gè)無限循環(huán)的結(jié)果,直到vfork 調(diào)用返回 error)

好了,現(xiàn)在再回到 return 和 exit,return會釋放局部變量,并彈棧,回到上級函數(shù)執(zhí)行。exit直接退掉。如果你用c++ 你就知道,return會調(diào)用局部對象的析構(gòu)函數(shù),exit不會。(注:exit不是系統(tǒng)調(diào)用,是glibc對系統(tǒng)調(diào)用 _exit()或_exitgroup()的封裝)

可見,子進(jìn)程調(diào)用exit() 沒有修改函數(shù)棧,所以,父進(jìn)程得以順利執(zhí)行。

但是!注意!如果你調(diào)用 exit() 函數(shù),還是會有問題的,正確的方法應(yīng)該是調(diào)用 _exit() 函數(shù),因?yàn)?exit() 函數(shù) 會 flush 并 close 所有的 標(biāo)準(zhǔn) I/O ,這樣會導(dǎo)致父進(jìn)程受到影響。(這個(gè)情況在fork下也會受到影響,會導(dǎo)致一些被buffer的數(shù)據(jù)被flush兩次,這里可以參看《一個(gè)fork的面試題》)

關(guān)于fork的優(yōu)化

很明顯,fork太重,而vfork又太危險(xiǎn),所以,就有人開始優(yōu)化fork這個(gè)系統(tǒng)調(diào)用。優(yōu)化的技術(shù)用到了著名的寫時(shí)拷貝(COW)。也就是說,對于fork后并不是馬上拷貝內(nèi)存,而是只有你在需要改變的時(shí)候,才會從父進(jìn)程中拷貝到子進(jìn)程中,這樣fork后立馬執(zhí)行exec的成本就非常小了。所以,Linux的Man Page中并不鼓勵(lì)使用vfork() --

Some consider the semantics of vfork() to be an architectural blemish, and the 4.2BSD man page stated: "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 又把傳統(tǒng)的vfork給撿了回來,說是vfork的性能在 Pentium Pro 200MHz 的機(jī)器(這機(jī)器好古董?。┥嫌锌梢蕴岣邘酌腌姷男阅?。詳情見——“NetBSD Documentation: Why implement traditional vfork()

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容