欢迎光临
我们一直在努力

Linux 进程等待与退出状态解析

在这里插入图片描述

文章目录

    • 1. 等待接口的职责
      • 1.1 等待结果与回收记录
      • 1.2 wait 与 waitpid
    • 2. 返回值决定下一步能做什么
      • 2.1 三种返回结果
      • 2.2 阻塞等待的几种时机
      • 2.3 EINTR 与 ECHILD
    • 3. 原始状态与解析宏
      • 3.1 先分类,再提取
      • 3.2 Linux 终止状态的低 16 位示意
    • 4. 正常退出与信号终止的完整对照
    • 5. WNOHANG 与有节奏的查询
      • 5.1 非阻塞不会自动产生回调
      • 5.2 查询间隔的取舍
    • 6. 多个子进程需要多次回收
      • 6.1 一次调用只报告一个结果
      • 6.2 数量已知与数量未知
    • 7. 暂停、继续与终止的边界
    • 8. 小结

上一篇我们大致讨论了进程是怎样终止的。那么这一篇将从父进程的角度继续:子进程结束以后,怎样知道结束的是谁,怎样取到结果,又怎样把保留的退出记录回收掉?

这里需要分清三个量(值):

量回答的问题典型类型与来源
等待函数的返回值 这次报告的是哪个子进程,或者有没有结果、是否出错 pid_t,来自 wait / waitpid 的返回值
原始等待状态 子进程是正常退出、被信号终止,还是发生了其他可报告事件 int status,由函数通过指针写入
正常退出码 子进程正常终止时报告了什么结果 确认 WIFEXITED(status) 后,用 WEXITSTATUS(status) 提取

waitpid 返回 42,与子进程退出码是 42,完全是两件事。 前者表示 PID,后者才是程序约定的执行结果。

1. 等待接口的职责


1.1 等待结果与回收记录

在默认的子进程回收设置下,子进程终止时,大部分运行资源都已经释放或解除关联,但仍需要保留必要的身份、终止状态等记录。父进程会通过等待接口取得这些结果,并完成相应回收。

如果子进程先结束,结果会保留到父进程来取;如果父进程先等待,可以睡眠到出现符合条件的结果。

僵尸进程的背景可以回看进程状态

1.2 wait 与 waitpid

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);

wait(&status) 等待任意一个子进程的终止结果。

在默认条件下,可以把它看作 waitpid(-1, &status, 0) 的简便形式。

waitpid 则把选择范围和等待方式明确写出来:

参数常用写法含义
pid 大于 0 只等待该 PID 对应的子进程
pid -1 等待任意一个子进程
wstatus &status 成功报告结果时,把状态写入这个整数
wstatus NULL 不需要状态,但仍进行等待和回收
options 0 默认阻塞等待终止结果
options WNOHANG 暂无可报告结果时立即返回

pid == 0 表示选择与调用者同进程组的子进程;pid < -1 表示选择进程组 ID 等于其绝对值的子进程。

普通父子进程关系下,waitpid 等待的是自己的子进程。所以即使知道某个服务器进程的 PID,也不代表可以调用它来回收那个进程。

2. 返回值决定下一步能做什么


2.1 三种返回结果

返回值含义能否解析这次的 status
大于 0 成功取得一个子进程的状态,返回其 PID 可以,随后先判断状态类别
0 使用了 WNOHANG,有符合选择条件的子进程,但目前没有所请求的可报告状态 不可以
-1 调用失败,按接口约定检查 errno 不可以

WNOHANG 返回 0,不表示“子进程以 0 退出”,也不表示“根本没有子进程”。如果没有符合条件、可等待的子进程,通常返回 -1,并设置 errno = ECHILD。

即使事先写了 int status = 0;,也不能在返回 0 后解析它。那只是自己初始化的旧值,不是这次取得的子进程结果。

2.2 阻塞等待的几种时机

调用 waitpid(child, &status, 0) 时:

子进程情况调用行为
还在运行,没有可报告终止结果 父进程进入等待;调度器可以运行其他任务
已终止,退出记录尚未回收 通常立即取得结果并回收,不需要再等一次
对应退出记录已经回收 不会再次取出相同结果,通常报 ECHILD
不存在符合条件的子进程 通常报 ECHILD

