【Linux学习】Linux中进程终止和进程等待

大家好,我是程序员小青蛙,今天讲解进程终止和进程等待问题。

一、进程退出的场景

进程退出主要分为 3 种情况:

  1. 正常终止 :代码运行完毕,结果正确(如 return 0)。
  2. 正常终止 :代码运行完毕,结果不正确(如 return 1)。
  3. 异常终止 :代码没跑完,程序被信号终止(如 Ctrl+Ckill 命令)。

进程退出码可以通过 echo $? 查看,$? 永远记录最近一个进程的退出码。


二、进程退出的 4 种方式

1. main 函数 return 返回

  • main 函数的 return n,本质上等同于调用 exit(n)
  • 进程退出码由 return 的值决定,只有低 8 位有效(0-255)。
  • 示例:return 0 表示程序正常结束;return 1 表示程序执行出错。

2. 调用 exit() 函数(库函数)

cpp 复制代码
#include <stdlib.h>
void exit(int status);
  • status:进程的退出状态,父进程可通过 wait 获取。
  • 核心特点:退出前会做清理工作
    1. 执行用户通过 atexit/on_exit 注册的清理函数。
    2. 刷新所有打开的流缓冲区(如 printf 的缓冲区)。
    3. 关闭所有打开的文件流。
    4. 最终调用 _exit() 终止进程。

3. 调用 _exit() 函数(系统调用)

cpp 复制代码
#include <unistd.h>
void _exit(int status);
参数:status定义了进程的终止状态,父进程通过wait来获取该值
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,
在终端执行$?发现返回值是255。
  • 直接终止进程,不会刷新缓冲区、不执行清理函数

  • 示例对比:

    cpp 复制代码
    // 用 exit() 时,缓冲区会被刷新,输出 "hello"
    printf("hello");
    exit(0);
    
    // 用 _exit() 时,缓冲区不会被刷新,无输出
    printf("hello");
    _exit(0);

4. 被信号终止(异常退出)

  • Ctrl+CSIGINT)、kill -9SIGKILL)、段错误(SIGSEGV)。
  • 这种情况下,进程退出码无意义,终止原因记录在 status 中。

三、exit() vs _exit() 核心区别

特性 exit() _exit()
类型 C 标准库函数 系统调用
刷新缓冲区 ✅ 会刷新用户态缓冲区 ❌ 不会刷新
执行清理函数 ✅ 执行 atexit 注册的函数 ❌ 不执行
关闭文件流 ✅ 关闭所有打开的流 ❌ 直接交给内核处理
适用场景 正常退出程序 异常情况下强制终止

四、进程等待:为什么必须 wait

必要性:

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

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

最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。

父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

  1. 回收僵尸进程:子进程退出后,如果父进程不回收,会变成僵尸进程,占用系统资源。
  2. 获取退出状态 :父进程可以通过 wait 知道子进程的退出码,判断子进程是否正常结束。
  3. 进程同步 :父进程可以通过 wait 阻塞等待子进程完成任务,再继续执行。

五、进程等待的两种方法

1. wait() 函数

复制代码
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
  • 功能:阻塞等待任意一个子进程退出,回收其资源。
  • 参数status 是输出型参数,用于获取子进程的退出状态(不关心可传 NULL)。
  • 返回值 :成功返回退出子进程的 pid;失败返回 -1

2. waitpid() 函数

复制代码
pid_t waitpid(pid_t pid, int *status, int options);
  • 功能:等待指定的子进程退出,支持非阻塞等待。
  • 参数详解
    • pid
      • pid > 0:等待 pid 等于指定值的子进程。
      • pid = -1:等待任意子进程(和 wait 等价)。
      • pid = 0:等待和调用者同组的任意子进程。
      • pid < -1:等待进程组 ID 等于 |pid| 的任意子进程。
    • status:同 wait(),获取退出状态。
    • options
      • 0:阻塞等待(默认行为)。
      • WNOHANG:非阻塞等待,若子进程未退出,函数立即返回 0

如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。

如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。

如果不存在该子进程,则立即出错返回。


六、解析 status:判断子进程退出原因

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。

如果传递NULL,表示不关心子进程的退出状态信息。

否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):


分析一下这个16位

st 是一个 16 位的状态数字

