Signal Sync: Full Implementation

Signal Sync: Full Implementation
Topic 7 → Subtopic 2  |  fork_sig_sync.c from TLPI — complete production-ready pattern
Topic 7
Subtopic 2
Production
Pattern
3
Code Examples

The Complete fork_sig_sync.c Pattern

This subtopic implements the textbook’s fork_sig_sync.c pattern from TLPI Chapter 24 — the complete, production-ready signal synchronization implementation for coordinating parent and child after fork(). We also extend it with real-world variations: multiple children, pipe-based sync, and a reusable sync helper.

Keywords:

fork_sig_sync.c sigsuspend() sigprocmask() sigaction() SIGUSR1 pipe sync multiple children sync helper

💻 Code Example 1: fork_sig_sync.c (from TLPI Chapter 24)

This is the complete textbook implementation. Parent performs setup, then signals child to proceed. Child waits atomically using sigsuspend():

/* fork_sig_sync.c — from TLPI Chapter 24
   Demonstrates parent → child signal synchronization */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>

#define SYNC_SIG SIGUSR1  /* signal used for synchronization */

static volatile sig_atomic_t sigReceived = 0;

/* Signal handler: just records that signal arrived */
static void handler(int sig)
{
    (void)sig;
    sigReceived = 1;
}

int main(int argc, char *argv[])
{
    pid_t childPid;
    sigset_t blockMask, origMask, emptyMask;
    struct sigaction sa;

    setbuf(stdout, NULL);  /* disable buffering */

    /* Install signal handler for SYNC_SIG */
    sigemptyset(&sa.sa_mask);
    sa.sa_flags   = SA_RESTART;
    sa.sa_handler = handler;
    if (sigaction(SYNC_SIG, &sa, NULL) == -1) {
        perror("sigaction"); exit(EXIT_FAILURE);
    }

    /* Block SYNC_SIG so child can't receive it before sigsuspend() */
    sigemptyset(&blockMask);
    sigaddset(&blockMask, SYNC_SIG);
    if (sigprocmask(SIG_BLOCK, &blockMask, &origMask) == -1) {
        perror("sigprocmask"); exit(EXIT_FAILURE);
    }

    /* Empty mask: used in sigsuspend — allows all signals */
    sigemptyset(&emptyMask);

    switch (childPid = fork()) {
    case -1:
        perror("fork"); exit(EXIT_FAILURE);

    case 0:
        /* CHILD: wait for parent to send SYNC_SIG */
        printf("[Child ] Waiting for parent sync signal...\n");

        /* Atomically unblock SYNC_SIG and sleep until it arrives */
        if (sigsuspend(&emptyMask) == -1 && errno != EINTR) {
            perror("sigsuspend"); _exit(EXIT_FAILURE);
        }
        /* We get here when SYNC_SIG was received */
        printf("[Child ] Sync received. sigReceived=%d\n",
               sigReceived);

        /* Child can now do its work safely */
        printf("[Child ] Doing work that requires parent setup.\n");
        _exit(EXIT_SUCCESS);

    default:
        /* PARENT: do setup work */
        printf("[Parent] Doing parent setup work...\n");
        sleep(2);  /* simulate real setup */
        printf("[Parent] Setup done. Signaling child (PID=%d)\n",
               (int)childPid);

        /* Send sync signal to child */
        if (kill(childPid, SYNC_SIG) == -1) {
            perror("kill"); exit(EXIT_FAILURE);
        }

        /* Restore original signal mask in parent */
        if (sigprocmask(SIG_SETMASK, &origMask, NULL) == -1) {
            perror("sigprocmask restore"); exit(EXIT_FAILURE);
        }

        /* Wait for child to complete */
        if (wait(NULL) == -1) {
            perror("wait"); exit(EXIT_FAILURE);
        }
        printf("[Parent] Child done.\n");
        exit(EXIT_SUCCESS);
    }
}
Key design points:
1. sigaction() instead of signal() — more portable and reliable
2. sigprocmask() blocks SYNC_SIG BEFORE fork — prevents the race
3. sigsuspend(&emptyMask) — atomic unblock + wait
4. sigprocmask(SIG_SETMASK, &origMask) in parent — restores original mask

💻 Code Example 2: Pipe-Based Sync (Simpler Alternative)

Pipes provide another clean synchronization mechanism — simpler than signals and works across non-related processes too:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

/* A "sync pipe": write 1 byte to signal ready;
   read 1 byte to wait for ready */

int main(void)
{
    int pipe_p2c[2];  /* parent to child pipe */
    int pipe_c2p[2];  /* child to parent pipe */

    setbuf(stdout, NULL);

    if (pipe(pipe_p2c) == -1 || pipe(pipe_c2p) == -1) {
        perror("pipe"); exit(1);
    }

    pid_t pid = fork();
    if (pid == -1) { perror("fork"); exit(1); }

    if (pid == 0) {
        /* Close unused ends */
        close(pipe_p2c[1]);  /* close write end of p2c */
        close(pipe_c2p[0]);  /* close read  end of c2p */

        /* Wait for parent's "ready" byte */
        char sync_byte;
        printf("[Child ] Waiting for parent...\n");
        read(pipe_p2c[0], &sync_byte, 1);
        printf("[Child ] Parent ready! Doing child work.\n");

        /* Signal child is done */
        write(pipe_c2p[1], "R", 1);

        close(pipe_p2c[0]);
        close(pipe_c2p[1]);
        _exit(0);
    }

    /* Close unused ends */
    close(pipe_p2c[0]);  /* close read  end of p2c */
    close(pipe_c2p[1]);  /* close write end of c2p */

    /* Parent does setup */
    printf("[Parent] Doing setup work...\n");
    sleep(2);
    printf("[Parent] Setup done. Signaling child.\n");

    /* Tell child we're ready */
    write(pipe_p2c[1], "R", 1);

    /* Wait for child to acknowledge */
    char sync_byte;
    read(pipe_c2p[0], &sync_byte, 1);
    printf("[Parent] Child acknowledged. Both done.\n");

    close(pipe_p2c[1]);
    close(pipe_c2p[0]);
    wait(NULL);
    return 0;
}
Advantage over signals: Pipe sync works even when processes are not parent-child (e.g., sibling processes or unrelated processes). It also naturally handles the case where the sender sends before the receiver is ready — the byte stays in the pipe.

