linux之进程终止与等待,一文讲透

 更新时间:2026年10月30日 17:23:58   作者:吹什么轩  
还在为进程退出、僵尸进程、wait和waitpid头疼吗?本文用最直白的方式讲透进程终止的本质、三种退出场景、exit与_exit的区别,以及如何用wait/waitpid回收子进程、避免僵尸进程,还详细解析了status位图,让你一眼看懂退出码和信号

一、进程终止的本质

进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。

注意这句话的两个层面:

  • 内核数据结构:task_struct、页表、文件描述符表……回收掉;
  • 数据和代码:占用的物理内存页、映射的共享库……释放掉。

所以"进程死了"不是"这个程序不跑了"这么简单,而是它占的一切资源被系统收回。

二、进程退出的三种场景

场景说明例子
代码跑完,结果正确正常结束,退出码一般是 0ls 执行成功
代码跑完,结果不正确正常结束,但退出码非 0程序逻辑判断"找不到文件",exit(1)
代码异常终止没跑完就被打断段错误、Ctrl+C、kill -9

关键区别:前两种是进程自己决定退出,第三种是被外力杀死。这两种情况在父进程 wait 时能区分(看 status 位图)。

三、进程的退出方法

3.1 正常终止(可以用echo $?查看退出码)

  1. 从 main 返回(最常见);
  2. 调用 exit;
  3. 调用 _exit。

3.2 异常退出

  • Ctrl + C(信号 SIGINT);
  • kill -9(信号 SIGKILL);
  • 段错误、除零等硬件异常触发的信号。

四、退出码(exit code)

退出码(退出状态)可以告诉我们最后一次执行的命令的状态。程序返回退出代码 0 表示执行成功;1 或 0 以外的任何代码都被视为不成功。

查看方式:

./a.out
echo $?        # 上一条第命令的退出码

Linux Shell 中的主要退出码(课件):

退出码含义
0命令执行无误,理想状态
1可以解释为"不被允许的操作"。例如没有 sudo 权限却执行 yum;再例如除以 0,命令 let a=1/0
130(128+2)被 SIGINT / ^C 终止
143(128+15)被 SIGTERM 终止

规律:信号终止的退出码 = 128 + 信号编号。

另外可以用 strerror 把错误码翻译成人类可读的描述:

#include <string.h>
#include <stdio.h>
printf("%s\n", strerror(1));   // 输出 "Operation not permitted" 之类

4.1_exit函数

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

参数 status 定义了进程的终止状态,父进程通过 wait 来获取该值。

重点细节:

虽然 status 是 int,但仅有低 8 位可以被父进程所用。所以 _exit(-1) 时,在终端执行 echo $? 发现返回值是 255(-1 的补码低 8 位 = 0xFF = 255)。

4.2exit函数

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

exit 最后也会调用 _exit,但在调用 _exit 之前,还做了其他工作:

  1. 执行用户通过 atexit 或 on_exit 定义的清理函数;
  2. 关闭所有打开的流,所有的缓存数据均被写入;
  3. 调用 _exit。

4.3exitvs_exit:一个能看见输出,一个看不见

这是最经典的区别演示。

int main()
{
    printf("hello");      // 注意:没有 \n
    exit(0);
}

运行结果:

hello[root@localhost linux]#

int main()
{
    printf("hello");      // 同样没有 \n
    _exit(0);
}

运行结果:

[root@localhost linux]#

什么也没打印出来!

原因链:

  1. printf("hello") 没有 \n,且 stdout 是行缓冲(终端下),所以"hello" 只待在用户态缓冲区里,还没真正 write 到内核;
  2. exit 会刷新所有流(把缓冲区写出去),所以你看到了 hello;
  3. _exit 不刷新缓冲区,直接进入内核销毁进程,缓冲区里的数据随进程一起蒸发了,所以你什么也看不到。

这也回头解释了第 2 篇那个坑:printf 不带 \n 就 fork,缓冲区被复制,父子各刷一次,于是"打印了两遍"。理解缓冲区,很多怪现象就不怪了。

4.4return退出

