Linux进程终止

一、进程的 8 种终止方式:正常与异常的边界

进程终止分为正常终止异常终止两大类,共 8 种常见方式。二者的核心区别在于:正常终止是进程主动完成任务后退出,退出状态由用户指定;异常终止是进程因外部信号或内部错误被迫退出,退出状态由内核决定。

1.1 正常终止:主动退出,可控清理

正常终止包含 4 种方式,均会按照预设逻辑完成资源释放,部分方式还会执行完整的清理流程。

  1. main 函数中的 return 语句 这是最常见的进程终止方式,但需注意:return 仅在 main 函数中触发进程终止,在普通函数中仅表示函数返回。main 函数中 return n 等价于 exit(n),其中 n 为用户指定的退出状态码。
  2. exit () 函数:C 标准库的全量清理方案 函数原型为 void exit(int status);,属于 C 标准库函数,其核心特性是退出前执行完整的清理流程
    • 刷新所有标准 IO 缓冲区(例如 printf 未用 fflush 刷新的内容会被输出);
    • 执行通过 atexit()on_exit() 注册的自定义清理函数;
    • 关闭所有打开的文件描述符;
    • 最终调用 _exit() 系统调用完成进程终止。状态码约定:EXIT_SUCCESS(值为 0)表示执行成功,EXIT_FAILURE(值为 1)表示执行失败。
  3. _exit ()/_Exit () 函数:系统调用的极速终止 函数原型为 void _exit(int status);,属于 Linux 系统调用,其核心特性是无任何额外清理操作
    • 不刷新 IO 缓冲区,不执行 atexit() 注册的清理函数;
    • 仅关闭打开的文件描述符,直接释放进程资源并终止。适用场景:需要快速终止进程,且无需保留缓冲区数据的场景。
  4. 主线程退出或 pthread_exit 调用 针对多线程进程,终止逻辑与线程状态强相关:
    • 若主线程退出,且进程中无其他非分离线程运行,则进程终止;
    • 若主线程调用 pthread_exit(),仅主线程终止,其他线程可继续执行,直到所有线程结束,进程才会终止。

1.2 异常终止:被动退出,内核主导

异常终止同样包含 4 种方式,进程无法自主控制退出时机,退出状态由内核根据终止原因分配。

  1. abort () 函数:强制生成核心转储 函数原型为 void abort(void);,其功能是向进程发送 SIGABRT 信号。该信号无法被捕获或忽略,会强制终止进程,并生成核心转储文件(core dump),用于调试程序崩溃的根本原因。
  2. kill 命令或 kill () 系统调用:外部信号触发终止 这是通过外部信号终止进程的常用方式,分为两种操作形式:
    • 命令行层面:使用 kill -信号名 PID 命令,例如 kill -9 1234 发送 SIGKILL 信号强制终止 PID 为 1234 的进程;
    • 程序层面:调用 kill() 系统调用,例如 kill(pid, SIGKILL) 向指定 PID 的进程发送终止信号。常见终止信号:SIGKILL(9 号信号)强制终止,无法拦截;SIGTERM(15 号信号)请求终止,是 kill 命令的默认信号。
  3. 最后一个线程被 pthread_cancel 取消 多线程进程中,若最后一个存活的线程被 pthread_cancel() 函数取消,则进程会触发异常终止,终止逻辑由内核的信号机制主导。
  4. 触发致命错误:内核发送终止信号 进程运行时触发内部错误,内核会自动发送对应信号终止进程,常见场景包括:
    • 访问非法内存地址(触发 SIGSEGV 信号);
    • 执行除以 0 的运算;
    • 总线错误(触发 SIGBUS 信号)等。

二、exit ()、_exit () 与 return:执行流程的核心差异

正常终止的三种核心方式(main 函数 returnexit()_exit()),其底层执行逻辑存在明显差异,关键在于是否执行清理操作、是否刷新缓冲区。三者的完整执行流程对比如下:

终止方式 完整执行流程 核心差异点
main 函数中 return n return n → 调用 exit(n) → 刷新 IO 缓冲区 → 执行 atexit 清理函数 → 关闭文件描述符 → 调用 _exit() → 终止进程 依赖 exit() 完成全量清理,仅在 main 函数中生效
exit(n) exit(n) → 刷新 IO 缓冲区 → 执行 atexit 清理函数 → 关闭文件描述符 → 调用 _exit() → 终止进程 主动触发全量清理,可在程序任意位置调用
_exit(n) _exit(n) → 关闭文件描述符 → 直接终止进程 无任何额外清理操作,进程极速终止

验证示例:缓冲区刷新差异

我们可以通过一个简单的代码示例,直观感受三者的缓冲区处理差异:

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

