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

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

exit(main(argc, argv))

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

#include &
#include &
#include &

int glob = 6;

int
main(void)
{
int var;
pid_t pid;
var = 88;
printf("before vfork
");
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 ", getpid(), glob, var); return 0; //exit(0); }

編譯後執行會導致 core-dump,但是將 return 改為 exit 後卻不會

#include &
#include &
#include &

int glob = 6;

int
main(void)
{
int var;
pid_t pid;
var = 88;
printf("before vfork
");
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 ", getpid(), glob, var); //return 0; exit(0); }

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

在此謝謝了。


基礎知識

首先說一下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創建子進程實際是一個複製父進程的過程。所以更貼切的說法是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]


return是給函數一個返回值,exit則是讓操作系統把一個數字留下來然後自殺。一個是語言提供的,一個是操作系統提供的,顯然不可比較。


很多人都在回答中指出了return與exit()之間的區別,我也表示贊同,但我對題主所遭遇的問題是否是由這種區別所引起的表示懷疑。

在我的測試環境下(Win8.1+MSYS2提供的GCC),使用return並沒有發生錯誤。


main函數並不是開始,之前有個函數(至少gcc是這樣,別的不懂),姑且叫start,start函數里初始化一些我也不知道是什麼的貌似並沒有什麼卵用的東西,然後調用main,

return是函數的返回,main的return就返回到start,

exit是退出進程,善後,然後自殺,參數是返回到調用進程的,比如shell,

-----

區別就在於兩者的善後工作是不一樣的,

return會把函數棧空間釋放掉,

exit不會,

vfork是空間共享的,一個的main釋放了,另一個的main就走投無路了,

----

唔,好像有點問題,我再看看,


實際上子進程return只會導致子進程自己的相關寄存器(特別是esp、ebp)被更改,不會對共享的棧內存空間的內容產生影響。而子進程修改自己的寄存器並不影響父進程的寄存器,所以本來return是不會導致問題的。

問題出在當子進程return返回到main()函數的調用點之後,接下來的語句中會有一些別的函數調用,這個時候main()原來的棧內存空間內容被覆蓋(主要是返回地址),導致了程序的不確定行為。利用這個特點可以玩個把戲:

#include &
#include &

void stack1() {
vfork();
}

void stack2() {
_exit(0);
}

int main() {

stack1();

printf("%d goes 1
", getpid());
stack2();

printf("%d goes 2
", getpid());
return 0;
}

正常情況下這個程序將會正確執行不會發生錯誤,如果父進程pid為1000,子進程pid為1001,那麼輸出將會是:

1001 goes 1

1000 goes 2

這是因為父進程的當前函數棧空間被子進程從stack1替換成了stack2,父進程從stack1返回時,實際上是從stack2()返回。

linux man手冊里明確說明vfork()之後,子進程只應該調用_exit()或者exec函數族,甚至調用exit()都是不正確的!


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


說白了return是return給調用main函數的函數,讓該函數在得知main函數返回後,開始除了main的後事,例如處理main佔用的資源,例如關閉打開的文件,進程間通信工具,刷新起std緩存,等。而exit()是對系統調用_exit()的封裝,函數可以return給操作系統由相應的處理程序調用exit或者程序自己直接調用exit


前面大神們寫得太複雜了,沒有本質上的區別。

使用exit函數可以提前返回,不需你做控制讓它走到main函數的return語句。


大部分答案都進的去,出不來。這個問題清楚的回答應該這樣分層:

1. return是函數分層,exit()是函數調用。

2. 大部分操作系統承諾,主函數返回,進程完成清理工作後退出,並返回主函數的返回值。而根據POSIX標準,exit()系統調用將直接導致進程退出,可以不進入系統庫的析構函數。

我們寫高級語言的程序,要工作在語意的層面上,不是工作在實現的層面上,這是入門常識。國內很多軟體寫得爛,就是因為廣泛的「能跑就行」的思路導致的。


return 和 exit 的區別上面已經講得很清楚了,system call 和 function call 的區別。至於為什麼兩個一起return 不行,我的理解是這樣的:

vfork 的child process 和 parent process 是共享一切資源,memory and stack,並且parent process要等到child process exit之後才會繼續。所以如果child process直接return, 等同於parent proces return,當然是沒有什麼問題的。但是如果parent process 再次return, 系統就迷惑了。

更專業的解釋請看這裡:

vfork(), just like fork(2), creates a child process of the calling process. For details and return value and errors, see fork(2).

vfork() is a special case of clone(2). It is used to create new processes without copying the page tables of the parent process. It may be useful in performance-sensitive applications where a child is created which then immediately issues anexecve(2).

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).

reference: http://linux.die.net/man/2/vfork


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

每個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返回,控制會傳給系統


推薦閱讀:

為何linux作為伺服器端十年不重啟都不卡而安卓用半年就十分卡?
2017年6月19-20日在北京舉行的 LinuxCon 會議有哪些看點?
linux為什麼需要內核棧,系統調用時直接使用用戶棧不行嗎?
裝載著操作系統的磁碟是默認裝到內存0x8000位置,那ORG 0x7c00又是在做一個什麼事情呢?
linux內核啟動關於先有雞再有蛋的問題?

TAG:C編程語言 | Linux內核 | UNIX環境高級編程書籍 |