好的,我们来深入学习 waitid
系统调用
1. 函数介绍
在 Linux 系统中,当一个进程创建了子进程(使用 fork
),父进程通常需要知道子进程何时结束(退出或被终止),以及它是如何结束的(正常退出码、被哪个信号杀死等)。这是进程管理和资源回收的重要环节。
waitid
系统调用就是用来让父进程(或具有适当权限的进程)等待一个或一组子进程的状态发生变化,并获取该变化的详细信息。
你可以把它想象成一个“进程状态监听器”。父进程调用 waitid
后,它会挂起(阻塞),直到它感兴趣的子进程发生了指定类型的事件(比如退出、被信号终止、停止、继续等)。当事件发生时,waitid
会返回,并把详细信息(哪个子进程、如何结束的)填充到一个结构体中。
waitid
相比于老一些的 wait
和 waitpid
,提供了更强大和灵活的功能。
简单来说,waitid
就是让你用程序来“等待”并“获取”子进程的“死亡/停止/恢复”通知书,并且通知书上写得非常详细。
2. 函数原型
#include <sys/wait.h> // 包含 waitid 函数声明和相关常量
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
3. 功能
挂起调用进程,直到由 idtype
和 id
指定的一个或多个子进程的状态发生变化(变化类型由 options
指定)。当满足条件的子进程状态改变时,将详细的状态信息填充到 infop
指向的 siginfo_t
结构体中。
4. 参数详解
idtype
:idtype_t
类型。- 指定要等待的进程的类型。它决定了
id
参数的含义。常见的值有:P_PID
: 等待由id
指定的特定进程 ID (PID) 的子进程。P_PGID
: 等待进程组 ID (PGID) 等于id
的所有子进程。P_ALL
: 等待调用进程的所有子进程(此时id
参数被忽略)。
id
:id_t
类型。- 其含义由
idtype
决定:- 如果
idtype
是P_PID
,则id
是要等待的子进程的 PID。 - 如果
idtype
是P_PGID
,则id
是要等待的子进程组的 PGID。 - 如果
idtype
是P_ALL
,则id
被忽略(通常设为 0)。
- 如果
infop
:siginfo_t *
类型。- 一个指向
siginfo_t
结构体的指针。当waitid
成功返回时,该结构体会被内核填充为关于已改变状态的子进程的详细信息。 siginfo_t
结构体包含很多字段,关键的有:si_pid
: 导致状态改变的子进程的 PID。si_status
: 子进程的退出状态或导致其状态改变的信号编号。si_code
: 状态改变的原因代码,例如:CLD_EXITED
: 子进程通过exit()
或从main
返回正常退出。CLD_KILLED
: 子进程被信号杀死。CLD_DUMPED
: 子进程被信号杀死并产生了核心转储 (core dump)。CLD_STOPPED
: 子进程被信号(如SIGSTOP
)停止。CLD_CONTINUED
: 子进程从停止状态被SIGCONT
信号恢复继续运行。
- … 还有其他字段。
options
:int
类型。- 一个位掩码,用于指定要等待的状态变化类型以及调用的行为。可以是以下值的按位或 (
|
) 组合:- 状态类型 (必须至少指定一个):
WEXITED
: 等待子进程正常退出(调用exit()
或从main
返回)。WSTOPPED
: 等待子进程被停止(通常是收到SIGSTOP
,SIGTSTP
,SIGTTIN
,SIGTTOU
信号)。WCONTINUED
: 等待被停止的子进程恢复运行(收到SIGCONT
信号)。
- 行为标志 (可选):
WNOHANG
: 非阻塞。如果没有任何子进程的状态符合条件,waitid
立即返回 0,而不挂起调用进程。WNOWAIT
: 不收割。获取子进程状态信息,但不将其从内核的子进程表中删除。这意味着后续的wait
调用仍可能获取到该子进程的信息。
- 状态类型 (必须至少指定一个):
5. 返回值
- 成功: 返回 0。
- 失败: 返回 -1,并设置全局变量
errno
来指示具体的错误原因。
6. 错误码 (errno
)
ECHILD
: 没有符合条件的子进程。例如,指定了一个不存在的 PID,或者使用WNOHANG
时没有子进程处于可收割状态。EINTR
: 系统调用被信号中断。EINVAL
:idtype
或options
参数无效。
7. 相似函数或关联函数
wait
: 最基础的等待子进程退出的函数。它等待任意一个子进程退出,并返回 PID 和状态码(需要使用宏如WIFEXITED
,WEXITSTATUS
等来解析)。pid_t wait(int *wstatus);
waitpid
:wait
的增强版。允许指定等待特定 PID 的子进程,或使用WNOHANG
等选项。pid_t waitpid(pid_t pid, int *wstatus, int options);
wait3
/wait4
: 更老的函数,功能与waitpid
类似,但可以额外返回资源使用信息(struct rusage
)。siginfo_t
:waitid
使用的关键数据结构,包含详细的子进程状态信息。
8. 示例代码
下面的示例演示了如何使用 waitid
来等待不同类型的子进程事件。
#define _GNU_SOURCE // 启用 GNU 扩展
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h> // 包含 waitid, siginfo_t 等
#include <signal.h> // 包含 kill, SIG* 常量
#include <string.h>
#include <errno.h>
// 辅助函数:打印 siginfo_t 中的信息
void print_siginfo(const siginfo_t *info) {
printf(" Child PID: %d\n", info->si_pid);
printf(" Signal/Exit Code: %d\n", info->si_status);
printf(" Reason Code: ");
switch (info->si_code) {
case CLD_EXITED:
printf("CLD_EXITED (Child called exit())\n");
printf(" Exit Status: %d\n", info->si_status);
break;
case CLD_KILLED:
printf("CLD_KILLED (Child was killed by signal)\n");
printf(" Signal Number: %d\n", info->si_status);
break;
case CLD_DUMPED:
printf("CLD_DUMPED (Child killed by signal and dumped core)\n");
printf(" Signal Number: %d\n", info->si_status);
break;
case CLD_STOPPED:
printf("CLD_STOPPED (Child was stopped by signal)\n");
printf(" Stop Signal Number: %d\n", info->si_status);
break;
case CLD_CONTINUED:
printf("CLD_CONTINUED (Child continued)\n");
// si_status for CLD_CONTINUED is not defined to be meaningful
break;
default:
printf("Unknown reason code: %d\n", info->si_code);
break;
}
}
int main() {
pid_t pid1, pid2, pid3;
siginfo_t info;
printf("--- Demonstrating waitid ---\n");
printf("Parent PID: %d\n", getpid());
// 1. 创建一个会正常退出的子进程
pid1 = fork();
if (pid1 == 0) {
// --- Child 1 ---
printf("[Child 1, PID %d] Running for 3 seconds then exiting with status 42.\n", getpid());
sleep(3);
exit(42);
}
// 2. 创建一个会被信号杀死的子进程
pid2 = fork();
if (pid2 == 0) {
// --- Child 2 ---
printf("[Child 2, PID %d] Running for 5 seconds then will be killed by SIGTERM.\n", getpid());
sleep(5);
// 这行不会执行到
exit(0);
}
// 3. 创建一个会停止和恢复的子进程
pid3 = fork();
if (pid3 == 0) {
// --- Child 3 ---
printf("[Child 3, PID %d] Running, then will stop and continue.\n", getpid());
printf("[Child 3] Entering loop, press Ctrl+Z in another terminal to stop me (if I'm foreground).\n");
printf("[Child 3] Or, the parent will send SIGSTOP and SIGCONT.\n");
int counter = 0;
while (counter < 10) {
printf("[Child 3] Working... %d\n", counter++);
sleep(1);
}
printf("[Child 3] Finished normally.\n");
exit(100);
}
// --- Parent Process ---
printf("[Parent] Created children: PID1=%d, PID2=%d, PID3=%d\n", pid1, pid2, pid3);
// 稍等一下,让子进程启动
sleep(1);
// 4. 向 Child 3 发送 SIGSTOP 使其停止
printf("\n[Parent] Sending SIGSTOP to Child 3 (PID %d)...\n", pid3);
if (kill(pid3, SIGSTOP) == -1) {
perror("[Parent] kill SIGSTOP");
}
// 等待 Child 3 停止
printf("[Parent] Waiting for Child 3 to stop using waitid(WSTOPPED)...\n");
memset(&info, 0, sizeof(info)); // 清零结构体
if (waitid(P_PID, pid3, &info, WSTOPPED) == -1) {
perror("[Parent] waitid for stop");
} else {
printf("[Parent] Detected Child 3 stopped:\n");
print_siginfo(&info);
}
// 5. 等待 Child 1 正常退出
printf("\n[Parent] Waiting for Child 1 to exit using waitid(WEXITED)...\n");
memset(&info, 0, sizeof(info));
if (waitid(P_PID, pid1, &info, WEXITED) == -1) {
perror("[Parent] waitid for exit pid1");
} else {
printf("[Parent] Detected Child 1 exited:\n");
print_siginfo(&info);
}
// 6. 向 Child 2 发送 SIGTERM 使其终止
printf("\n[Parent] Sending SIGTERM to Child 2 (PID %d)...\n", pid2);
if (kill(pid2, SIGTERM) == -1) {
perror("[Parent] kill SIGTERM");
}
// 等待 Child 2 被杀死
printf("[Parent] Waiting for Child 2 to be killed using waitid(WEXITED)...\n");
memset(&info, 0, sizeof(info));
if (waitid(P_PID, pid2, &info, WEXITED) == -1) {
perror("[Parent] waitid for exit pid2");
} else {
printf("[Parent] Detected Child 2 killed/exited:\n");
print_siginfo(&info);
}
// 7. 向 Child 3 发送 SIGCONT 使其恢复
printf("\n[Parent] Sending SIGCONT to Child 3 (PID %d)...\n", pid3);
if (kill(pid3, SIGCONT) == -1) {
perror("[Parent] kill SIGCONT");
}
// 等待 Child 3 恢复运行 (这个可能不会立即发生,取决于子进程何时真正恢复)
// 更常见的是等待它最终退出
printf("[Parent] Waiting for Child 3 to continue and then exit using waitid(WCONTINUED | WEXITED)...\n");
printf("[Parent] (WCONTINUED detection might be unreliable, waiting for exit instead)\n");
memset(&info, 0, sizeof(info));
// 通常我们只等待最终的退出
if (waitid(P_PID, pid3, &info, WEXITED) == -1) {
perror("[Parent] waitid for exit pid3");
} else {
printf("[Parent] Detected Child 3 exited:\n");
print_siginfo(&info);
}
// 8. 演示 WNOHANG (非阻塞)
printf("\n[Parent] Demonstrating WNOHANG...\n");
printf("[Parent] Calling waitid(P_ALL, 0, info, WEXITED | WNOHANG)...\n");
memset(&info, 0, sizeof(info));
int result = waitid(P_ALL, 0, &info, WEXITED | WNOHANG);
if (result == -1) {
perror("[Parent] waitid WNOHANG");
} else if (result == 0) {
// 如果返回 0,表示成功调用,但没有符合条件的子进程状态改变
// 因为我们已经等待了所有子进程退出,所以这里应该没有更多可收割的
printf("[Parent] WNOHANG returned 0: No children available to wait for.\n");
}
printf("\n[Parent] All children have been waited for. Parent exiting.\n");
printf("\n--- Summary ---\n");
printf("1. waitid(idtype, id, infop, options) waits for child process state changes.\n");
printf("2. idtype/id let you specify which child/children to wait for (PID, PGID, ALL).\n");
printf("3. options specify what events to wait for (WEXITED, WSTOPPED, WCONTINUED).\n");
printf("4. WNOHANG makes it non-blocking. WNOWAIT gets status without reaping.\n");
printf("5. infop (siginfo_t*) provides detailed information about the event.\n");
printf("6. It's more flexible and informative than wait/waitpid.\n");
return 0;
}
9. 编译和运行
# 假设代码保存在 waitid_example.c 中
gcc -o waitid_example waitid_example.c
# 运行程序
./waitid_example
10. 预期输出
--- Demonstrating waitid ---
Parent PID: 12345
[Child 1, PID 12346] Running for 3 seconds then exiting with status 42.
[Child 2, PID 12347] Running for 5 seconds then will be killed by SIGTERM.
[Child 3, PID 12348] Running, then will stop and continue.
[Child 3] Entering loop, press Ctrl+Z in another terminal to stop me (if I'm foreground).
[Child 3] Or, the parent will send SIGSTOP and SIGCONT.
[Parent] Created children: PID1=12346, PID2=12347, PID3=12348
[Parent] Sending SIGSTOP to Child 3 (PID 12348)...
[Parent] Waiting for Child 3 to stop using waitid(WSTOPPED)...
[Child 3] Working... 0
[Child 3] Working... 1
[Parent] Detected Child 3 stopped:
Child PID: 12348
Signal/Exit Code: 19
Reason Code: CLD_STOPPED (Child was stopped by signal)
Stop Signal Number: 19
[Parent] Waiting for Child 1 to exit using waitid(WEXITED)...
[Child 1] Running for 3 seconds then exiting with status 42.
[Parent] Detected Child 1 exited:
Child PID: 12346
Signal/Exit Code: 42
Reason Code: CLD_EXITED (Child called exit())
Exit Status: 42
[Parent] Sending SIGTERM to Child 2 (PID 12347)...
[Parent] Waiting for Child 2 to be killed using waitid(WEXITED)...
[Child 2] Running for 5 seconds then will be killed by SIGTERM.
[Parent] Detected Child 2 killed/exited:
Child PID: 12347
Signal/Exit Code: 15
Reason Code: CLD_KILLED (Child was killed by signal)
Signal Number: 15
[Parent] Sending SIGCONT to Child 3 (PID 12348)...
[Parent] Waiting for Child 3 to continue and then exit using waitid(WCONTINUED | WEXITED)...
[Parent] (WCONTINUED detection might be unreliable, waiting for exit instead)
[Child 3] Working... 2
[Child 3] Working... 3
[Child 3] Working... 4
[Child 3] Working... 5
[Child 3] Working... 6
[Child 3] Working... 7
[Child 3] Working... 8
[Child 3] Working... 9
[Child 3] Finished normally.
[Parent] Detected Child 3 exited:
Child PID: 12348
Signal/Exit Code: 100
Reason Code: CLD_EXITED (Child called exit())
Exit Status: 100
[Parent] Demonstrating WNOHANG...
[Parent] Calling waitid(P_ALL, 0, info, WEXITED | WNOHANG)...
[Parent] WNOHANG returned 0: No children available to wait for.
[Parent] All children have been waited for. Parent exiting.
--- Summary ---
1. waitid(idtype, id, infop, options) waits for child process state changes.
2. idtype/id let you specify which child/children to wait for (PID, PGID, ALL).
3. options specify what events to wait for (WEXITED, WSTOPPED, WCONTINUED).
4. WNOHANG makes it non-blocking. WNOWAIT gets status without reaping.
5. infop (siginfo_t*) provides detailed information about the event.
6. It's more flexible and informative than wait/waitpid.
11. 总结
waitid
是一个功能强大且信息丰富的系统调用,用于等待子进程状态变化。
- 核心优势:
- 灵活性高:可以精确指定等待哪个进程/进程组,以及等待哪种类型的事件(退出、停止、恢复)。
- 信息详细:通过
siginfo_t
结构体返回非常详细的子进程状态信息,比wait
/waitpid
的wstatus
整数更易于理解和使用。 - 功能完整:支持停止/恢复事件的等待(
WSTOPPED
,WCONTINUED
)。
- 参数:
idtype
/id
定义范围,options
定义事件类型和行为,infop
接收结果。 - 使用场景:
- 需要精确控制等待哪个子进程。
- 需要区分子进程是正常退出、被信号杀死还是停止/恢复。
- 编写复杂的进程管理器或守护进程。
- 与
wait
/waitpid
的关系:wait(&status)
基本等价于waitpid(-1, &status, 0)
。waitpid(pid, &status, options)
功能是waitid
的子集。waitid
提供了waitpid
所没有的WSTOPPED
/WCONTINUED
等选项(除非使用非标准扩展),以及更详细的信息返回方式。