
根据此图,进程终止方式有三种(不涉及多线程情况下),分别为:
- 进程调用_exit或_Exit(两者等价),进程立即终止,内核负责各项清理工作,如移除进程表项(自然也包括“关闭”进程的文件描述符),发送SIGCHILD信号给父进程等。
- 进程调用exit函数,exit逐一调用进程事先通过atexit或onexit注册的exit handler,然后清理stdio,最后调用_exit。
- 从main函数return,然后start-up例程(在main函数之前执行的代码,与操作系统有关,会完成为进程提供环境变量/命令行参数等工作)调用exit函数。
不难看出,exit相比于_exit/_Exit,就是多做了两件事: - 调用用户通过atexit/on_exit注册的exit handler,要了解这部分内容可以参考这atexit/on_exit两个函数的手册。
- 清理stdio,即通过fclose,关闭stdin,stdout,stderr。
exit多做的两件事中,第一件很好理解,带来的区别也很容易感受到,所以我们接下来看看exit多做的第二件事,即清理stdio,会带来什么影响。
首先我们要知道,stdio的一大作用,就是为输入输出提供缓冲,尽量减少系统调用(read/write)的次数。而一旦涉及到缓冲,就不得不提出一个问题:如何缓冲?
stdio提供了三种缓冲形式:
- 无缓冲。这种模式下,每一次调用getchar/gets等标准输入函数都会调用read,每一次调用putchar/puts等标准输出函数都会调用write。好处是不用担心缓冲带来的各类问题,比如子进程与父进程对同一内容的两次输出,缓冲区内容丢失等;坏处是系统调用次数多,容易降低程序性能。
- 行缓冲。这种模式下,标准输入输出函数会在遇到换行符时,才进行真正的I/O:putchar('a')只是将'a'放入stdout的缓冲区,再接一个putchar('\n'),才会令'a'和'\n'真正被输出。当然,缓冲区如果满了,也会导致真正的I/O发生。fgets的行为也是行缓存式行为(哪怕stdin不是行缓冲)。
- 全缓冲。这种模式下,标准输入输出函数仅在缓冲区满了的情况下,才会进行真正的I/O。
对于Linux来说,stdio所选的默认缓冲形式一般为: - stderr无缓冲。
- 如果stdin/stdout指向终端,则为行缓冲,否则为全缓冲。
知道stdio存在缓冲与缓冲形式后,我们要知道,清理stdio,即通过fclose关闭stdin/stdout/stderr时,fclose会对stdout/stderr调用fflush,将其缓冲区剩余数据输出到目标文件(或设备)中。
最后,我们就能明白exit多做的第二件事,会带来怎样的区别了:如果stdout/stderr缓冲区中还有数据,那么exit会将这些数据输出到文件(或设备)中,而_exit则不会,从而导致缓冲区数据被抛弃。
我们可以通过一个简单的程序来测试一下上述说法是否正确:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("abcd"); //注意,没有换行
_exit(0);
}
nspt@linux:~$ gcc test.c
nspt@linux:~$ ./a.out
nspt@linux:~$
程序没有输出"abcd",这个不难理解,因为此时stdout指向终端,为行缓冲,而我们调用printf所输出的字符串没有换行(也没有填满缓冲区),所以"abcd"留在了缓冲区,_exit又没有清理stdio,所以缓冲区中的"abcd"也就随着进程终止,被清理掉了(而不是输出到目标)。
如果我们将_exit替换为exit,那么"abcd"就会输出到终端:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("abcd");
exit(0);
}
nspt@linux:~$ gcc test.c
nspt@linux:~$ ./a.out
abcdnspt@linux:~$
最后,我们来看看子进程调用exit与调用_exit的区别。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("abcd");
if (fork()==0)
exit(0); //Child exit
sleep(2); //Wait child
exit(0);
}
nspt@linux:~$ gcc test.c
nspt@linux:~$ ./a.out
abcdabcdnspt@linux:~$