“阻塞”描述调用暂时不能完成,调用者等待条件成立;不等于父进程不停执行循环占满 CPU,也不等于整台服务器一起停住。

2.3 EINTR 与 ECHILD

阻塞等待可能被已捕获的信号打断。常见的稳妥写法是重试 EINTR,其他错误另行处理:

/* 局部写法:child 是 fork 成功后保存的子进程 PID */
pid_t got;
do {
got = waitpid(child, &status, 0);
} while (got == –1 && errno == EINTR);

这里的循环没有忙等:没有结果时,waitpid 本身会阻塞。

#mermaid-svg-SJcA3wxr9pzPRt3n{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SJcA3wxr9pzPRt3n .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SJcA3wxr9pzPRt3n .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SJcA3wxr9pzPRt3n .error-icon{fill:#552222;}#mermaid-svg-SJcA3wxr9pzPRt3n .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SJcA3wxr9pzPRt3n .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SJcA3wxr9pzPRt3n .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SJcA3wxr9pzPRt3n .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SJcA3wxr9pzPRt3n .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SJcA3wxr9pzPRt3n .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SJcA3wxr9pzPRt3n .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SJcA3wxr9pzPRt3n .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SJcA3wxr9pzPRt3n .marker.cross{stroke:#333333;}#mermaid-svg-SJcA3wxr9pzPRt3n svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SJcA3wxr9pzPRt3n p{margin:0;}#mermaid-svg-SJcA3wxr9pzPRt3n .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-SJcA3wxr9pzPRt3n .cluster-label text{fill:#333;}#mermaid-svg-SJcA3wxr9pzPRt3n .cluster-label span{color:#333;}#mermaid-svg-SJcA3wxr9pzPRt3n .cluster-label span p{background-color:transparent;}#mermaid-svg-SJcA3wxr9pzPRt3n .label text,#mermaid-svg-SJcA3wxr9pzPRt3n span{fill:#333;color:#333;}#mermaid-svg-SJcA3wxr9pzPRt3n .node rect,#mermaid-svg-SJcA3wxr9pzPRt3n .node circle,#mermaid-svg-SJcA3wxr9pzPRt3n .node ellipse,#mermaid-svg-SJcA3wxr9pzPRt3n .node polygon,#mermaid-svg-SJcA3wxr9pzPRt3n .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SJcA3wxr9pzPRt3n .rough-node .label text,#mermaid-svg-SJcA3wxr9pzPRt3n .node .label text,#mermaid-svg-SJcA3wxr9pzPRt3n .image-shape .label,#mermaid-svg-SJcA3wxr9pzPRt3n .icon-shape .label{text-anchor:middle;}#mermaid-svg-SJcA3wxr9pzPRt3n .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SJcA3wxr9pzPRt3n .rough-node .label,#mermaid-svg-SJcA3wxr9pzPRt3n .node .label,#mermaid-svg-SJcA3wxr9pzPRt3n .image-shape .label,#mermaid-svg-SJcA3wxr9pzPRt3n .icon-shape .label{text-align:center;}#mermaid-svg-SJcA3wxr9pzPRt3n .node.clickable{cursor:pointer;}#mermaid-svg-SJcA3wxr9pzPRt3n .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SJcA3wxr9pzPRt3n .arrowheadPath{fill:#333333;}#mermaid-svg-SJcA3wxr9pzPRt3n .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SJcA3wxr9pzPRt3n .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SJcA3wxr9pzPRt3n .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SJcA3wxr9pzPRt3n .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SJcA3wxr9pzPRt3n .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SJcA3wxr9pzPRt3n .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SJcA3wxr9pzPRt3n .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SJcA3wxr9pzPRt3n .cluster text{fill:#333;}#mermaid-svg-SJcA3wxr9pzPRt3n .cluster span{color:#333;}#mermaid-svg-SJcA3wxr9pzPRt3n div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SJcA3wxr9pzPRt3n .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SJcA3wxr9pzPRt3n rect.text{fill:none;stroke-width:0;}#mermaid-svg-SJcA3wxr9pzPRt3n .icon-shape,#mermaid-svg-SJcA3wxr9pzPRt3n .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SJcA3wxr9pzPRt3n .icon-shape p,#mermaid-svg-SJcA3wxr9pzPRt3n .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SJcA3wxr9pzPRt3n .icon-shape .label rect,#mermaid-svg-SJcA3wxr9pzPRt3n .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SJcA3wxr9pzPRt3n .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SJcA3wxr9pzPRt3n .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SJcA3wxr9pzPRt3n :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

