waitid 系统调用及示例

好的,我们来深入学习 waitid 系统调用

1. 函数介绍

在 Linux 系统中,当一个进程创建了子进程(使用 fork),父进程通常需要知道子进程何时结束(退出或被终止),以及它是如何结束的(正常退出码、被哪个信号杀死等)。这是进程管理和资源回收的重要环节。

data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">

waitid 系统调用就是用来让父进程(或具有适当权限的进程)等待一个或一组子进程的状态发生变化,并获取该变化的详细信息。

你可以把它想象成一个“进程状态监听器”。父进程调用 waitid 后,它会挂起(阻塞),直到它感兴趣的子进程发生了指定类型的事件(比如退出、被信号终止、停止、继续等)。当事件发生时,waitid 会返回,并把详细信息(哪个子进程、如何结束的)填充到一个结构体中。

waitid 相比于老一些的 wait 和 waitpid,提供了更强大和灵活的功能。

简单来说,waitid 就是让你用程序来“等待”并“获取”子进程的“死亡/停止/恢复”通知书,并且通知书上写得非常详细。

2. 函数原型

1
2
3
4
#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 来等待不同类型的子进程事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#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("&#91;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("&#91;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("&#91;Child 3, PID %d] Running, then will stop and continue.\n", getpid());
printf("&#91;Child 3] Entering loop, press Ctrl+Z in another terminal to stop me (if I'm foreground).\n");
printf("&#91;Child 3] Or, the parent will send SIGSTOP and SIGCONT.\n");
int counter = 0;
while (counter < 10) {
printf("&#91;Child 3] Working... %d\n", counter++);
sleep(1);
}
printf("&#91;Child 3] Finished normally.\n");
exit(100);
}

// --- Parent Process ---
printf("&#91;Parent] Created children: PID1=%d, PID2=%d, PID3=%d\n", pid1, pid2, pid3);

// 稍等一下,让子进程启动
sleep(1);

// 4. 向 Child 3 发送 SIGSTOP 使其停止
printf("\n&#91;Parent] Sending SIGSTOP to Child 3 (PID %d)...\n", pid3);
if (kill(pid3, SIGSTOP) == -1) {
perror("&#91;Parent] kill SIGSTOP");
}

// 等待 Child 3 停止
printf("&#91;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("&#91;Parent] waitid for stop");
} else {
printf("&#91;Parent] Detected Child 3 stopped:\n");
print_siginfo(&info);
}

// 5. 等待 Child 1 正常退出
printf("\n&#91;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("&#91;Parent] waitid for exit pid1");
} else {
printf("&#91;Parent] Detected Child 1 exited:\n");
print_siginfo(&info);
}

// 6. 向 Child 2 发送 SIGTERM 使其终止
printf("\n&#91;Parent] Sending SIGTERM to Child 2 (PID %d)...\n", pid2);
if (kill(pid2, SIGTERM) == -1) {
perror("&#91;Parent] kill SIGTERM");
}

// 等待 Child 2 被杀死
printf("&#91;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("&#91;Parent] waitid for exit pid2");
} else {
printf("&#91;Parent] Detected Child 2 killed/exited:\n");
print_siginfo(&info);
}

// 7. 向 Child 3 发送 SIGCONT 使其恢复
printf("\n&#91;Parent] Sending SIGCONT to Child 3 (PID %d)...\n", pid3);
if (kill(pid3, SIGCONT) == -1) {
perror("&#91;Parent] kill SIGCONT");
}

// 等待 Child 3 恢复运行 (这个可能不会立即发生,取决于子进程何时真正恢复)
// 更常见的是等待它最终退出
printf("&#91;Parent] Waiting for Child 3 to continue and then exit using waitid(WCONTINUED | WEXITED)...\n");
printf("&#91;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("&#91;Parent] waitid for exit pid3");
} else {
printf("&#91;Parent] Detected Child 3 exited:\n");
print_siginfo(&info);
}

// 8. 演示 WNOHANG (非阻塞)
printf("\n&#91;Parent] Demonstrating WNOHANG...\n");
printf("&#91;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("&#91;Parent] waitid WNOHANG");
} else if (result == 0) {
// 如果返回 0,表示成功调用,但没有符合条件的子进程状态改变
// 因为我们已经等待了所有子进程退出,所以这里应该没有更多可收割的
printf("&#91;Parent] WNOHANG returned 0: No children available to wait for.\n");
}

printf("\n&#91;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. 编译和运行

1
2
3
4
5
6
# 假设代码保存在 waitid_example.c 中
gcc -o waitid_example waitid_example.c

# 运行程序
./waitid_example

10. 预期输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
--- Demonstrating waitid ---
Parent PID: 12345
&#91;Child 1, PID 12346] Running for 3 seconds then exiting with status 42.
&#91;Child 2, PID 12347] Running for 5 seconds then will be killed by SIGTERM.
&#91;Child 3, PID 12348] Running, then will stop and continue.
&#91;Child 3] Entering loop, press Ctrl+Z in another terminal to stop me (if I'm foreground).
&#91;Child 3] Or, the parent will send SIGSTOP and SIGCONT.
&#91;Parent] Created children: PID1=12346, PID2=12347, PID3=12348

&#91;Parent] Sending SIGSTOP to Child 3 (PID 12348)...
&#91;Parent] Waiting for Child 3 to stop using waitid(WSTOPPED)...
&#91;Child 3] Working... 0
&#91;Child 3] Working... 1
&#91;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

&#91;Parent] Waiting for Child 1 to exit using waitid(WEXITED)...
&#91;Child 1] Running for 3 seconds then exiting with status 42.
&#91;Parent] Detected Child 1 exited:
Child PID: 12346
Signal/Exit Code: 42
Reason Code: CLD_EXITED (Child called exit())
Exit Status: 42

&#91;Parent] Sending SIGTERM to Child 2 (PID 12347)...
&#91;Parent] Waiting for Child 2 to be killed using waitid(WEXITED)...
&#91;Child 2] Running for 5 seconds then will be killed by SIGTERM.
&#91;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

&#91;Parent] Sending SIGCONT to Child 3 (PID 12348)...
&#91;Parent] Waiting for Child 3 to continue and then exit using waitid(WCONTINUED | WEXITED)...
&#91;Parent] (WCONTINUED detection might be unreliable, waiting for exit instead)
&#91;Child 3] Working... 2
&#91;Child 3] Working... 3
&#91;Child 3] Working... 4
&#91;Child 3] Working... 5
&#91;Child 3] Working... 6
&#91;Child 3] Working... 7
&#91;Child 3] Working... 8
&#91;Child 3] Working... 9
&#91;Child 3] Finished normally.
&#91;Parent] Detected Child 3 exited:
Child PID: 12348
Signal/Exit Code: 100
Reason Code: CLD_EXITED (Child called exit())
Exit Status: 100

&#91;Parent] Demonstrating WNOHANG...
&#91;Parent] Calling waitid(P_ALL, 0, info, WEXITED | WNOHANG)...
&#91;Parent] WNOHANG returned 0: No children available to wait for.

&#91;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 等选项(除非使用非标准扩展),以及更详细的信息返回方式。

data-ad-format="auto" data-full-width-responsive="true">