Linux 中,wait(&st) 拿到的 st 是一个 int 整数 ,但真正有用的只有低 16 位

复制代码
 15 14 13 12 11 10 9 8 │ 7  │ 6 5 4 3 2 1 0
      退出码(8位)      │core│   信号号(7位)

结构固定死了:

  • bit 0~6(低 7 位)终止信号编号
  • bit 7:core dump 标志
  • bit 8~15(高 8 位)进程退出码(exit code)

第一句:st & 0x7F → 取低 7 位(信号编号)

  1. 0x7F 是什么?

二进制:

复制代码
0111 1111

刚好 7 个 1

  1. 按位与 & 作用

st & 0x7F → 只保留 最低 7 位 → 高位全部清零

  1. 这 7 位存的是什么?

如果进程被信号杀死 ,这 7 位就是信号编号

  • 9 → SIGKILL
  • 15 → SIGTERM
  • 2 → SIGINT

所以:

复制代码
st & 0x7F   → 拿到杀死子进程的信号编号

第二句:(st >> 8) & 0xFF → 取高 8 位(退出码)

  1. st >> 8

把 16 位数字向右移动 8 位

移动前:

复制代码
高8位(退出码)    低8位(信号+core)
[15.............8][7.............0]

右移 8 位后:

复制代码
原来的高8位 来到了低8位
[15.............8] → 低8位
  1. & 0xFF

只保留低 8 位 ,也就是退出码

  1. 最终结果

    (st >> 8) & 0xFF → 拿到子进程的 exit(10) 里的 10

status 是一个 int 类型,其低 16 位包含了子进程的退出状态信息:

  • 正常终止 :低 8 位为 0,高 8 位为退出码(通过 WEXITSTATUS(status) 获取)。
  • 异常终止 :低 7 位为终止信号(通过 status & 0x7F 获取),第 8 位为 core dump 标志。

测试代码

cpp 复制代码
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main( void )
{
pid_t pid;
if ( (pid=fork()) == -1 )
perror("fork"),exit(1);
if ( pid == 0 ){
sleep(20);
exit(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 );
}
}
}

关键点:

  • st & 0x7F:取低 7 位,信号编号在这里
  • (st>>8)&0xFF:取高 8 位,退出码在这里

正常退出

杀死之后异常退出

常用宏定义

复制代码
WIFEXITED(status)   // 若为真,表示子进程正常退出
WEXITSTATUS(status) // 获取子进程的退出码(仅正常退出时有效)
WIFSIGNALED(status) // 若为真,表示子进程被信号终止
WTERMSIG(status)    // 获取终止子进程的信号编号

七、代码示例:进程等待的两种方式

1. 阻塞等待(waitpid + options=0

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

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    }
    else if (pid == 0) { // 子进程
        printf("child[%d] is running...\n", getpid());
        sleep(3); // 模拟任务执行
        exit(10); // 子进程退出,退出码为10
    }
    else { // 父进程
        printf("parent[%d] waiting for child...\n", getpid());
        int status;
        pid_t ret = waitpid(pid, &status, 0); // 阻塞等待
        if (ret > 0) {
            if (WIFEXITED(status)) {
                printf("child exited normally, exit code: %d\n", WEXITSTATUS(status));
            } else {
                printf("child exited abnormally, signal: %d\n", status & 0x7F);
            }
        }
    }
    return 0;
}
cpp 复制代码
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{
int status = 0;
pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
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;
}
  1. fork() 创建子进程

    pid = fork();

  • 调用后变成两个进程
  • 父进程返回子进程 pid
  • 子进程返回 0

  1. 子进程做了什么?

    else if( pid == 0 ){
    printf("child is run, pid is : %d\n",getpid());
    sleep(5);
    exit(257);
    }

  • 打印自己的 PID
  • sleep(5):休眠 5 秒
  • exit(257) :退出,退出码是 257

  1. 父进程做了什么?

    pid_t ret = waitpid(-1, &status, 0);

  • -1 :等待任意子进程
  • &status:输出型参数,拿到子进程退出状态
  • 0阻塞等待(子进程不退出,父进程就一直等)

  1. 判断子进程是否正常退出

    if( WIFEXITED(status) && ret == pid )

  • WIFEXITED(status) → 为真 → 子进程正常退出(不是被信号杀死)
  • ret == pid → 确保等待的就是目标子进程

  1. 获取退出码

    WEXITSTATUS(status)

  • 拿到子进程 exit(n) 中的 n
  • 只取低 8 位

