C語言裏,main 函數中 return x和 exit(x)

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

2014-11-27 程序猿

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


最近讀 APUE,APUE 7.3 節中說,main 函數 return 相當於

exit(main(argc, argv))

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



#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 後卻不會

#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/


基礎知識


首先說一下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語言函數的,不在一個層面上。


來自:知乎

鏈接:http://www.zhihu.com/question/26591968




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