return 是一种更常见的退出方法。执行 return n 等同于执行 exit(n),因为调用 main 的运行时函数会把 main 的返回值当作 exit 的参数。

所以这三种写法等价:

int main() { return 0; }            // 从 main 返回
int main() { exit(0); }             // 显式调用
int main() { _exit(0); }            // 极端:不刷缓冲区

五、进程等待:为什么必须等

5.1 四条必要性(课件原话)

不管不顾会造成僵尸进程,进而内存泄漏。 子进程退出,父进程如果不管,就可能造成僵尸问题。

僵尸"刀枪不入"。 进程一旦变成僵尸状态,kill -9 也无能为力,因为你没有办法杀死一个已经死去的进程。

父进程需要知道子进程干得怎么样。 父进程派给子进程的任务完成得如何?结果对不对?是否正常退出?

回收资源需要主动动作。 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

一句话:wait 是杀死僵尸的唯一正道。

5.2wait

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);

返回值:成功返回被等待进程的 pid,失败返回 -1。

参数:status 是输出型参数,获取子进程退出状态;不关心可以传 NULL。

特性:

  • 等待任意一个子进程;
  • 阻塞式,子进程不退出就一直卡在这里。

5.3waitpid(更强大)

pid_t waitpid(pid_t pid, int *status, int options);

返回值:

  • 正常返回:收集到的子进程 ID;
  • 如果设置了 WNOHANG,且没有已退出的子进程可收集:返回 0;
  • 出错:返回 -1,errno 会被设置。

参数 pid:

值含义
-1等待任意一个子进程(与 wait 等效)
>0等待指定 pid 的子进程

参数 status(输出型):

宏作用
WIFEXITED(status)若为正常终止子进程返回的状态,则为真(判断是否正常退出)
WEXITSTATUS(status)若 WIFEXITED 非零,提取子进程退出码

参数 options:

  • 0:默认,阻塞等待;
  • WNOHANG:若指定的子进程没有结束,waitpid() 返回 0,不予等待(非阻塞)。

5.4 三种时机下的行为

情况wait / waitpid 的行为
子进程已经退出立即返回,释放资源,拿到退出信息
子进程存在且正在运行可能阻塞(阻塞模式下)
不存在该子进程立即出错返回

第三种很关键:没有子进程时 wait 会立刻返回 -1(errno = ECHILD),不会傻等。

六、status 不是整数,是位图

status 不能简单地当作整形来看待,可以当作位图来看待(只研究低 16 比特位)。

低 16 位的布局:

 15          8 7        0
 ┌────────────┬─────────┐
 │  退出状态码  │ 终止信号 │
 │ (exit code) │ (signal)│
 └────────────┴─────────┘
        ↑            ↑
  (st >> 8) & 0xFF   st & 0x7F
  • 高 8 位(bits 8-15):正常退出时的退出码;
  • 低 7 位(bits 0-6):导致终止的信号编号,为 0 表示正常终止;
  • bit 7:core dump 标志位。

所以:

  • (status & 0x7F) == 0 → 正常退出,退出码 = (status >> 8) & 0xFF;
  • (status & 0x7F) != 0 → 异常终止,信号 = status & 0x7F。

6.1 手动解析 status

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
    pid_t pid;
    if ((pid = fork()) == -1)
        perror("fork"), exit(1);
    if (pid == 0) {
        sleep(20);
        exit(10);                    // 子进程 20 秒后以 10 退出
    } else {
        int st;
        int ret = wait(&st);
        if (ret > 0 && (st & 0x7F) == 0) {      // 正常退出
            printf("child exit code: %d\n", (st >> 8) & 0xFF);
        } else if (ret > 0) {                    // 异常退出
            printf("sig code : %d\n", st & 0x7F);
        }
    }
    return 0;
}

测试结果(课件):

# 等 20 秒自然退出
child exit code: 10
# 在另一个终端 kill -9 掉子进程
sig code : 9

这就是"父进程怎么区分'任务成功'和'被外面杀了'"的标准答案。

七、阻塞等待 vs 非阻塞等待

