exit系统调用及示例

好的,我们继续按照您的列表顺序,介绍下一个函数。


1. 函数介绍

exit 是一个 C 标准库函数(而非直接的系统调用,但它会调用底层的 _exit 系统调用),用于终止调用它的当前进程

你可以把 exit 想象成主角在电影结尾谢幕并优雅退场

  • 主角(当前进程)完成了它的表演(执行了所有代码)。
  • 它调用 exit,告诉导演(操作系统):“我的戏演完了,现在我要离开了。”
  • 在正式退场前,主角可能会鞠躬致谢(执行清理工作),然后离开舞台(进程终止)。

exit 不仅会终止进程,还会执行一些标准的清理(cleanup)操作,然后将控制权交还给操作系统。


2. 函数原型

#include <stdlib.h> // 必需 (C 标准库)

void exit(int status);

注意exit 是 C 标准库函数。其底层通常会调用 Linux 系统调用 _exit


3. 功能

  • 终止进程: 立即终止调用 exit 的进程。
  • 执行清理: 在终止进程之前,exit 会执行一系列标准的清理操作:
    1. 调用退出处理函数: 按照与注册时相反的顺序(后注册先调用),调用所有通过 atexit 或 on_exit 注册的函数。
    2. 刷新并关闭标准 I/O 流: 自动刷新所有输出流(如 stdoutstderr)的缓冲区,确保所有待写数据都被写出。然后关闭所有标准 I/O 流。
  • 返回状态码: 将 status 参数作为进程的退出状态(exit status)返回给父进程
    • 按照惯例,0 表示成功非 0 值通常表示某种错误或异常
  • 不返回exit 函数永远不会返回到调用它的函数。一旦调用,进程即终止。

4. 参数

  • int status: 这是进程的退出状态码
    • 这是一个整数值,它会被传递给父进程(通常是启动该进程的 shell 或父进程)。
    • 惯例:
      • EXIT_SUCCESS (通常定义为 0): 表示程序成功执行完毕。
      • EXIT_FAILURE (通常定义为 1): 表示程序执行失败
      • 自定义值: 你可以使用 0-255 范围内的任何整数来表示特定的错误类型(例如,2 表示配置错误,3 表示文件未找到等)。超出 0-255 范围的值会被模 256 处理。

5. 返回值

  • voidexit 函数没有返回值,因为它永远不会返回

6. 相似函数,或关联函数

  • _exit: 这是一个直接的 Linux 系统调用。它立即终止进程,不执行任何标准 I/O 缓冲区刷新或 atexit 注册函数的调用。它只关闭文件描述符并返回 status 给父进程。在 fork 之后的子进程中,如果 exec 失败,通常推荐使用 _exit 而非 exit
  • atexit: 用于注册在进程正常终止(通过 exit)时要调用的函数。
  • on_exit: 类似于 atexit,但注册的函数可以接收 status 和一个用户提供的参数。
  • return from main: 在 main 函数中执行 return status; 等价于调用 exit(status)
  • abort: 立即异常终止进程,通常会产生核心转储(core dump)文件。

7. 示例代码

示例 1:基本的 exit 使用和 atexit 注册清理函数

这个例子演示了 exit 如何终止进程,并展示 atexit 注册的清理函数是如何被调用的。

// exit_atexit.c
#include <stdlib.h> // exit, atexit, EXIT_SUCCESS, EXIT_FAILURE
#include <stdio.h>  // printf, perror

// 定义两个清理函数
void cleanup_function_1(void) {
    printf("Cleanup function 1 is running.\n");
}

void cleanup_function_2(void) {
    printf("Cleanup function 2 is running.\n");
}

int main() {
    printf("Main function started.\n");

    // 1. 注册清理函数
    // 注意注册顺序: 2 -> 1
    if (atexit(cleanup_function_1) != 0) {
        perror("atexit for function 1 failed");
        // 即使注册失败,程序也可以继续,但这不是好习惯
        exit(EXIT_FAILURE);
    }

    if (atexit(cleanup_function_2) != 0) {
        perror("atexit for function 2 failed");
        exit(EXIT_FAILURE);
    }

    printf("Cleanup functions registered.\n");

    // 2. 执行一些工作
    printf("Performing some work in main...\n");
    for (int i = 0; i < 3; ++i) {
        printf("  Work step %d\n", i + 1);
    }

    // 3. 刷新 stdout 缓冲区 (可选,exit 会自动做)
    fflush(stdout);

    // 4. 正常退出,触发清理
    printf("Main function finished. Calling exit(EXIT_SUCCESS).\n");
    exit(EXIT_SUCCESS); // 等价于 return EXIT_SUCCESS; from main

    // --- 下面的代码永远不会执行 ---
    printf("This line will never be printed.\n");
}

代码解释:

  1. 定义了两个简单的清理函数 cleanup_function_1 和 cleanup_function_2,它们只是打印一条消息。
  2. 在 main 函数中,使用 atexit() 注册这两个清理函数。
    • 注意注册顺序:先注册 cleanup_function_1,再注册 cleanup_function_2
  3. 执行一些模拟工作。
  4. 调用 exit(EXIT_SUCCESS)
  5. 关键: 程序不会打印 “This line will never be printed.”。
  6. 关键exit 会按注册的相反顺序调用清理函数。因此,会先打印 “Cleanup function 2 is running.”,然后是 “Cleanup function 1 is running.”。
  7. exit 会自动刷新 stdout 的缓冲区。
  8. 进程终止,返回状态码 0 给父进程。