大于 0

正常退出

信号终止

其他事件

等于 0

等于 -1

是

否

调用 waitpid

返回值

记录返回的子进程 PID

状态类别

提取 WEXITSTATUS

提取 WTERMSIG

按请求的选项另行处理

本次没有结果,继续其他工作

适当等待后再次查询

errno 是 EINTR

处理错误,不解析 status

3. 原始状态与解析宏


3.1 先分类,再提取

status 不是一个简单的可以直接当退出码打印的普通业务整数。它包含着系统编码的状态信息,应当通过 <sys/wait.h> 提供的宏解析来具体判断。

第一步:判断类别判定为真时,第二步才有意义
WIFEXITED(status):正常终止 WEXITSTATUS(status):低 8 位退出码
WIFSIGNALED(status):由信号终止 WTERMSIG(status):终止信号编号
WIFSTOPPED(status):报告暂停 WSTOPSIG(status):暂停信号编号
WIFCONTINUED(status):报告继续 表示曾暂停的子进程已经继续运行

例如,WIFEXITED(status) 为真且退出码为 7,表示子进程通过正常终止方式报告了 7;不是“检测到系统错误 7”。

3.2 Linux 终止状态的低 16 位示意

在这里插入图片描述

在这一限定范围内:

  • 正常退出:位 8~15 保存低 8 位退出码,低 8 位为 0。
  • 信号终止:位 0~6 保存终止信号,位 7 表示 core dump 相关标志;此时不能把高 8 位当正常退出码。

例如,正常情况下的 exit(42) 在这一 Linux 编码下常见的原始状态是 0x2a00;默认行为下由 SIGTERM 终止,常见原始状态是 0x000f。这同时也解释了为什么直接打印 status,得到的往往不是 42 或 Shell 的 143。

4. 正常退出与信号终止的完整对照

下面让子进程先等待约 1 秒,再选择正常退出 42,或者向自己发送默认会终止进程的 SIGTERM。不需要修改非法地址,也不需要寻找其他进程来结束。

编写程序并把内容保存为 wait_demo.c:

#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

// 解析并打印waitpid拿到的子进程状态信息
// pid:被回收的子进程PID
// status:waitpid输出的打包状态值,不能直接打印,要用宏解析
static void report_status(pid_t pid, int status)
{
// 打印子进程PID与status原始十六进制值
printf("reaped pid=%ld, raw status=0x%04x\\n", (long)pid, (unsigned int)status);

// WIFEXITED:判断子进程是否正常退出(_exit/exit/main return)
if (WIFEXITED(status)) {
// WEXITSTATUS:提取子进程退出码
printf("normal exit, code=%d\\n", WEXITSTATUS(status));
}
// WIFSIGNALED:判断子进程是否被信号终止
else if (WIFSIGNALED(status)) {
// WTERMSIG:获取杀死子进程的信号编号
printf("signal termination, signal=%d\\n", WTERMSIG(status));
#ifdef WCOREDUMP
// WCOREDUMP:判断进程崩溃是否生成core转储文件
printf("core flag=%d\\n", WCOREDUMP(status) != 0);
#endif
}
// 其他状态(暂停、继续等,本程序默认不会触发)
else {
puts("another reported state");
}
}