7.1 阻塞等待(options = 0)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
    pid_t pid;
    pid = fork();
    if (pid < 0) {
        printf("%s fork error\n", __FUNCTION__);
        return 1;
    } else if (pid == 0) {                 // child
        printf("child is run, pid is : %d\n", getpid());
        sleep(5);
        exit(257);
    } else {                                // parent
        int status = 0;
        pid_t ret = waitpid(-1, &status, 0);   // 阻塞式等待,等待 5 秒
        printf("this is test for wait\n");
        if (WIFEXITED(status) && ret == pid) {
            printf("wait child 5s success, child return code is :%d.\n",
                   WEXITSTATUS(status));
        } else {
            printf("wait child failed, return.\n");
            return 1;
        }
    }
    return 0;
}

运行结果:

child is run, pid is : 45110
this is test for wait
wait child 5s success, child return code is :1.

注意两点:

  1. this is test for wait 是5 秒之后才打印的——父进程 真的被卡住了;
  2. 子进程 exit(257),但打印出来是 1。因为退出码只用低 8 位:257 & 0xFF = 1。这又一次印证了第 4.1 节那条规则。

7.2 非阻塞等待(WNOHANG)

父进程不傻等,而是"一边等一边干别的":

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <vector>
typedef void (*handler_t)();        // 函数指针类型
std::vector<handler_t> handlers;    // 函数指针数组
void fun_one() { printf("这是一个临时任务1\n"); }
void fun_two() { printf("这是一个临时任务2\n"); }
void Load() {
    handlers.push_back(fun_one);
    handlers.push_back(fun_two);
}
// 父进程在等待期间可以干的"其他事"
void handler() {
    if (handlers.empty()) Load();
    for (auto iter : handlers) iter();
}
int main() {
    pid_t pid = fork();
    if (pid < 0) {
        printf("%s fork error\n", __FUNCTION__);
        return 1;
    } else if (pid == 0) {              // child
        printf("child is run, pid is : %d\n", getpid());
        sleep(5);
        exit(1);
    } else {                             // parent
        int status = 0;
        pid_t ret = 0;
        do {
            ret = waitpid(-1, &status, WNOHANG);   // 非阻塞式等待
            if (ret == 0) {
                printf("child is running\n");
            }
            handler();                     // 干点别的
        } while (ret == 0);
        if (WIFEXITED(status) && ret == pid) {
            printf("wait child 5s success, child return code is :%d.\n",
                   WEXITSTATUS(status));
        } else {
            printf("wait child failed, return.\n");
            return 1;
        }
    }
    return 0;
}

运行效果:

child is run, pid is : 45111
child is running
这是一个临时任务1
这是一个临时任务2
child is running
...(反复若干次)
wait child 5s success, child return code is :1.

这种"轮询 + 顺便干活"的模式,就是所有单线程事件循环(包括 Nginx、Redis 的雏形)的思维方式。

注意 WNOHANG 模式下 CPU 会被吃满——真实工程里要配合 sleep、select、epoll 来等,而不是死循环轮询。

八、把僵尸问题彻底解决

回到第 3 篇。当一个父进程不断 fork,正确的收尾长这样:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
    pid_t id = fork();
    if (id == 0) {
        // 子进程干活
        sleep(2);
        exit(42);
    }
    // 父进程必须回收,否则子进程变僵尸
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if (ret == id && WIFEXITED(status)) {
        printf("子进程正常结束,退出码 = %d\n", WEXITSTATUS(status));
    }
    sleep(1);   // 给演示留点时间
    return 0;
}

三种"避免僵尸"的策略:

策略做法适用
主动 wait(推荐)父进程 wait/waitpid 回收大多数场景
忽略 SIGCHLDsignal(SIGCHLD, SIG_IGN),子进程退出自动回收不关心退出状态
让父进程先退出子进程变孤儿,由 init 收尸(double fork)守护进程

waitpid 只回收指定 pid 或任意一个子进程;有多个子进程时要循环回收,否则剩下的还是会变僵尸。

九、易错点

误解正确理解
status 就是退出码status 是位图,退出码在高 8 位,信号在低 7 位
exit(257) 父进程能拿到 257只能拿到 1(低 8 位)
_exit 和 exit 一样_exit 不刷缓冲区、不跑 atexit
wait 会等所有子进程只等任意一个,要全收得循环
没有子进程时 wait 会一直等立即返回 -1(ECHILD),不会阻塞
kill -9 能杀僵尸不能,僵尸已死,只能由父进程 wait 回收