示例 2:exit 与 _exit 的区别 (在 fork 子进程中)

这个例子通过对比演示了在 fork 子进程中使用 exit 和 _exit 的区别。

// exit_vs__exit.c
#include <sys/socket.h> // fork
#include <unistd.h>     // _exit, fork
#include <stdio.h>      // printf, perror
#include <stdlib.h>     // exit

int main() {
    pid_t pid;

    // 打印一些内容到 stdout,但不换行,数据会留在缓冲区
    printf("Parent process (PID: %d) printing without newline. Buffer content: ");

    pid = fork();

    if (pid == -1) {
        perror("fork failed");
        exit(EXIT_FAILURE); // 父进程失败,使用 exit

    } else if (pid == 0) {
        // --- 子进程 ---
        printf("This is child process (PID: %d).\n", getpid());

        // 子进程打印到 stdout,数据会留在缓冲区
        printf("Child is about to terminate. ");

        // --- 关键区别 ---
        // 使用 exit: 会刷新 stdio 缓冲区
        // printf("Child calling exit(EXIT_SUCCESS).\n");
        // exit(EXIT_SUCCESS);

        // 使用 _exit: 不会刷新 stdio 缓冲区
        printf("Child calling _exit(EXIT_SUCCESS).\n");
        _exit(EXIT_SUCCESS); // 子进程推荐使用 _exit

    } else {
        // --- 父进程 ---
        printf("Parent continues after fork.\n");
        printf("Parent process (PID: %d) finished.\n", getpid());
        // 父进程正常退出,会刷新自己的缓冲区
        exit(EXIT_SUCCESS);
    }

    // 这行代码不会被执行
    return 0;
}

代码解释:

  1. 父进程首先打印一条消息到 stdout,但没有换行符 (\n)。在大多数系统上,stdout 在连接到终端时是行缓冲的,这意味着没有换行符的数据会暂时保存在stdio 缓冲区中,而不会立即显示在屏幕上。
  2. 调用 fork() 创建子进程。
  3. 在子进程中:
    • 打印一条消息 “This is child process …”。
    • 再打印一条消息 “Child is about to terminate. “(同样没有换行符)。
    • 关键: 调用 _exit(EXIT_SUCCESS)
      • _exit 会立即终止子进程。
      • 不会刷新 stdio 缓冲区。
      • 因此,子进程中打印但未刷新的 “Child is about to terminate. ” 不会出现在输出中。
  4. 在父进程中:
    • 打印消息。
    • 调用 exit(EXIT_SUCCESS)
    • exit 会刷新父进程的 stdio 缓冲区。
    • 因此,父进程中打印但未刷新的 “Parent process (PID: …) printing without newline. Buffer content: ” 会因为 exit 的刷新操作而被打印出来。
    • 然后打印 “Parent continues …” 和 “Parent process … finished.”。

运行结果:

Parent process (PID: 12345) printing without newline. Buffer content: This is child process (PID: 12346).
Child is about to terminate. Child calling _exit(EXIT_SUCCESS).
Parent continues after fork.
Parent process (PID: 12345) finished.

分析:

  • 父进程的缓冲区内容 “Buffer content: ” 被打印了,因为父进程的 exit 调用刷新了它。
  • 子进程的缓冲区内容 “Child is about to terminate. ” 没有被打印,因为子进程调用的 _exit 没有刷新缓冲区。

如果子进程调用 exit(EXIT_SUCCESS):

  • 子进程的 exit 也会尝试刷新缓冲区。
  • 这会导致 “Child is about to terminate. ” 被打印。
  • 但是,如果父进程也在运行并且也调用 exit,两个进程都试图刷新 stdout,可能会导致输出混乱或重复(因为它们共享了 fork 时的缓冲区状态)。虽然这个简单例子可能看不出问题,但在更复杂的情况下,这可能导致不可预测的行为。
  • 因此,在 fork 的子进程中,如果后续调用 exec 失败需要退出,强烈推荐使用 _exit 以避免这种潜在的 stdio 状态混乱。

重要提示与注意事项:

  1. 永不返回exit 调用后,当前进程立即终止,函数不返回。
  2. 清理工作exit 会执行重要的清理工作(atexit 函数、刷新 stdio)。这是它与 _exit 的主要区别。
  3. _exit 在子进程中: 在 fork 之后的子进程中,如果需要在 exec 失败后退出,应使用 _exit 而非 exit,以避免刷新共享的 stdio 缓冲区。
  4. main 中的 return: 在 main 函数中,return status; 等价于 exit(status);
  5. 状态码: 使用 EXIT_SUCCESS 和 EXIT_FAILURE 宏比直接使用数字更具可读性和可移植性。
  6. atexit 注册顺序atexit 注册的函数在 exit 时按后进先出(LIFO)的顺序被调用。
  7. stdio 缓冲区: 理解 exit 会刷新缓冲区,而 _exit 不会,对于避免输出混乱至关重要。

总结:

exit 是 C 程序终止的标准方式。它不仅终止进程,还负责任地执行清理工作,确保资源得到释放,输出得到刷新。理解其与系统调用 _exit 的区别,尤其是在多进程编程中,对于编写健壮的程序非常重要。

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注