C 語言裏,main 函數中 return x 和 exit(x) 到底有什麼區別?

原文: http://www.zhihu.com/question/26591968linux

 

問題:C語言裏,main 函數中 return x和 exit(x) 到底有什麼區別 ?c++

 

最近讀 APUE,APUE 7.3 節中說,main 函數 return 至關於程序員

exit(main(argc, argv))

可是在實踐程序 8-2 時候出現了問題。shell

 

如:安全

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

編譯後執行會致使 core-dump,可是將 return 改成 exit 後卻不會。less

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

本人小白,求諸位高手們解惑。函數

 

在此謝謝了。性能

 

陳皓,酷殼:http://coolshell.cn/優化

 

基礎知識ui

 

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

 

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

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

 

這兩個的差異是,一個是copy,一個是share。


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

 

  1. 保證子進程先執行。

  2. 當子進程調用exit()或exec()後,父進程往下執行。

 

那麼,爲何要幹出一個vfork這個玩意? 緣由是這樣的—— 起初只有fork,可是不少程序在fork一個子進程後就exec一個外部程序,因而fork須要copy父進程的數據這個動做就變得毫無心了,並且還很重,因此,搞出了個父子進程共享的vfork。因此,vfork本就是爲了exec而生。

 

爲何return會掛掉,exit()不會?

 

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

 

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

 

  1. 子進程的main() 函數 return了

  2. 而main()函數return後,一般會調用 exit()或類似的函數(如:exitgroup())

  3. 這時,父進程收到子進程exit(),開始從vfork返回,可是尼瑪,老子的棧都被你幹廢掉了,你讓我怎麼執行?(注:棧會返回一個詭異一個棧地址,對於某些內核版本的實現,直接報「棧錯誤」就給跪了,然而,對於某些內核版本的實現,因而有可能會再次調用main(),因而進入了一個無限循環的結果,直到vfork 調用返回 error)

 

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


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

 

————更新————

 

有人在評論中問,寫時拷貝呢?還說vfork產生的緣由不太對。我在這裏說明一下:

 

關於寫時拷貝(COW)。

 

就是fork後來採用的優化技術,這樣,對於fork後並非立刻拷貝內存,而是隻有你在須要改變的時候,纔會從父進程中拷貝到子進程中,這樣fork後立馬執行exec的成本就很是小了。而vfork由於共享內存因此比較危險,因此,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()」

 

關於vfork產生的緣由

 

你能夠看一下Linux 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 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.

 

孫建希,linux c 程序員

 

內核代碼分析!

 

linux建立子進程實際是一個複製父進程的過程。因此更貼切的說法是clone。linux一開始使用fork的緣由是當時clone這個詞尚未流行。 實際存在fork,clone,vfork 三個系統調用。fork是徹底複製,clone則是有選擇的複製,vfork則徹底使用父進程的資源。能夠理解vfork是建立的線程。 vfork的出現主要是爲了當即就執行exec的程序考慮的。可是後來的kernel都支持copy_on_write ,因此vfork提升效率的機制也沒有那麼明顯了。

 

內核中三個系統調用最後都是調用do_fork:

 

fork:

return do_fork(SIGCHLD, regs.esp, &regs, 0);

clone:

return do_fork(clone_flags, newsp, &regs, 0);

vfork:

return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, &regs, 0);
#define CLONE_VFORK 0x00004000  /* set if the parent wants the child to wake it up on mm_release*/
#define CLONE_VM 0x00000100  /* set if VM shared between processes */

上面兩個宏指出:

 

vfork 要求子進程執行mm_release 後喚醒 父進程, 而且共享虛擬內存

 

爲何要求子進程先行呢?

 

拿虛擬內存作比方。 進程須要有結構管理本身的虛擬內存空間, 該結構在進程 結構體 task_struct 中就是一個mm_struct 類型的指針。fork的時候內核會新建結構體,將該mm_struct 自己以及下級結構都複製一份,並設置子進程的mm_struct 指向新的內存。而vfork則只是複製了task_struct 自己,並無遞歸下去。簡單說就是:fork複製了內存,vfork複製了指針。

 

do_fork:

#define DECLARE_MUTEX_LOCKED(name) __DECLARE_SEMAPHORE_GENERIC(name,0)
DECLARE_MUTEX_LOCKED(sem);
if ((clone_flags & CLONE_VFORK) && (retval > 0))
    down(&sem);

能夠看到申明瞭信號兩sem, 並初始化爲0,也就是說當使用vfork時,父進程會睡眠。(須要說一下此時子進程已經進入就緒隊列。而且該信號量是局部變量,子進程使用的父進程的地址空間,因此也是能夠看到該局部變量的。) 子進程被調度執行時,使用的是父進程的地址空間(由於用的父進程的mm_struct 指針), 此時子進程能夠該父進程的堆棧。因此此時父子進程絕對不能同時運行。 execve和exit兩個系統調用是不退棧的,而是直接進入系統空間,將共享的地址空間分開,因此這兩個系統調用是安全的。return是會退棧的,而子進程的退棧會致使父進程的棧也被改了(應該很好理解), 因此子進程絕對不能退到父進程當前棧頂如下的地方。

 

因此開發人員注意: 子進程絕對不容許在調用vfork的函數中return,vfork就是用來調用execve的。並且該系統調用在cow後就應該禁止使用了!

 

想看的繼續:

 

execve,exit兩個系統調用會在內核調用mm_release函數,該函數會調用up操做。