总结

  • 进程终止 = 释放内核数据结构 + 代码数据;三种场景:正常对、正常错、异常终止。
  • 退出方法:return / exit / _exit / 信号。exit 会刷缓冲区、跑清理函数;_exit 直接走。
  • 退出码只有 低 8 位有效(_exit(-1) → $? = 255);信号终止时 $? = 128 + 信号号。
  • 必须等待:不然子进程变僵尸,kill -9 也救不了;父进程也要拿子进程的结果。
  • wait 等任意一个;waitpid 可选指定 pid、可选 WNOHANG 非阻塞。
  • status 是位图:(st & 0x7F) == 0 表示正常退出,退出码 = (st >> 8) & 0xFF;否则低 7 位是终止信号。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • 一步步教你配置并使用Xshell7免费版

    一步步教你配置并使用Xshell7免费版

    想要轻松掌握一款强大的终端仿真器吗?恭喜你找到了答案!本指南将一步步教你如何配置并使用 Xshell7 免费版,无论是新手还是资深用户,都能在这里找到实用技巧和惊喜,立即跟随我们的脚步,探索 Xshell7 的魅力吧!让我们一起开启高效运维之旅!
    2024-01-01
  • linux 清理内存命令详细介绍

    linux 清理内存命令详细介绍

    这篇文章主要介绍了linux 清理内存命令详细介绍的相关资料,这里对Linux 清理内存进行了详解及查看内存条命令,需要的朋友可以参考下
    2016-11-11
  • 使用光盘配置本地yum源的方法 yum源配置

    使用光盘配置本地yum源的方法 yum源配置

    这篇文章主要介绍了linux使用光盘配置本地yum源的方法
    2014-01-01
  • Linux date命令知识点总结

    Linux date命令知识点总结

    在本篇文章里小编给大家分享的是关于Linux date命令详细说明,有兴趣的朋友们跟着学习下。
    2020-02-02
  • Linux中SSH远程访问和控制的实现

    Linux中SSH远程访问和控制的实现

    SSH是一种安全通道协议,主要用来实现字符界面的远程登录、远程复制等功能,本文主要介绍了Linux中SSH远程访问和控制的实现,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-12-12
  • Linux服务器配置—搭建NFS服务器步骤

    Linux服务器配置—搭建NFS服务器步骤

    本文介绍linux服务器配置之如何搭建NFS服务器,供大家学习参考
    2013-02-02
  • CentOS7编译安装新版LNMP环境

    CentOS7编译安装新版LNMP环境

    本文给大家分享的是在最新版的centos系统中编译安装lnmp环境的详细步骤,非常的实用,推荐需要的小伙伴们参考下
    2016-10-10
  • Linux系统下为Nginx安装多版本PHP

    Linux系统下为Nginx安装多版本PHP

    本文为大家讲解了如何在Linux,Nginx环境下安装多版本PHP,并支持每个站点不同PHP版本的安装配置方法
    2018-10-10
  • Linux系统批量替换文件内容的详细方法不操作步骤

    Linux系统批量替换文件内容的详细方法不操作步骤

    由于编辑器批量操作或者其他误操作,且没有线上代码仓库,或者无法通过代码仓库回滚(原因是本地修改了大量的代码)而导致要批量修改文件内容要怎么办,所以本文给大家介绍了详细的方法和操作步骤,适用于不同场景,需要的朋友可以参考下
    2025-09-09
  • 解决Ubuntu系统中“Temporary Failure in Name Resolution“错误问题

    解决Ubuntu系统中“Temporary Failure in Name Re

    在Ubuntu系统中,"TemporaryFailureinNameResolution"错误通常是DNS查询失败导致的,可能由DNS配置错误、网络连接问题或防火墙设置引起,解决方法包括检查DNS配置、确保网络连接、重启网络服务、检查防火墙设置以及使用systemd-resolved管理DNS配置
    2026-05-05

最新评论