欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

exit() 和 _exit() 的区别

程序员文章站 2024-01-21 20:57:16
...

exit()和_exit()的效果都是让程序退出执行,而_exit()用来“尽快”退出。

atexit()

先说一下atexit()函数。我们可以用atexit()注册一个或多个函数退出清理函数(或者on_exit()但这个函数不建议用),这些清理函数按照注册时的反顺序,在exit()或main函数return时被调用。

#include <stdlib.h>
int atexit(void (*function)(void)); //return 0 on success.

注意,fork子进程时,这些函数会被继承到子进程。而exec系列执行成功后,所有函数会被清空。

exit()

我们知道父进程要wait子进程的退出状态,在子进程退出到父进程调用wait()期间,子进程就处于僵尸状态。因此,exit()将进程正常退出,并将(status & 0377)返回到父进程的wait(),其中status可以是EXIT_SUCCESS或EXIT_FAILURE。

#include <stdlib.h>
void exit(int status);

exit()在返回到父进程前要做的事情包括:按出栈顺序(反序)依次调用atexit()/on_exit()注册的函数。然后将所有打开的stdio流进行flush并关闭,如果有通过tmpfile()创建的文件也会被删除。(这里要注意,如果你注册的某个清理函数中调用_exit()或把自己kill结果退出了,那么后面的清理函数以及刷stdio等就不会执行了。所以,清理函数里不要调用exit()或_exit()。)

子进程exit()后会发送一个SIGCHLD信号给父进程(如果父进程设置了SA_NOCLDWAIT,那这个信号是否发送是未定义的)。
如果子进程在exit()后,父进程已经在等待(wait()系列函数)子进程的状态,或者父进程设置了SA_NOCLDWAIT或者将SIGCHLD的处理置为SIG_IGN,子进程都会立即退出。而如果父进程既没有wait,又没有设置忽略子进程退出,子进程就会变成僵尸进程(除了一个字节的exit status,什么都没有,用来确保以后某个时刻父进程wait的时候仍能拿到status,来解除子进程的僵尸状态)。如果父进程到死都没有wait,那僵尸进程会被init收养然后用wait清理掉它。

_exit()

而_exit()是企图让程序“立即”退出,它不会调用上述atexit()/on_exit()注册的函数。它也是会关闭自己打开的所有文件描述符的,但是否flush stdio以及是否删除tmpfile创建的文件则是与具体实现相关的(即没有明确规定)。我本地实验的结果是exit()会flush所有打开文件(stdio和其他文件)的缓冲,而_exit()不会。

#include <unistd.h>
void _exit(int status);

当然,由于_exit()会关闭文件描述符,所以可能也会有些delay(例如还没写完就close),如果想达到“立即”的目的,在_exit()之前调用一下tcflush()可能会有帮助。

exit()和return

在main()函数里调用exit()或return都可以使程序退出,但二者有一些差别:
- return是返回到main的调用者(如_libc_start_main),main()函数栈出栈,相关的局部变量被释放,调用者函数再让程序退出;
- exit()则是直接进入exit()函数去执行并退出函数。

无论哪种方式,它们都会调用exit_group(status)系统调用来让整个进程退出,其中参数status就是要发给父进程的状态码,即 exit(status) 或 return status; 中的status。

通常我们在main()之后就没什么待办的事情了,也不会关心exit()和return的差别,但对于C++程序而言,main()中对象的析构函数是在return之后执行的,如果中途调了exit()就不会执行到析构函数。

我们看下面这个程序:

#include <iostream>
#include <stdlib.h>
#include <string.h>
using namespace std;

void exit_func(void)
{
    cout << "oh yeah!" << endl;
}

class Date
{
public:
    Date(int year1 = 1970, int month1 = 12, int day1 = 31)
    {
        cout << "Date constructor" << endl;
        this->year = year1;
        this->month = month1;
        this->day = day1;
    }

    void printDate()
    {
        cout << year << ":" << month << ":" << day << endl;
    }
    int isLeapYear() { return 1; }
    void setDate(int year, int month, int day) {}

    ~Date() {cout << "Date destructor!" << endl;}

private:
    int year;
    int month;
    int day;
};

class A
{
private:
    int dataA;
public:
    A() {cout << "A's constructor" << endl;}
    ~A() {cout << "A's destructor!" << endl;}
};

class B
{
private:
    int dataB;
    A dataClassA;
public:
    B() {cout << "B's constructor" << endl;}
    ~B() {cout << "B's destructor!" << endl;}
};

static Date d199;

int main(int argc, char *argv[])
{
    cout << "main start" << endl;

    Date d1(2014, 1);
    d1.printDate();

    static Date d2;

    A a1;
    B b1;

    atexit(exit_func);

    cout << "main done" << endl;

    return 0;
}

这个程序中有几点需要关注:1.定义了一个全局的对象d199,我们知道全局对象的构造是在main()之前做的,相应的全局对象或局部静态对象的析构在main()之后。2.我注册了一个atexit函数exit_func(),上面讲过它会在main()之后被执行。3.在main()函数最后我调用了return 0。
运行程序:

[[email protected]]diskroot:$ c++ newmain.c 
[[email protected]]diskroot:$ ./a.out 
Date constructor
main start
Date constructor
2014:1:31
Date constructor
A's constructor
A's constructor
B's constructor
main done
B's destructor!
A's destructor!
A's destructor!
Date destructor!
oh yeah!
Date destructor!
Date destructor!

可以看到,代码执行顺序是:全局对象的构造 -> 进入main() -> main()中对象的构造 -> main()返回 -> main()中对象的析构 -> atexit注册的退出函数 -> 全局和局部静态对象的析构。

接下来我们再看一下把 return 改为 exit 的结果(其他代码不变):

[[email protected]]diskroot:$ c++ newmain.c 
[[email protected]]diskroot:$ ./a.out 
Date constructor
main start
Date constructor
2014:1:31
Date constructor
A's constructor
A's constructor
B's constructor
main done
oh yeah!
Date destructor!
Date destructor!

可以看到,main()中定义的对象的析构没有被调用。

一般情况下,析构函数就是释放对象的资源,而进程退出后,进程所有资源就都被释放了,所以实际上调用exit()退出程序也并不会出现资源泄漏。只是说如果你的析构函数会涉及到与其他进程通信或IO操作等影响到系统其他资源的情况下就要注意了。

另外由于使用return的话,main()函数返回,其函数栈被释放,这对于vfork()会导致父进程还没用完的栈被破坏。不过vfork()已经不被使用了,现在的fork()也不是当年发明vfork()时的那个低效的fork()了,所以这个问题我们不用关心。

在main()最后什么都不写也就相当于return 0, 当然不建议这样写,最好还是明确返回值退出。

相关标签: exit() atexit