void mm_release(void)
{
    struct task_struct *tsk = current;
    /* notify parent sleeping on vfork() */
    if (tsk->flags & PF_VFORK) {
        tsk->flags &= ~PF_VFORK;
        up(tsk->p_opptr->vfork_sem);
    }
}

struct task_struct {
....
unsigned long flags; /* per process flags, defined below */
struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr;
...
}

p_opptr 指向父進程的task_struct 結構。分別是 生父,養父,子進程,弟弟進程,哥哥進程。

 

劉暢

 

題主你若是反彙編一下 gcc 生成的代碼,而後對 core dump 的程序運行一下 gdb backtrace 就能夠知道這二者的差異,以及爲何 return 0 會 core dump 了。

 

反彙編後能夠發現,在 Linux+gcc+x86_64 (x86 下只要吧全部彙編指令中的 q 去掉都是同樣的) 下 return 0 生成的代碼最後執行了 retq, 這樣控制就跳轉到以前調用 main() 的那個那個 callq 指令以後,這是在函數 __libc_start_main,就是在這裏 libc 調用 main() 函數的。main() 執行完後就返回這裏。__libc_start_main 很是複雜,須要完成 libc 的一大堆功能。例如,若是你生成的是靜態連接的 a.out,那麼 __libc_start_main 會在這個函數中執行大量的操做,例如和當前的區域 LC_ALL 有關的操做(很神奇吧!)。若是是動態連接的 a.out, 那麼 __libc_start_main 調用一個全局跳轉表中的各個函數。全部的操做執行完後最終控制會轉移到 _exit(),就是操做系統提供的系統調用,操做系統(在內核態)將進程殺掉。

 

相反,若是你調用 exit() (也是在 libc 實現的, 見 [2]),最後控制轉移到 exit() 函數(也就是說不返回 __libc_start_main 了),這個函數比較簡單,它只是調用一個簡單的函數 __run_exit_handlers, 這個函數按順序執行 atexit() 註冊的退出函數,而後直接調用 _exit()。

 

因爲你在上面 fork 子進程的時候使用的是 vfork,vfork 是沒有 copy-on-write 的。這樣父進程的 image 是和子進程共享的。父進程一旦退出,那麼子進程就沒有 image 了,這樣訪問父進程的數據就會致使頁異常。

 

因爲exit() 函數調用的 __run_exit_handlers 一) 比較簡單 (看 [2] 中的代碼),二) 空指針不是強行報錯而是默默的忽略(看代碼),這樣作沒有形成問題,__libc_start_main 就不同了。

 

當動態連接 a.out 時 gdb backtrace 返回的結果是:

#0  0x00007ffff7a6b967 in raise () from /usr/lib/libc.so.6
#1  0x00007ffff7a6cd3a in abort () from /usr/lib/libc.so.6
#2  0x00007ffff7a648ad in __assert_fail_base () from /usr/lib/libc.so.6
#3  0x00007ffff7a64962 in __assert_fail () from /usr/lib/libc.so.6
#4  0x00007ffff7a6e4ca in __new_exitfn () from /usr/lib/libc.so.6
#5  0x00007ffff7a6e549 in __cxa_atexit_internal () from /usr/lib/libc.so.6
#6  0x00007ffff7a57fa3 in __libc_start_main () from /usr/lib/libc.so.6
#7  0x0000000000400559 in _start ()

結合 glibc 的代碼 [1], 能夠看到錯誤發生在 __libc_start_main 試圖執行在 atexit() 中註冊的函數。事實上你能夠在代碼的前面加入 atexit() 註冊一個 exit callback function, 這時你能夠看到這個函數只被執行了一次。而若是你使用 fork() 這個函數被執行兩次。這代表錯誤就是在 __libc_start_main 試圖執行 atexit 註冊的函數時發生的。

 

運行 a.out 提示的錯誤

a.out: cxa_atexit.c:100: __new_exitfn: Assertion `l != ((void *)0)' failed.

是在上面代碼的 90 行產生的(我機器裏的glibc版本不同,因此顯示的位置是100行,都是差很少的)。

 

當靜態連接 a.out 時 gdb backtrace 返回的結果是:

#0  0x000000000043f6a7 in raise ()
#1  0x000000000040609a in abort ()
#2  0x000000000040978f in __libc_message ()
#3  0x00000000004097ac in __libc_fatal ()
#4  0x0000000000400f21 in __libc_start_main ()
#5  0x0000000000400c1c in _start ()

此次錯誤發生的更靠前,在 __libc_start_main 中就發生了錯誤。我沒有去查代碼,題主有興趣能夠去查一查具體是哪一行出錯了。

 

求贊。。。

 

[1] fxr.watson.org: GLIBC27 sys/stdlib/cxa_atexit.c

[2] exit.c [glibc/stdlib/exit.c]

 

徐麗,Unix世界的妹子

 

前面的答題很好了,可是不容易理解,簡單點說:

 

每一個C程序的入口點_start處的代碼用僞代碼表示爲

 

_start:

 

call __libc_init_first // 一些初始化

call _init
call atexit
call main
call _exit

 

從僞代碼就看出來了,每一個C程序都要在執行一些初始化函數後對main調用,若main末尾爲return語句,那麼控制返回,最終會call _exit,把控制返回系統。若省略return,那麼也將會call _exit。若是代碼中有exit函數,那麼會先執行atexit註冊的函數,進而執行_exit()把控制還給操做系統。

 

總之,這些狀況下,當main返回,控制會傳給系統

 

SCrip,業餘IT

 

exit是操做系統的,return是c語言函數的,不在一個層面上。

相關文章
相關標籤/搜索