waitid / wait3 / wait4
Advanced
3 Programs
Beyond waitpid()
Linux provides three more wait-family system calls, each offering capabilities beyond waitpid():
- waitid() — fine-grained event control, returns rich
siginfo_tinfo, can leave child in waitable state - wait3() — like
waitpid(-1,...)but also returns child resource usage - wait4() — like
waitpid(pid,...)but also returns child resource usage
#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
/*
* Returns:
* 0 on success (or WNOHANG with no children ready)
* -1 on error
*/
| idtype | id value | Meaning |
|---|---|---|
| P_ALL | ignored | Wait for any child |
| P_PID | process ID | Wait for child with that specific PID |
| P_PGID | process group ID | Wait for any child in that process group |
waitpid(0,...), you cannot pass 0 to mean “same process group as caller”. You must use getpgrp() explicitly: waitid(P_PGID, getpgrp(), &info, options)| Flag | Description |
|---|---|
| WEXITED | Report children that have terminated (normally or abnormally) |
| WSTOPPED | Report children stopped by a signal |
| WCONTINUED | Report stopped children resumed by SIGCONT |
| WNOHANG | Non-blocking poll; return 0 if no children ready |
| WNOWAIT | Unique to waitid: leave child in waitable state so it can be waited for again later |
| Field | Content |
|---|---|
| si_code | CLD_EXITED, CLD_KILLED, CLD_STOPPED, or CLD_CONTINUED |
| si_pid | PID of the child that changed state |
| si_signo | Always SIGCHLD |
| si_status | Exit code, or signal number (check si_code to know which) |
| si_uid | Real user ID of the child (Linux-specific, most others don’t set this) |
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main(void)
{
pid_t child;
siginfo_t info;
child = fork();
if (child == 0) {
printf("[Child] PID=%d exiting with code 7\n", getpid());
exit(7);
}
/* Wait for any child that exits */
memset(&info, 0, sizeof(siginfo_t));
if (waitid(P_ALL, 0, &info, WEXITED) == -1) {
perror("waitid");
exit(EXIT_FAILURE);
}
printf("[Parent] Child PID : %d\n", info.si_pid);
printf("[Parent] Signal : %d (SIGCHLD)\n", info.si_signo);
switch (info.si_code) {
case CLD_EXITED:
printf("[Parent] Event : Normal exit\n");
printf("[Parent] Exit status : %d\n", info.si_status);
break;
case CLD_KILLED:
printf("[Parent] Event : Killed by signal %d\n", info.si_status);
break;
case CLD_STOPPED:
printf("[Parent] Event : Stopped by signal %d\n", info.si_status);
break;
case CLD_CONTINUED:
printf("[Parent] Event : Continued\n");
break;
}
return 0;
}
WNOWAIT is unique to waitid(). It lets you “peek” at a child’s status without removing it from the waitable state.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main(void)
{
pid_t child;
siginfo_t info;
int status;
child = fork();
if (child == 0) {
exit(42);
}
/* Give child time to exit */
sleep(1);
/* PEEK — does not consume the zombie */
memset(&info, 0, sizeof(siginfo_t));
if (waitid(P_PID, child, &info, WEXITED | WNOWAIT) == -1) {
perror("waitid peek");
exit(EXIT_FAILURE);
}
printf("[Peek] Child PID=%d exited with status=%d\n",
info.si_pid, info.si_status);
/* Child is still a zombie — peek again */
memset(&info, 0, sizeof(siginfo_t));
waitid(P_PID, child, &info, WEXITED | WNOWAIT);
printf("[Peek2] Child PID=%d still waitable, status=%d\n",
info.si_pid, info.si_status);
/* NOW actually consume the zombie with waitpid */
waitpid(child, &status, 0);
printf("[Reap] Zombie reaped.\n");
/* Try to wait again — no more zombie */
if (waitpid(child, &status, 0) == -1)
printf("[Reap2] No child to wait for (as expected)\n");
return 0;
}
#define _BSD_SOURCE
#include <sys/resource.h>
#include <sys/wait.h>
pid_t wait3(int *status, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
The key difference is the rusage argument which returns:
- ru_utime — user CPU time consumed by the child
- ru_stime — system CPU time consumed by the child
- ru_maxrss — peak resident set size (memory)
- and many other fields (see
getrusage(2))
wait3() is equivalent to waitpid(-1, &status, options) plus rusage.
wait4() is equivalent to waitpid(pid, &status, options) plus rusage.
#define _BSD_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/resource.h>
/* Child that burns CPU */
static void burn_cpu(int iterations)
{
volatile long x = 0;
for (int i = 0; i < iterations; i++)
for (int j = 0; j < 1000; j++)
x += j * i;
(void)x;
}
int main(void)
{
pid_t child;
int status;
struct rusage usage;
child = fork();
if (child == 0) {
printf("[Child] Burning CPU...\n");
burn_cpu(100000);
printf("[Child] Done. Exiting.\n");
exit(0);
}
/* wait4 — wait for specific child AND get resource usage */
if (wait4(child, &status, 0, &usage) == -1) {
perror("wait4");
exit(EXIT_FAILURE);
}
if (WIFEXITED(status))
printf("[Parent] Child exited with status %d\n", WEXITSTATUS(status));
printf("[Parent] Child CPU time (user): %ld.%06ld sec\n",
(long)usage.ru_utime.tv_sec,
(long)usage.ru_utime.tv_usec);
printf("[Parent] Child CPU time (system): %ld.%06ld sec\n",
(long)usage.ru_stime.tv_sec,
(long)usage.ru_stime.tv_usec);
printf("[Parent] Child max RSS: %ld KB\n",
usage.ru_maxrss);
return 0;
}
Note: wait3() and wait4() are BSD-derived and not in SUSv3. Avoid them for portable code — use waitpid() + getrusage(RUSAGE_CHILDREN,...) instead.
| Feature | wait() | waitpid() | waitid() | wait3/wait4 |
|---|---|---|---|---|
| Wait for specific child | ✗ | ✓ | ✓ | wait4 only |
| Non-blocking (poll) | ✗ | ✓ WNOHANG | ✓ WNOHANG | ✓ WNOHANG |
| Detect stopped child | ✗ | ✓ WUNTRACED | ✓ WSTOPPED | ✓ WUNTRACED |
| Resource usage | ✗ | ✗ | ✗ | ✓ |
| Peek without consuming | ✗ | ✗ | ✓ WNOWAIT | ✗ |
| POSIX / SUSv3 | ✓ | ✓ | ✓ | ✗ (BSD) |
Q1. What is the main advantage of waitid() over waitpid()?
waitid() provides finer-grained control over which events to wait for (using WEXITED, WSTOPPED, WCONTINUED separately), returns richer information via siginfo_t, and supports WNOWAIT to “peek” without consuming the status.
Q2. What does WNOWAIT do in waitid()?
It returns the child’s current status but leaves the child in a waitable state. A subsequent waitid() or waitpid() call can retrieve the same information again.
Q3. Why must you memset siginfo_t to 0 before calling waitid() with WNOHANG?
When WNOHANG is used and no child has changed state, some implementations leave siginfo_t unchanged. Zeroing first lets you check if si_pid == 0, which reliably indicates “no child ready”.
Q4. How do you use waitid() to wait for a specific process group?
waitid(P_PGID, (id_t)getpgrp(), &info, WEXITED);
Q5. What is the key difference between wait3() and wait4()?
wait3() waits for ANY child. wait4() waits for a SPECIFIC child (selected by pid). Both return resource usage via rusage.
Q6. Why should wait3()/wait4() be avoided in portable code?
They originated in BSD and are not in SUSv3/POSIX. For portable code, use waitpid() for waiting and getrusage(RUSAGE_CHILDREN) for resource stats.
Q7. What si_code value does waitid() set when a child is killed by a signal?
CLD_KILLED. The signal number that killed the child is in si_status.
Q8. What happens if you waitid() with WEXITED only and the child is stopped (not exited)?
The call blocks until the child actually terminates (or returns 0 if WNOHANG is also set), because the stop event doesn’t match the requested events.