int main(int argc, char *argv[])
{
// 参数校验:必须传入1个参数,只能是 exit 或 signal
if (argc != 2 || (strcmp(argv[1],"exit") && strcmp(argv[1],"signal"))) {
fputs("usage: ./wait_demo exit|signal\\n", stderr);
return 2;
}

// 创建子进程
pid_t child = fork();
// fork失败的情况
if (child < 0) { perror("fork"); return 1; }

// 子进程逻辑
if (child == 0) {
sleep(1); // 休眠1秒,延长子进程运行时间,方便观察时序
if (!strcmp(argv[1], "signal")) {
raise(SIGTERM); // 给自己发送SIGTERM信号,进程被信号终止
_exit(126); // 收到SIGTERM默认终止,这行代码不会执行
}
_exit(42); // 正常退出,设置退出码42
}

int status;
pid_t got;
/*
* 阻塞waitpid等待指定子进程
* do-while循环:专门处理EINTR
* 当阻塞系统调用被捕获的信号打断(返回-1,errno=EINTR),重试waitpid
*/

do {
got = waitpid(child, &status, 0);
} while (got == –1 && errno == EINTR);

// waitpid调用真正失败
if (got == –1) { perror("waitpid"); return 1; }
// 解析打印子进程状态
report_status(got, status);
return 0;
}

gcc -std=c11 wait_demo.c -o wait_demo
./wait_demo exit
./wait_demo signal

PID 每次会变化,重点观察其它字段: 在这里插入图片描述

  • ./wait_demo exit:子进程调用_exit(42)正常退出,父进程 waitpid 回收,WIFEXITED(status)成立,WEXITSTATUS拿到 42。

  • ./wait_demo signal:子进程raise(SIGTERM)给自己发送 15 号 SIGTERM 信号,被信号终止,WIFSIGNALED(status)成立,WTERMSIG拿到信号号 15。

父进程最后 return 0,因此这两次 ./wait_demo 自身通常都向 Shell 报告 0。子进程退出 42,不会自动让父进程也退出 42。

5. WNOHANG 与有节奏的查询


5.1 非阻塞不会自动产生回调

把 options 改为 WNOHANG 后,暂无退出结果时立刻返回 0。它让父进程有机会继续做别的工作,但不会替你注册一个“结束后自动调用的函数”。

下面是完整的 poll_wait.c。子进程约 2 秒后退出,父进程每次查询没有结果,就完成一轮简单的计数工作,并等待 1 秒再查:

