waitpid() Extensions for Cloned Children

Chapter 28.2.2 — TLPI
waitpid() Extensions for Cloned Children
__WCLONE, __WALL, and __WNOTHREAD — waiting for non-standard clone() children
__WCLONE
Clone-only wait
__WALL
Wait all types
__WNOTHREAD
Own children only

The Problem: Waiting for clone() Children

The normal waitpid() call waits for children that send SIGCHLD when they terminate. When you use clone() and specify a different termination signal (or no signal at all), regular waitpid() will not see those children. Linux adds three special option flags to handle this.

Note: These flags work with waitpid(), wait3(), and wait4(), but NOT with waitid().

What Is a “Clone Child”?

In the context of these flags, a clone child is a child process created by clone() that terminates by delivering a signal other than SIGCHLD to its parent. A child created with SIGCHLD as the termination signal is treated as a normal (non-clone) child by waitpid().

Child Type Termination Signal Seen by plain waitpid()? Needs flag
Normal / fork() child SIGCHLD ✅ Yes None
clone() child with SIGCHLD SIGCHLD ✅ Yes None
clone() child with SIGUSR1 SIGUSR1 ❌ No __WCLONE or __WALL
clone() child with signal 0 None (0) ❌ No __WCLONE or __WALL

The Three Extension Flags

__WCLONE — Wait for Clone Children Only

When __WCLONE is set, waitpid() waits only for clone children — those that deliver a signal other than SIGCHLD. Normal fork() children and SIGCHLD-terminated clone children are ignored.

Ignored if __WALL is also specified.

__WALL — Wait for All Children (Clone and Non-Clone)

When __WALL is set, waitpid() waits for any child regardless of termination signal — both normal children (SIGCHLD) and clone children. Use this when you have a mix of child types and want one waitpid() to catch them all.

__WNOTHREAD — Limit to Calling Process’s Children Only

By default, waitpid() can reap children of any thread in the same thread group, not just the calling thread. This is required by POSIX (“any thread may wait for any child of the process”). Setting __WNOTHREAD limits the wait to only the calling thread’s own children.

Available: Linux 2.4 onward.

waitpid() Flag Decision Logic

waitpid(-1, &status, options)
Is __WALL set?
YES → __WALL
Wait for ALL children (clone + non-clone). __WCLONE is ignored.
OR
NO → Check __WCLONE
__WCLONE SET
Wait for clone children only
(non-SIGCHLD terminators)
__WCLONE NOT SET
Wait for non-clone children only
(SIGCHLD terminators — normal behavior)
Additionally: add __WNOTHREAD to restrict to only THIS thread’s children (not thread group’s children)

Example 1 — Using __WCLONE for Non-SIGCHLD Child

/* wclone_demo.c — Use __WCLONE to wait for a custom-signal clone child */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sched.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#define STACK_SIZE (64 * 1024)
#define CHILD_SIG   SIGUSR1   /* Non-standard termination signal */

static int child_func(void *arg)
{
    printf("[Child] running, PID=%d\n", getpid());
    sleep(1);
    return 77;   /* exit status */
}

int main(void)
{
    char  *stack     = malloc(STACK_SIZE);
    char  *stack_top = stack + STACK_SIZE;
    int    status;
    pid_t  child_pid, waited_pid;

    /* Must ignore CHILD_SIG or it will terminate the parent */
    if (signal(CHILD_SIG, SIG_IGN) == SIG_ERR) {
        perror("signal"); exit(1);
    }

    /* Lower byte = SIGUSR1 as termination signal */
    child_pid = clone(child_func, stack_top, CHILD_SIG, NULL);
    if (child_pid == -1) { perror("clone"); exit(1); }

    printf("[Parent] created clone child PID=%d with SIGUSR1\n", child_pid);

    /* ---- WRONG: plain waitpid() will NOT see this child ---- */
    waited_pid = waitpid(child_pid, &status, WNOHANG);
    printf("[Parent] plain waitpid result: %d (0 = no child ready, "
           "-1 = error: %s)\n", waited_pid,
           waited_pid == -1 ? strerror(errno) : "n/a");

    /* ---- CORRECT: use __WCLONE ---- */
    printf("[Parent] waiting with __WCLONE...\n");
    waited_pid = waitpid(child_pid, &status, __WCLONE);
    if (waited_pid == -1) { perror("waitpid __WCLONE"); exit(1); }

    printf("[Parent] reaped PID=%d, exit status=%d\n",
           waited_pid, WEXITSTATUS(status));

    free(stack);
    return 0;
}

Example 2 — __WALL to Handle Mixed Children

/* wall_demo.c — Parent has both fork() and clone() children; use __WALL */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sched.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>

#define STACK_SIZE (64 * 1024)

static int clone_child(void *arg)
{
    printf("[Clone child] PID=%d, sleeping 1s...\n", getpid());
    sleep(1);
    return 11;
}