最重要考点:exit (257) 为什么输出 1?

  • 257 的二进制:

    复制代码
    257 = 00000001 00000001
  • 退出码只保留低 8 位

  • 所以:

    复制代码
    257 & 0xFF = 1

所以程序最终输出:

复制代码
wait child 5s success, child return code is :1.

2. 非阻塞等待(waitpid + WNOHANG

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

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    }
    else if (pid == 0) { // 子进程
        printf("child[%d] is running...\n", getpid());
        sleep(3);
        exit(10);
    }
    else { // 父进程
        printf("parent[%d] waiting non-blocking...\n", getpid());
        int status;
        pid_t ret;
        do {
            ret = waitpid(pid, &status, WNOHANG); // 非阻塞等待
            if (ret == 0) {
                printf("child is still running, parent do other work...\n");
                sleep(1);
            }
        } while (ret == 0);
        
        if (ret > 0) {
            if (WIFEXITED(status)) {
                printf("child exited normally, exit code: %d\n", WEXITSTATUS(status));
            }
        }
    }
    return 0;
}
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.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(1);
} else{
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
if( ret == 0 ){
printf("child is running\n");
}
sleep(1);
}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;
}
  1. 父进程 非阻塞等待 子进程
  • waitpid(-1, &status, WNOHANG)
  • WNOHANG :非阻塞模式
    • 如果子进程还在运行waitpid 立即返回 0
    • 如果子进程已经退出 → 返回子进程 pid
  1. 父进程做了什么?
  • 每秒轮询一次
  • 没等到就打印:child is running
  • 等到了就打印退出码

运行结果

cpp 复制代码
child is run, pid is :
child is running
child is running
child is running
child is running
wait child 5s success, child return code is :1.

一共打印 5 次 child is running 因为子进程 sleep (5),父进程每秒问一次。


关键函数讲解

  1. waitpid(-1, &status, WNOHANG)
  • -1:等待任意子进程
  • &status:拿到退出状态
  • WNOHANG :非阻塞等待
    • 子进程没退出 → 返回 0
    • 子进程已退出 → 返回 子进程 pid
  1. WIFEXITED(status)

判断子进程是否正常退出(不是被信号杀死)

  1. WEXITSTATUS(status)

拿到子进程的退出码


阻塞 vs 非阻塞(总结)

  • 阻塞等待(wait /waitpid 0) 父进程一直等,啥也不干,直到子进程退出

  • 非阻塞等待(waitpid WNOHANG) 父进程每隔一段时间问一次,不等的时候可以干别的


八、关键知识点总结

  1. 进程退出方式returnexit()_exit()、信号终止。
  2. exit() vs _exit():前者刷新缓冲区、执行清理;后者直接终止。
  3. 进程等待的目的:回收僵尸进程、获取退出状态、进程同步。
  4. wait() vs waitpid()wait 阻塞等待任意子进程;waitpid 支持指定进程和非阻塞等待。
  5. status 解析 :通过 WIFEXITEDWEXITSTATUS 等宏判断子进程退出原因。
相关推荐
码农阿强1 小时前
OpenAI Codex 全平台详细安装与配置教程(Windows/Mac/Linux)
linux·windows·macos·ai
用户2367829801681 小时前
Linux mv 命令:文件移动与重命名的底层机制
linux
niannian1 小时前
从零开始!RuyiSDK + RISC-V 新手完整入门指南(Windows WSL2 环境)
操作系统
迷枫7121 小时前
DM8 数据共享集群 DSC 学习总结:共享存储、集群组件与常见误区
数据库·学习
都在酒里1 小时前
Linux字符设备驱动开发(一):从零搭建一个可直接运行的驱动框架(附完整代码)
linux·运维·驱动开发
蓝莓薄荷1 小时前
Ubuntu修改主机名操作指南
linux·ubuntu
MandalaO_O1 小时前
Java:面向对象 & Spring 框架
java·学习·spring
遇印记1 小时前
软考知识点(防火墙)
运维·服务器·网络·学习·安全