#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void)
{
// 创建子进程
pid_t child = fork();
if (child == –1) {
perror("fork");
return 1;
}

// 子进程分支:休眠2秒后正常退出,退出码9
if (child == 0)
{
sleep(2);
_exit(9);
}

unsigned int rounds = 0;
int status;
// WNOHANG:非阻塞模式,子进程未结束时waitpid立刻返回0,不阻塞等待
int options = WNOHANG;

// 循环轮询检测子进程状态
for (;;)
{
pid_t got = waitpid(child, &status, options);
if (got > 0)
{
// got>0:成功回收子进程,解析退出状态
if (WIFEXITED(status))
printf("reaped pid=%ld, code=%d\\n", (long)got, WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("reaped pid=%ld, signal=%d\\n", (long)got, WTERMSIG(status));
break; // 回收完成,退出循环
}
if (got == 0)
{
// got==0:子进程还在运行,父进程继续做自己的任务
rounds++;
printf("parent work round=%u\\n", rounds);
sleep(1); // 暂停1秒,再进行下一轮轮询
continue;
}
// 被信号打断,直接重试
if (errno == EINTR)
continue;

perror("waitpid");
return 1;
}
return 0;
}

gcc -std=c11 poll_wait.c -o poll_wait
./poll_wait

运行示例: 在这里插入图片描述

  • 子进程sleep(2),2 秒后退出;父进程每次没检测到子进程结束就sleep(1)。
  • 第一轮:打印round=1,休眠 1s
  • 第二轮:打印round=2,休眠 1s
  • 再过 1s,子进程已经退出,waitpid成功回收,输出reaped pid=…, code=9

5.2 查询间隔的取舍

没有间隔地反复调用 waitpid(…, WNOHANG),会在等待期间不断占用 CPU。

适当间隔可以避免空转,但也会带来最多约一个查询周期的发现延迟,再加上调度延迟。

如果父进程除了等子进程什么也不需要做,直接阻塞等待相对更加简单。

6. 多个子进程需要多次回收


6.1 一次调用只报告一个结果

当一个父进程同时拥有多个子进程时,一次成功的 wait 或 waitpid,只会取得其中一个子进程的状态。它不会一次性回收全部子进程,也不会返回一个结果数组。

选择指定 PID 时,父进程可能仍在等较慢的子进程 A,而较快的子进程 B 已经结束并暂时保留退出记录。

如果不关心处理的顺序,可以使用 waitpid(-1, …),并根据返回的 PID 来识别任务。

下面的 wait_many.c 会最多创建 3 个子进程:

#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void)
{
const int wanted = 3; // 计划创建3个子进程
int created = 0; // 记录成功创建的子进程数量
int failed = 0; // 标记fork是否失败

// 循环创建wanted个子进程
for (int i = 0; i < wanted; ++i)
{
pid_t child = fork();
if (child == –1)
{
perror("fork");
failed = 1;
break;
}
if (child == 0)
{
// 子进程:休眠不同时长再退出,实现子进程先后结束
sleep((unsigned int)(wanted – i));
_exit(10 + i); // 子进程退出码:10+i
}
// 父进程执行到这里,child是子进程pid
++created;
printf("created pid=%ld, task=%d\\n", (long)child, i);
}

int remaining = created; // 待回收的子进程数目
while (remaining > 0)
{
int status;
// waitpid(-1, …, 0):阻塞等待任意一个子进程退出
pid_t got = waitpid(–1, &status, 0);
if (got == –1) {
if (errno == EINTR) {
// 被信号中断,继续循环等待
continue;
}
perror("waitpid");
return 1;
}
—remaining; // 成功回收一个子进程

// 判断子进程是正常退出还是被信号杀死
if (WIFEXITED(status)) {
// 正常退出,获取退出码
printf("reaped pid=%ld, code=%d\\n",
(long)got, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
// 被信号终止,获取信号编号
printf("reaped pid=%ld, signal=%d\\n",
(long)got, WTERMSIG(status));
}
}
printf("all %d created children reaped\\n", created);
return failed;
}

gcc -std=c11 wait_many.c -o wait_many
./wait_many

运行示例: 在这里插入图片描述

正常情况下会创建 3 个子进程,分别报告 10、11、12,最后打印全部回收完成。

6.2 数量已知与数量未知

上面的独立程序明确记录成功创建的子进程的数量,所以按照 remaining 等待。另一种常见方式是不预先计数,而是反复地调用 waitpid(-1, &status, 0),直到返回 -1 且 errno == ECHILD,表示没有剩余可等待子进程。

这两种方式有不同前提。如果程序其他部分也会创建子进程,就要明确由谁负责等待、怎样根据 PID 分发结果;否则一个模块可能先取得另一个模块等待的结果。

7. 暂停、继续与终止的边界


waitpid 不只可以报告终止。下面这些选项简单知道用途即可,具体信号控制留到后续学习:

选项增加报告的事件对应判断
WUNTRACED 普通子进程因信号暂停 WIFSTOPPED、WSTOPSIG
WCONTINUED 已暂停的子进程因 SIGCONT 继续 WIFCONTINUED

暂停不是退出,报告一次暂停也没有回收一个仍然活着的进程。如果以后给程序增加这些选项,就不能沿用“返回大于 0,剩余子进程数立刻减一”的逻辑;应当仅在确认正常终止或信号终止时减少存活数量。

8. 小结


  • wait /waitpid 用于父进程等待子进程退出,回收僵尸进程资源,防止资源泄漏。

  • status 是复合状态变量,不能直接当作退出码,需要配套宏解析。

  • WIFEXITED 判断子进程是否正常退出,WEXITSTATUS 获取子进程退出码。

  • WIFSIGNALED 判断子进程是否被信号终止,WTERMSIG 获取终止信号编号。

  • 进程暂停不等于进程终止,暂停事件不会产生僵尸进程,不需要执行回收计数。

在这里插入图片描述

赞(0)
未经允许不得转载:171主机测评 » Linux 进程等待与退出状态解析
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址