// 测试 exit():会刷新缓冲区
void test_exit() {
    printf("测试 exit() 缓冲区:"); // 无换行符,默认不刷新缓冲区
    exit(EXIT_SUCCESS); // 执行 exit 会触发缓冲区刷新
}

// 测试 _exit():不刷新缓冲区
void test__exit() {
    printf("测试 _exit() 缓冲区:"); // 无换行符,缓冲区未刷新
    _exit(EXIT_SUCCESS); // 直接终止,不处理缓冲区数据
}

int main() {
    // 取消注释分别测试
    // test_exit();
    // test__exit();
    return 0;
}

运行结果:

  • 执行 test_exit():输出 测试 exit() 缓冲区:
  • 执行 test__exit():无任何输出。

三、进程退出状态的传递与解析

进程终止时,会将退出状态信息传递给父进程,这些信息包括终止类型(正常 / 异常)、状态码 / 终止信号编号 等。父进程需要通过 wait()waitpid() 函数获取这些信息,同时完成子进程资源的回收。

3.1 核心解析宏:提取退出状态信息

父进程通过 wait()/waitpid() 获取的 status 参数是一个整数,内核通过该整数封装了子进程的终止详情。Linux 提供了 4 个核心宏来解析该值:

功能说明
WIFEXITED(status) 判断子进程是否正常终止(return/exit ()/_exit ()),返回非 0 表示是,0 表示否
WEXITSTATUS(status) WIFEXITED(status) 为真时有效,获取正常终止的状态码(如 exit(8) 中的 8)
WIFSIGNALED(status) 判断子进程是否被信号异常终止,返回非 0 表示是,0 表示否
WTERMSIG(status) WIFSIGNALED(status) 为真时有效,获取终止子进程的信号编号(如 9 表示 SIGKILL

3.2 代码示例:解析子进程退出状态

复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <signal.h>

int main() {
    pid_t pid1 = fork();
    if (pid1 == -1) {
        perror("fork pid1 failed");
        return EXIT_FAILURE;
    }

    if (pid1 == 0) {
        // 子进程1:正常终止,状态码 8
        printf("子进程1(PID:%d)正常退出,状态码 8\n", getpid());
        exit(8);
    }

    pid_t pid2 = fork();
    if (pid2 == -1) {
        perror("fork pid2 failed");
        return EXIT_FAILURE;
    }

    if (pid2 == 0) {
        // 子进程2:等待被信号终止
        printf("子进程2(PID:%d)等待被 SIGKILL 终止...\n", getpid());
        sleep(5);
        exit(EXIT_SUCCESS);
    }

    // 父进程回收子进程1(正常终止)
    int status;
    pid_t ret = waitpid(pid1, &status, 0);
    if (ret > 0) {
        printf("回收子进程1(PID:%d):\n", ret);
        if (WIFEXITED(status)) {
            printf("  - 终止类型:正常终止\n");
            printf("  - 退出状态码:%d\n", WEXITSTATUS(status));
        }
    }

    // 父进程发送 SIGKILL 终止子进程2
    kill(pid2, SIGKILL);
    ret = waitpid(pid2, &status, 0);
    if (ret > 0) {
        printf("回收子进程2(PID:%d):\n", ret);
        if (WIFSIGNALED(status)) {
            printf("  - 终止类型:信号终止\n");
            printf("  - 终止信号编号:%d\n", WTERMSIG(status));
        }
    }

    return EXIT_SUCCESS;
}

运行结果:

复制代码
子进程1(PID:12345)正常退出,状态码 8
子进程2(PID:12346)等待被 SIGKILL 终止...
回收子进程1(PID:12345):
  - 终止类型:正常终止
  - 退出状态码:8
回收子进程2(PID:12346):
  - 终止类型:信号终止
  - 终止信号编号:9

四、进程终止后的特殊状态:僵尸进程与孤儿进程

父进程与子进程的终止顺序不同,会产生两种特殊的进程状态 ------ 僵尸进程和孤儿进程。二者对系统资源的影响差异巨大,是 Linux 进程管理中必须重点关注的内容。

4.1 僵尸进程:子死父不埋,资源泄漏隐患

定义

父进程创建子进程后,子进程先终止,但父进程未调用 wait()/waitpid() 函数回收子进程的 PCB(进程控制块) ,此时子进程的用户空间内存已释放,但内核空间的 PCB 仍存在,这类进程称为僵尸进程

危害

PCB 会占用内核内存资源,若父进程长期运行且频繁创建子进程,会导致系统中积累大量僵尸进程,耗尽内核内存,进而引发系统不稳定甚至崩溃。

查看方式

使用 ps 命令查看进程状态,僵尸进程的状态标记为 Z

复制代码
ps aux | grep Z

使用 top 命令时,僵尸进程的数量会显示在 zombie 列中。

解决方法:wait ()/waitpid () 主动回收

父进程通过调用 wait()waitpid() 函数,可主动回收子进程的 PCB,释放内核资源。二者的核心特性对比如下:

函数 原型 核心特性
wait() pid_t wait(int *status) 阻塞等待任意一个子进程终止,回收其 PCB;失败返回 -1
waitpid() pid_t waitpid(pid_t pid, int *status, int options) 可指定回收特定 PID 的子进程;支持非阻塞模式(WNOHANG),无待回收子进程时返回 0

waitpid () 非阻塞回收示例

复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork failed");
        return EXIT_FAILURE;
    }

    if (pid == 0) {
        // 子进程睡眠3秒后退出
        printf("子进程(PID:%d)睡眠3秒后退出\n", getpid());
        sleep(3);
        exit(10);
    } else {
        int status;
        while (1) {
            // 非阻塞模式回收指定子进程
            pid_t ret = waitpid(pid, &status, WNOHANG);
            if (ret > 0) {
                // 成功回收
                printf("回收子进程(PID:%d),状态码:%d\n", ret, WEXITSTATUS(status));
                break;
            } else if (ret == 0) {
                // 暂无子进程退出,父进程执行其他任务
                printf("暂无子进程退出,父进程执行其他任务...\n");
                sleep(1);
            } else {
                // 回收失败
                perror("waitpid failed");
                break;
            }
        }
    }

    return EXIT_SUCCESS;
}