int main(void)
{
    char  *stack     = malloc(STACK_SIZE);
    char  *stack_top = stack + STACK_SIZE;

    /* Ignore SIGUSR1 so it doesn't kill parent */
    signal(SIGUSR1, SIG_IGN);

    /* Create a normal fork() child */
    pid_t fork_child = fork();
    if (fork_child == 0) {
        printf("[Fork child]  PID=%d, sleeping 2s...\n", getpid());
        sleep(2);
        exit(22);
    }

    /* Create a clone() child with SIGUSR1 termination signal */
    pid_t clone_pid = clone(clone_child, stack_top, SIGUSR1, NULL);
    if (clone_pid == -1) { perror("clone"); exit(1); }

    printf("[Parent] fork child=%d, clone child=%d\n",
           fork_child, clone_pid);

    /* Reap both children using __WALL */
    int reaped = 0;
    while (reaped < 2) {
        int    status;
        pid_t  pid = waitpid(-1, &status, __WALL);
        if (pid == -1) { perror("waitpid"); break; }

        printf("[Parent] reaped PID=%d, exit=%d\n",
               pid, WEXITSTATUS(status));
        reaped++;
    }

    free(stack);
    return 0;
}
/*
 * __WALL reaps BOTH the fork() child (SIGCHLD) and
 * the clone() child (SIGUSR1) — no need for separate calls.
 */

Example 3 — __WNOTHREAD to Restrict to Own Children

/* wnothread_demo.c — Demonstrate __WNOTHREAD limiting wait scope */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sched.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

#define STACK_SIZE (64 * 1024)

/*
 * Scenario:
 *  Thread A creates child X.
 *  Thread B tries to reap child X with __WNOTHREAD.
 *  Result: Thread B CANNOT reap X (not its own child).
 *
 * Without __WNOTHREAD, any thread in the group can reap any child.
 */

static int grandchild_func(void *arg)
{
    sleep(1);
    return 55;
}

static int thread_b_func(void *arg)
{
    pid_t child_pid = *((pid_t *) arg);
    int   status;

    printf("[Thread B] trying to reap child %d with __WNOTHREAD\n",
           child_pid);

    /* __WNOTHREAD: only wait for children of THIS thread */
    pid_t result = waitpid(child_pid, &status, __WNOTHREAD | __WALL);
    if (result == -1)
        printf("[Thread B] waitpid failed: %s (expected — not our child)\n",
               strerror(errno));
    else
        printf("[Thread B] unexpectedly reaped %d\n", result);
    return 0;
}

int main(void)
{
    char  *stA = malloc(STACK_SIZE), *stB = malloc(STACK_SIZE);
    pid_t  child_pid, thread_b_pid;
    int    status;

    signal(SIGUSR1, SIG_IGN);

    /* Thread A (main) creates child X */
    child_pid = clone(grandchild_func, stA + STACK_SIZE, SIGUSR1, NULL);
    if (child_pid == -1) { perror("clone child"); exit(1); }
    printf("[Main/ThreadA] created child X = %d\n", child_pid);

    /* Thread B tries to reap X using __WNOTHREAD */
    thread_b_pid = clone(thread_b_func, stB + STACK_SIZE,
                         CLONE_VM | CLONE_SIGHAND | CLONE_THREAD,
                         &child_pid);
    sleep(2);   /* Wait for thread B to attempt its wait */

    /* Main reaps child X (Thread A can do it) */
    waitpid(child_pid, &status, __WCLONE);
    printf("[Main/ThreadA] reaped child X=%d, exit=%d\n",
           child_pid, WEXITSTATUS(status));

    free(stA); free(stB);
    return 0;
}

Key Rules Summary

  • A clone child = terminates with signal ≠ SIGCHLD
  • Plain waitpid() only sees SIGCHLD children
  • __WCLONE: only clone children (non-SIGCHLD)
  • __WALL: all children (SIGCHLD + non-SIGCHLD); overrides __WCLONE
  • __WNOTHREAD: restrict to THIS thread’s children (not thread group)
  • These flags do NOT work with waitid()
  • For CLONE_THREAD children: use pthread_join() / futex, not waitpid()

Interview Questions

Q1. Why can’t you use plain waitpid() to wait for a clone() child that uses SIGUSR1?
By default, waitpid() only waits for children that deliver SIGCHLD on termination — that is what “normal” fork() children do. A clone() child configured with SIGUSR1 as its termination signal will send SIGUSR1 to the parent, not SIGCHLD. The waitpid() system call therefore does not see it and will return ECHILD or block waiting for a different child. You must add __WCLONE or __WALL to the options to make it visible.
Q2. When would you use __WALL instead of __WCLONE?
Use __WALL when a parent process has a mix of child types — some created with fork() (SIGCHLD) and some created with clone() using a different signal. __WALL reaps all of them with a single waitpid() call. __WCLONE only reaps clone children with non-SIGCHLD signals, so it would miss fork() children. When __WALL is specified, __WCLONE is ignored.
Q3. What does __WNOTHREAD change about waitpid() behavior?
By default, any thread in a thread group can wait for any child of any thread in that group — POSIX requires this. __WNOTHREAD overrides this: it restricts waitpid() to only wait for children created by the calling thread itself. This is useful in special scenarios where you need strict ownership of child reaping, preventing other threads from accidentally reaping children they did not create.
Q4. Why can’t you use waitpid() at all for CLONE_THREAD children?
CLONE_THREAD places the child in the same thread group as the parent. According to POSIX semantics, threads within a group are not children of each other — they are siblings. waitpid() can only wait for children (processes where the waited process’s PPID == calling process’s PID). A CLONE_THREAD sibling does not satisfy this. Instead, pthread_join() (which internally uses a futex on ctid) is the correct mechanism.
Q5. What happens if you forget to wait for a clone child that uses SIGUSR1?
The child becomes a zombie — it has terminated but its entry remains in the process table because the parent has not collected its exit status via waitpid(). This consumes a process table slot. The zombie is cleared only when the parent calls waitpid() with __WCLONE/__WALL, or when the parent itself exits (at which point init/systemd adopts and reaps the zombie).

Leave a Reply

Your email address will not be published. Required fields are marked *