delphij's Chaos

选择chaos这个词是因为~~实在很难找到一个更合适的词来形容这儿了……

14 May 2023

C main() 的 exit() 和 return

这里讨论一个犀利而无用的细节问题。事情的缘起是有人在 GitHub 上提了一个 pull request 要求把许多程序的 main() 的终结部分从 exit(X) 改为 return X;,我反对了这一变动。

值得注意的是,在实践上,从 mainreturn 和调用 exit(3) 几乎等效的(此处还是有细微差别, 后面将会讨论),原因是 C 运行环境库的启动部分(这部分会在连接过程中嵌入到可执行文件中, FreeBSD 的实现中,这部分位于 lib/libc/csu/libc_start1.c__libc_start1

void
__libc_start1(int argc, char *argv[], char *env[], void (*cleanup)(void),
    int (*mainX)(int, char *[], char *[]))
{
/* ... */
	exit(mainX(argc, argv, env));
}

但是目前大部分的用户态代码和 style(9) 均采用了调用 exit(3) 而不是 return

前面提到,两者之间存在些许的不同:当函数返回时,属于它的栈帧就销毁掉了。而当函数调用另外一个函数时, 属于它的栈帧依然存在。在追溯内存泄漏时,比较常见的办法是从有效栈帧开始沿着所有指针遍历并标记内存块, 这样,在完成时没有标记的已分配内存块就是泄漏的内存了。习惯上,C 程序在已经知道要调用 exit(3) 或其它终止进程的函数(部分编译器支持将这类函数标记为 __attribute__((__noreturn__)) 属性, 在 FreeBSD 中这类函数通常会用 __dead2 来标记)时,在此前分配的内存无需再做显式地释放。 因此,如果在 main() 中在堆上分配(例如,通过 malloc(3)) 了一些缓冲区而在 return 时没有释放,则这些缓冲区有可能被视作内存泄漏, 尽管实践上内核最终仍然会回收进程的全部资源,但相关的警告有可能给开发人员带来困扰。

不过,这些差异并不总是存在。例如,程序的 main() 函数可能完全不从堆上分配内存, 那么此时两者的区别就真的可以忽略不计了。在写新程序时,我认为两者基本上没什么区别, 个人比较倾向于使用 exit,除非 main() 由于某些原因可能在其他地方由 C 运行环境以外的其它地方调用(一个比较典型的例子是嵌入 sh(1) 的那些命令: 如果这些命令的实现中使用了 exit(),那么嵌入的版本就必须 fork()wait(), 而不是简单地像调用函数那样使用它们,这会让 shell 脚本的性能大幅下降), 但对没有这类要求,又没有在堆上分配内存的新程序来说,两种写法都可以接受。

今天从 Thomas Yao 那里 得知陈皓 @haoel 老师已于当地时间周六晚因突发心梗辞世, 在此让我们一起缅怀他。