4.2 孤儿进程:父死子无依,系统自动接管

定义

父进程创建子进程后,父进程先终止,子进程失去父进程,这类进程称为孤儿进程

处理机制

Linux 系统中,孤儿进程会被 init 进程(PID=1) 接管,init 进程会成为孤儿进程的新父进程。当孤儿进程终止时,init 进程会自动调用 wait() 函数回收其 PCB,因此孤儿进程不会像僵尸进程那样占用系统资源,通常无需人工干预。

代码示例:孤儿进程的产生与接管
复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork failed");
        return EXIT_FAILURE;
    }

    if (pid == 0) {
        // 子进程
        printf("子进程(PID:%d),原父进程(PID:%d)\n", getpid(), getppid());
        sleep(3); // 等待父进程终止
        printf("子进程(PID:%d),新父进程(PID:%d)\n", getpid(), getppid());
    } else {
        // 父进程立即退出
        printf("父进程(PID:%d)即将退出\n", getpid());
        exit(EXIT_SUCCESS);
    }

    return EXIT_SUCCESS;
}

运行结果:

复制代码
父进程(PID:12347)即将退出
子进程(PID:12348),原父进程(PID:12347)
子进程(PID:12348),新父进程(PID:1)

五、总结

  1. 进程终止分为正常和异常两类共 8 种方式,核心差异在于是否主动退出、是否执行清理流程;
  2. main 函数 return 依赖 exit() 完成清理,exit() 执行全量清理后调用 _exit()_exit() 则直接终止进程;
  3. 父进程通过 wait()/waitpid() 结合解析宏,可获取子进程的终止状态并回收资源;
  4. 僵尸进程是 "子死父不埋",需父进程主动回收;孤儿进程是 "父死子无依",由 init 进程接管,无资源风险。
相关推荐
晓得迷路了2 小时前
栗子前端技术周刊第 110 期 - shadcn/create、Github 更新 npm 令牌政策、Deno 2.6...
前端·javascript·css
nvd112 小时前
GKE web 应用实现 Auth0 + GitHub OAuth 2.0登录实施指南
前端·github
前端小端长2 小时前
项目里满是if-else?用这5招优化if-else让你的代码清爽到飞起
开发语言·前端·javascript
胡萝卜3.02 小时前
现代C++特性深度探索:模板扩展、类增强、STL更新与Lambda表达式
服务器·开发语言·前端·c++·人工智能·lambda·移动构造和移动赋值
_OP_CHEN2 小时前
【Git原理与使用】(六)Git 企业级开发模型实战:从分支规范到 DevOps 全流程落地
大数据·linux·git·gitee·项目管理·devops·企业级组件
bruk_spp2 小时前
linux gpio获取
java·linux·服务器
郝学胜-神的一滴2 小时前
Linux C++会话编程:从基础到实践
linux·运维·服务器·开发语言·c++·程序人生·性能优化
AI_56782 小时前
Vue3组件通信的实战指南
前端·javascript·vue.js
烤麻辣烫2 小时前
黑马大事件学习-16 (前端主页面)
前端·css·vue.js·学习