1. 函数介绍
wait4
是一个 Linux 系统调用,它是 waitpid
的一个扩展版本。它的主要功能是等待子进程的状态发生变化(通常是子进程终止或停止),并获取该子进程的退出状态信息。
你可以把 wait4
想象成一位家长在等待孩子(子进程)完成任务后回来汇报情况:
- 家长(父进程)给孩子(子进程)布置了一个任务(比如运行一个程序)。
- 孩子出去执行任务。
- 家长调用
wait4
,表示“我在家等你回来,告诉我任务完成得怎么样”。 - 孩子完成任务回家(子进程终止)。
wait4
返回,告诉家长孩子的 PID 和他是如何完成任务的(成功、失败、被中断等)。
wait4
比 wait
和 waitpid
更强大,因为它不仅可以获取子进程的 PID 和状态,还可以同时获取子进程使用的资源统计信息(如用户 CPU 时间、系统 CPU 时间、最大内存使用量等)。
2. 函数原型
#include <sys/wait.h> // 必需
#include <sys/resource.h> // 包含 struct rusage
pid_t wait4(pid_t pid, int *wstatus, int options, struct rusage *rusage);
3. 功能
- 等待子进程: 挂起调用进程(父进程),直到由
pid
参数指定的一个或多个子进程的状态发生变化。 - 获取状态: 当子进程状态变化被检测到时,
wait4
会返回该子进程的进程 ID (PID),并将其退出状态(exit status)存储到wstatus
指向的整型变量中。 - 获取资源使用信息(可选): 如果
rusage
参数非空,wait4
还会将子进程的资源使用统计信息(Resource Usage)填充到rusage
指向的struct rusage
结构体中。
4. 参数
pid_t pid
: 指定要等待的子进程的 ID。其行为与waitpid
的pid
参数完全相同:< -1
: 等待进程组 ID 等于pid
绝对值的任意子进程。-1
: 等待任意子进程(这是最常用的情况)。0
: 等待调用进程组 ID 与调用进程相同的任意子进程。> 0
: 等待进程 ID 等于pid
的特定子进程。
int *wstatus
: 这是一个指向int
类型变量的指针,用于接收子进程的退出状态。- 如果不需要获取状态,可以传入
NULL
(但在实践中很少这么做)。 - 子进程的退出状态包含了它是如何结束的(正常退出、被信号终止等)以及具体的退出码或信号编号。
- 通常使用
<sys/wait.h>
中定义的宏来检查和解析wstatus
的值:WIFEXITED(wstatus)
: 如果子进程是正常退出(通过exit()
或从main
返回),返回真(非 0)。WEXITSTATUS(wstatus)
: 如果WIFEXITED(wstatus)
为真,返回子进程的退出码(0-255)。WIFSIGNALED(wstatus)
: 如果子进程是被信号终止的,返回真。WTERMSIG(wstatus)
: 如果WIFSIGNALED(wstatus)
为真,返回终止子进程的信号编号。WIFSTOPPED(wstatus)
: 如果子进程当前是停止状态(通常由SIGSTOP
,SIGTSTP
,SIGTTIN
,SIGTTOU
信号导致),返回真。WSTOPSIG(wstatus)
: 如果WIFSTOPPED(wstatus)
为真,返回导致子进程停止的信号编号。
- 如果不需要获取状态,可以传入
int options
: 这是一个位掩码,用于修改wait4
的行为。它可以是以下零个或多个标志的按位或(OR)组合:WNOHANG
: 如果没有子进程状态立即可用,则wait4
不阻塞,立即返回 0。WUNTRACED
: 报告因信号而停止的子进程状态(即使没有追踪它们)。WCONTINUED
: 报告先前因信号停止、现已收到SIGCONT
信号并继续执行的子进程。
struct rusage *rusage
: 这是一个指向struct rusage
结构体的指针。- 如果非
NULL
,wait4
会将子进程的资源使用统计信息填充到该结构体中。 - 如果为
NULL
,则不收集资源信息。struct rusage
包含了很多关于子进程执行期间资源消耗的详细信息,例如:
struct rusage { struct timeval ru_utime; // 用户 CPU 时间 struct timeval ru_stime; // 系统 CPU 时间 long ru_maxrss; // 最大常驻集大小 (KB) long ru_ixrss; // 共享内存大小积分 (未维护) long ru_idrss; // 未共享数据大小积分 (未维护) long ru_isrss; // 未共享栈大小积分 (未维护) long ru_minflt; // 缺页中断次数 (无需从磁盘加载页面) long ru_majflt; // 主缺页中断次数 (需要从磁盘加载页面) long ru_nswap; // 内存交换次数 (未维护) long ru_inblock; // 文件系统读入操作次数 long ru_oublock; // 文件系统写入操作次数 long ru_msgsnd; // IPC 消息发送次数 (未维护) long ru_msgrcv; // IPC 消息接收次数 (未维护) long ru_nsignals; // 信号接收次数 long ru_nvcsw; // 自愿上下文切换次数 long ru_nivcsw; // 非自愿上下文切换次数 };
- 注意: 并非所有字段在所有系统上都得到维护或精确计算。
- 如果非
5. 返回值
- 成功时:
- 返回已更改状态的子进程的 PID。
- 失败时:
- 返回 -1,并设置全局变量
errno
来指示具体的错误原因(例如ECHILD
没有符合条件的子进程,EINTR
调用被信号中断,EINVAL
options
参数无效)。
- 返回 -1,并设置全局变量
WNOHANG
且无子进程状态改变时:- 返回 0。
6. 相似函数,或关联函数
wait
:wait(&status)
等价于wait4(-1, &status, 0, NULL)
。它是最简单的等待任意子进程结束的函数。waitpid
:waitpid(pid, &status, options)
等价于wait4(pid, &status, options, NULL)
。它比wait
更灵活,允许指定等待哪个子进程以及设置选项。waitid
: POSIX.1-2001 标准引入的更现代的等待函数,提供了更细粒度的控制和信息。getrusage
: 用于获取调用进程自身或其已终止子进程的资源使用信息。wait4
的rusage
参数提供了类似的功能,但针对特定的已终止子进程。
7. 示例代码
示例 1:基本的 wait4
使用
这个例子演示了 wait4
最基本的用法,等待子进程结束并获取其退出状态。
// wait4_basic.c
#include <sys/wait.h> // wait4, WIFEXITED, WEXITSTATUS, WIFSIGNALED, WTERMSIG
#include <sys/resource.h> // struct rusage
#include <unistd.h> // fork, getpid
#include <stdio.h> // printf, perror
#include <stdlib.h> // exit
int main() {
pid_t pid;
int status;
struct rusage usage;
printf("Parent process (PID: %d) starting.\n", getpid());
pid = fork();
if (pid == -1) {
perror("fork failed");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// --- 子进程 ---
printf("Child process (PID: %d) started.\n", getpid());
// 模拟一些工作
for (int i = 0; i < 3; ++i) {
printf(" Child working... %d\n", i + 1);
sleep(1);
}
// 子进程以特定状态码退出
int exit_code = 42;
printf("Child process (PID: %d) finished. Exiting with code %d.\n", getpid(), exit_code);
exit(exit_code); // 正常退出
} else {
// --- 父进程 ---
printf("Parent process (PID: %d) created child (PID: %d).\n", getpid(), pid);
// --- 关键: 调用 wait4 等待子进程结束 ---
// pid: 等待特定子进程 pid
// &status: 接收子进程退出状态
// 0: 默认选项 (阻塞等待)
// &usage: 接收资源使用信息
pid_t waited_pid = wait4(pid, &status, 0, &usage);
if (waited_pid == -1) {
perror("wait4 failed");
exit(EXIT_FAILURE);
}
printf("Parent: wait4 returned. Waited for child PID %d.\n", (int)waited_pid);
// --- 检查和解析子进程退出状态 ---
if (WIFEXITED(status)) {
int exit_status = WEXITSTATUS(status);
printf("Parent: Child exited normally with status code %d.\n", exit_status);
} else if (WIFSIGNALED(status)) {
int signal_num = WTERMSIG(status);
printf("Parent: Child was killed by signal %d.\n", signal_num);
} else {
printf("Parent: Child did not exit normally or by signal.\n");
}
// --- 打印资源使用信息 ---
printf("\n--- Resource Usage of Child (PID: %d) ---\n", (int)waited_pid);
printf("User CPU time used: %ld.%06ld seconds\n",
(long)usage.ru_utime.tv_sec, (long)usage.ru_utime.tv_usec);
printf("System CPU time used: %ld.%06ld seconds\n",
(long)usage.ru_stime.tv_sec, (long)usage.ru_stime.tv_usec);
printf("Maximum resident set size: %ld KB\n", usage.ru_maxrss);
printf("Page reclaims (soft page faults): %ld\n", usage.ru_minflt);
printf("Page faults (hard page faults): %ld\n", usage.ru_majflt);
printf("Voluntary context switches: %ld\n", usage.ru_nvcsw);
printf("Involuntary context switches: %ld\n", usage.ru_nivcsw);
printf("------------------------------------------\n");
printf("Parent process (PID: %d) finished.\n", getpid());
}
return 0;
}
代码解释:
- 父进程调用
fork()
创建子进程。 - 子进程:
- 执行一些模拟工作(循环打印并
sleep
)。 - 以状态码 42 调用
exit(42)
正常退出。
- 执行一些模拟工作(循环打印并
- 父进程:
- 调用
wait4(pid, &status, 0, &usage)
。pid
: 等待之前创建的特定子进程。&status
: 指向int
变量,用于接收退出状态。0
: 使用默认选项,即阻塞等待。&usage
: 指向struct rusage
变量,用于接收资源使用信息。
- 检查
wait4
的返回值。如果成功,返回值是子进程的 PID。 - 使用
WIFEXITED
和WEXITSTATUS
宏检查子进程是否正常退出,并获取其退出码(42)。 - 打印从
rusage
结构体中获取的资源使用统计信息,如用户 CPU 时间、系统 CPU 时间、最大内存使用量等。
- 调用
示例 2:使用 wait4
等待任意子进程 (pid = -1
)
这个例子演示了如何使用 wait4
等待任意一个子进程结束。
// wait4_any.c
#include <sys/wait.h>
#include <sys/resource.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM_CHILDREN 3
int main() {
pid_t pid;
int i;
printf("Parent process (PID: %d) creating %d children.\n", getpid(), NUM_CHILDREN);
// 1. 创建多个子进程
for (i = 0; i < NUM_CHILDREN; i++) {
pid = fork();
if (pid == -1) {
perror("fork failed");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// --- 子进程 ---
printf("Child %d (PID: %d) started.\n", i, getpid());
// 每个子进程睡眠不同的时间
sleep(i + 2);
printf("Child %d (PID: %d) finished. Exiting with code %d.\n", i, getpid(), i);
exit(i); // 以 i 作为退出码
}
// --- 父进程继续循环 ---
}
printf("Parent (PID: %d) created all children. Now waiting for them to finish.\n", getpid());
// 2. 循环等待所有子进程结束
// 使用 wait4(pid=-1, ...) 等待任意子进程
for (i = 0; i < NUM_CHILDREN; i++) {
int status;
struct rusage usage;
pid_t waited_pid;
// --- 关键: 使用 pid = -1 等待任意子进程 ---
waited_pid = wait4(-1, &status, 0, &usage);
if (waited_pid == -1) {
perror("wait4 failed");
// 可能需要更复杂的错误处理
continue;
}
printf("\nParent: wait4 returned. Waited for child PID %d.\n", (int)waited_pid);
if (WIFEXITED(status)) {
int exit_code = WEXITSTATUS(status);
printf("Parent: Child (PID: %d) exited normally with code %d.\n", (int)waited_pid, exit_code);
} else {
printf("Parent: Child (PID: %d) did not exit normally.\n", (int)waited_pid);
}
printf("Resource usage summary for child (PID: %d):\n", (int)waited_pid);
printf(" User CPU time: %ld.%06lds\n", (long)usage.ru_utime.tv_sec, (long)usage.ru_utime.tv_usec);
printf(" Sys CPU time: %ld.%06lds\n", (long)usage.ru_stime.tv_sec, (long)usage.ru_stime.tv_usec);
printf(" Max RSS: %ld KB\n", usage.ru_maxrss);
}
printf("\nParent (PID: %d) finished. All children have been reaped.\n", getpid());
return 0;
}
代码解释:
- 父进程通过循环调用
fork()
创建 3 个子进程。 - 每个子进程执行不同的睡眠时间(2秒、3秒、4秒),然后以自己的索引
i
作为退出码退出。 - 父进程进入另一个循环,调用
NUM_CHILDREN
次wait4
。 - 关键: 每次调用
wait4(-1, &status, 0, &usage)
。pid = -1
: 表示等待任意一个子进程结束。
- 每次
wait4
返回,就处理一个子进程的退出信息和资源使用情况。 - 循环结束后,所有子进程都被回收。
示例 3:使用 wait4
的 WNOHANG
选项
这个例子演示了如何使用 WNOHANG
选项让 wait4
非阻塞地检查是否有子进程已经结束。
// wait4_nonblock.c
#include <sys/wait.h>
#include <sys/resource.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h> // errno, ECHILD
int main() {
pid_t pid;
int i;
printf("Parent process (PID: %d) creating 2 children.\n", getpid());
// 1. 创建两个子进程
for (i = 0; i < 2; i++) {
pid = fork();
if (pid == -1) {
perror("fork failed");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// --- 子进程 ---
printf("Child %d (PID: %d) started, will sleep for %d seconds.\n", i, getpid(), 5 + i * 2);
sleep(5 + i * 2); // 第一个睡 5 秒,第二个睡 7 秒
printf("Child %d (PID: %d) finished. Exiting.\n", i, getpid());
exit(0);
}
}
printf("Parent (PID: %d) created children. Now polling with WNOHANG.\n", getpid());
int children_finished = 0;
int loop_count = 0;
// 2. 循环,使用 WNOHANG 非阻塞地检查子进程状态
while (children_finished < 2) {
loop_count++;
int status;
struct rusage usage;
pid_t waited_pid;
// --- 关键: 使用 WNOHANG 选项 ---
waited_pid = wait4(-1, &status, WNOHANG, &usage);
if (waited_pid == -1) {
if (errno == ECHILD) {
// 没有子进程了,但这在循环中不太可能发生
// 因为我们知道有两个子进程
printf("No children exist (ECHILD). This is unexpected in this loop.\n");
break;
} else {
perror("wait4 failed");
break;
}
} else if (waited_pid == 0) {
// 没有子进程状态改变
printf("Loop %d: No child has finished yet. Parent doing other work...\n", loop_count);
// 模拟父进程在等待期间做其他事情
sleep(1); // 实际应用中可能是处理其他任务
} else {
// 有一个子进程结束了
children_finished++;
printf("\nLoop %d: Parent: wait4 returned. Child PID %d has finished.\n", loop_count, (int)waited_pid);
if (WIFEXITED(status)) {
printf(" Child (PID: %d) exited normally with code %d.\n", (int)waited_pid, WEXITSTATUS(status));
}
printf(" Parent did other work for %d loop(s) while waiting.\n", loop_count);
}
}
printf("\nParent (PID: %d) finished. Both children have been reaped after %d loops.\n", getpid(), loop_count);
return 0;
}
代码解释:
- 父进程创建两个子进程,它们分别睡眠 5 秒和 7 秒。
- 父进程进入一个
while
循环。 - 关键: 在循环内部调用
wait4(-1, &status, WNOHANG, &usage)
。WNOHANG
: 使wait4
成为非阻塞调用。
- 检查
wait4
的返回值:waited_pid == -1
: 调用失败。检查errno
是否为ECHILD
。waited_pid == 0
: 没有子进程状态改变。父进程可以在此时执行其他任务(这里用sleep(1)
模拟)。waited_pid > 0
: 一个子进程结束了。处理其状态,并增加计数器children_finished
。
- 循环直到两个子进程都结束。
- 打印父进程在等待期间执行了多少次循环(模拟做了多少其他工作)。
重要提示与注意事项:
- 僵尸进程: 当子进程结束而父进程尚未调用
wait4
(或wait
,waitpid
)来获取其状态时,子进程会变成僵尸进程(Zombie Process)。这会浪费一个进程表项。因此,父进程必须等待其子进程。 ECHILD
错误: 如果没有符合条件的子进程(例如,所有子进程都已结束且状态已被回收),wait4
会返回 -1 并设置errno
为ECHILD
。EINTR
错误: 如果wait4
(阻塞模式)在等待期间被信号中断,它会返回 -1 并设置errno
为EINTR
。可以使用sigaction
设置SA_RESTART
标志或在循环中处理此错误。rusage
的准确性:rusage
提供的资源信息的准确性和完整性可能因系统和内核版本而异。某些字段可能未被维护。WNOHANG
的用途:WNOHANG
对于实现非阻塞的服务器或需要在等待子进程的同时处理其他任务的程序非常有用。- 与
wait
/waitpid
的关系:wait(&status)
等价于wait4(-1, &status, 0, NULL)
。waitpid(pid, &status, options)
等价于wait4(pid, &status, options, NULL)
。
总结:
wait4
是一个功能强大的系统调用,用于等待子进程状态变化并获取其退出状态和资源使用信息。它通过 pid
参数提供了灵活的等待目标选择,通过 options
参数(特别是 WNOHANG
)提供了阻塞/非阻塞行为控制,并通过 rusage
参数提供了宝贵的性能分析数据。理解其参数和返回值对于编写健壮、高效的多进程程序至关重要。