💻 Code Example 3: Reusable Sync Helper for Multiple Children
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>

/* ---- Reusable synchronization helper ---- */

static volatile sig_atomic_t _sync_sig_got = 0;
static void _sync_handler(int s) { (void)s; _sync_sig_got = 1; }

/* Call before fork(): sets up handler and blocks SIGUSR1.
   Returns original signal mask in orig_mask. */
void sync_setup(sigset_t *orig_mask)
{
    struct sigaction sa;
    sigset_t block;

    sa.sa_handler = _sync_handler;
    sa.sa_flags   = SA_RESTART;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGUSR1, &sa, NULL);

    sigemptyset(&block);
    sigaddset(&block, SIGUSR1);
    sigprocmask(SIG_BLOCK, &block, orig_mask);
}

/* Call in child: wait for SIGUSR1 from parent */
void sync_wait(void)
{
    sigset_t empty;
    sigemptyset(&empty);
    _sync_sig_got = 0;
    while (!_sync_sig_got)
        sigsuspend(&empty);
}

/* Call in parent: signal a child it may proceed */
void sync_signal(pid_t child_pid)
{
    kill(child_pid, SIGUSR1);
}

/* ---- Application code ---- */

int main(void)
{
    sigset_t orig_mask;
    pid_t pids[3];

    setbuf(stdout, NULL);
    sync_setup(&orig_mask);

    /* Create 3 children, all waiting for parent's signal */
    for (int i = 0; i < 3; i++) {
        switch (pids[i] = fork()) {
        case -1: perror("fork"); exit(1);
        case 0:
            printf("[Child %d PID=%d] Waiting for go signal...\n",
                   i, getpid());
            sync_wait();
            printf("[Child %d PID=%d] Go received! Working.\n",
                   i, getpid());
            sleep(1);
            _exit(i);  /* exit with index as status */
        default: break;
        }
    }

    /* Parent: do setup */
    printf("[Parent] Doing setup work...\n");
    sleep(2);
    printf("[Parent] Setup done. Signaling all children.\n");

    /* Signal each child to start */
    for (int i = 0; i < 3; i++) {
        sync_signal(pids[i]);
        printf("[Parent] Signaled child %d (PID=%d)\n", i, pids[i]);
    }

    /* Restore mask in parent */
    sigprocmask(SIG_SETMASK, &orig_mask, NULL);

    /* Wait for all children */
    int status;
    for (int i = 0; i < 3; i++) {
        pid_t done = wait(&status);
        printf("[Parent] Child PID=%d exited: %d\n",
               done, WEXITSTATUS(status));
    }

    printf("[Parent] All children done.\n");
    return 0;
}
Reusable helper pattern: sync_setup() / sync_wait() / sync_signal() form a clean API. The parent calls sync_setup() once, creates N children (all call sync_wait()), does its setup, then calls sync_signal() for each child. All children start together after parent setup is complete.

🅾 Interview Questions
Q1: Walk through the complete steps of fork_sig_sync.c from TLPI.

(1) Install SIGUSR1 handler. (2) Block SIGUSR1 with sigprocmask() before fork. (3) fork(). (4) Child calls sigsuspend(&emptyMask) — atomically unblocks SIGUSR1 and sleeps. (5) Parent does setup, then calls kill(childPid, SIGUSR1). (6) Child’s sigsuspend returns (EINTR). (7) Child proceeds with work. (8) Parent restores original signal mask with sigprocmask(SIG_SETMASK, &origMask). (9) Parent calls wait().

Q2: What is the advantage of pipe-based sync over signal-based sync?

Pipes are simpler: no signal handler setup, no sigprocmask, no sigsuspend needed. A byte in a pipe is naturally queued — if the sender writes before the receiver reads, the byte waits in the pipe buffer. Pipes also work across unrelated processes, not just parent-child. Signals can be lost if not carefully blocked before fork.

Q3: Why use sigaction() instead of signal() for sync handlers?

sigaction() is more portable and reliable. It lets you specify SA_RESTART to automatically restart interrupted system calls. It also lets you specify which signals are blocked during handler execution (sa_mask). signal()’s behavior for SA_RESTART varies by system and is undefined on some platforms. Always prefer sigaction() in production code.

Q4: What does sigsuspend() return and how do you check for success?

sigsuspend() always returns -1 and sets errno to EINTR when it is interrupted by a signal. This is the normal, expected return. Check for failure with if (sigsuspend(&mask) == -1 && errno != EINTR) — only error cases set errno to something other than EINTR.

Series Complete!
Topic 7 → Subtopic 2 of 2  |  Chapter 24 fully covered

← Previous 🏠 Back to Index

Leave a Reply

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