VFORK 掛掉的一個(gè)問(wèn)題

在知乎上,有個(gè)人問(wèn)了這樣的一個(gè)問(wèn)題——為什么vfork的子進(jìn)程里用return,整個(gè)程序會(huì)掛掉,而且exit()不會(huì)?并給出了如下的代碼,下面的代碼一運(yùn)行就掛掉了,但如果把子進(jìn)程的return改成exit(0)就沒(méi)事。

我受邀后本來(lái)不想回答這個(gè)問(wèn)題的,因?yàn)檫@個(gè)問(wèn)題明顯就是RTFM的事,后來(lái),發(fā)現(xiàn)這個(gè)問(wèn)題放在那里好長(zhǎng)時(shí)間,而掛在下面的幾個(gè)答案又跑偏得比較嚴(yán)重,我覺(jué)得可能有些朋友看到那樣的答案會(huì)被誤導(dǎo),所以就上去回答了一下這個(gè)問(wèn)題。

下面我把問(wèn)題和我的回答發(fā)布在這里,也供更多的人查看。

#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) { /* 子進(jìn)程 */
        var++;
        return 0;
    }
    printf("pid=%d, glob=%d, var=%d\n", getpid(), glob, var);
    return 0;
}

基礎(chǔ)知識(shí)

首先說(shuō)一下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是這樣的工作的:

  • 保證子進(jìn)程先執(zhí)行。
  • 當(dāng)子進(jìn)程調(diào)用exit()或exec()后,父進(jìn)程往下執(zhí)行。

那么,為什么要干出一個(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 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一個(gè)子進(jìn)程后就exec一個(gè)外部程序,于是fork需要copy父進(jìn)程的數(shù)據(jù)這個(gè)動(dòng)作就變得毫無(wú)意了,而且這樣干還很重(注:后來(lái),fork做了優(yōu)化,詳見(jiàn)本文后面),所以,BSD搞出了個(gè)父子進(jìn)程共享的 vfork,這樣成本比較低。因此,vfork本就是為了exec而生。

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

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

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

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

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

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

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

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

很明顯,fork太重,而vfork又太危險(xiǎn),所以,就有人開(kāi)始優(yōu)化fork這個(gè)系統(tǒng)調(diào)用。優(yōu)化的技術(shù)用到了著名的寫時(shí)拷貝(COW)

也就是說(shuō),對(duì)于fork后并不是馬上拷貝內(nèi)存,而是只有你在需要改變的時(shí)候,才會(huì)從父進(jìn)程中拷貝到子進(jìn)程中,這樣fork后立馬執(zhí)行exec的成本就非常小了。所以,Linux的Man Page中并不鼓勵(lì)使用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開(kāi)始,他們讓vfork和fork變成一樣的了

但在后來(lái),NetBSD 1.3 又把傳統(tǒng)的vfork給撿了回來(lái),說(shuō)是vfork的性能在 Pentium Pro 200MHz 的機(jī)器(這機(jī)器好古董?。┥嫌锌梢蕴岣邘酌腌姷男阅堋T斍橐?jiàn)——“NetBSD Documentation: Why implement traditional vfork()

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

作者:陳皓
鏈接:https://coolshell.cn/articles/12103.html
來(lái)源:酷 殼 – COOLSHELL
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。

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

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

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