Synchronizing with Signals After fork()

Synchronizing with Signals After fork()
Topic 7 → Subtopic 1  |  Using SIGUSR1 to coordinate parent and child
Topic 7
Signal Sync
Subtopic 1
of 2
3
Code Examples

Why Signal-Based Synchronization?

After fork(), if you need the child to run after the parent completes some setup (or the parent to wait until the child is ready), the cleanest lightweight mechanism is signals. One process blocks waiting for a specific signal; the other sends it when ready. No pipes, no shared memory needed.

Keywords:

SIGUSR1 sigsuspend() sigprocmask() sigaction() kill() signal handler block before fork atomic wait

💡 The Signal Synchronization Pattern

The correct pattern for using signals to synchronize after fork() has 3 critical steps:

1
Block the signal BEFORE fork()
Use sigprocmask(SIG_BLOCK, ...) to block SIGUSR1 before forking. This ensures the signal isn’t delivered before the receiver sets up sigsuspend().
2
Do work, then send the signal
The “sender” (parent or child) does its setup work, then calls kill(pid, SIGUSR1) to notify the other process it is ready.
3
Wait with sigsuspend(), not pause()
The “waiter” calls sigsuspend() which atomically unblocks SIGUSR1 and sleeps until it arrives. Using pause() has a race window — sigsuspend() doesn’t.

⚠ Why sigsuspend() and Not pause()?
✗ pause() — Race Condition
unblock SIGUSR1;
/* signal arrives HERE! */
pause(); /* never wakes up */
Gap between unblock and pause = signal can arrive and be lost
✓ sigsuspend() — Atomic
/* atomic: unblock + sleep */
sigsuspend(&empty_mask);
/* wakes when signal arrives */
Unblock and sleep are a single atomic operation — no race

💻 Code Example 1: Parent Notifies Child with SIGUSR1
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

/* Signal handler for synchronization */
static volatile sig_atomic_t got_signal = 0;

void sync_handler(int sig)
{
    (void)sig;
    got_signal = 1;
}

int main(void)
{
    sigset_t block_mask, empty_mask;
    struct sigaction sa;

    setbuf(stdout, NULL);

    /* Install SIGUSR1 handler */
    sa.sa_handler = sync_handler;
    sa.sa_flags   = SA_RESTART;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGUSR1, &sa, NULL);

    /* Block SIGUSR1 BEFORE fork to prevent race */
    sigemptyset(&block_mask);
    sigaddset(&block_mask, SIGUSR1);
    sigprocmask(SIG_BLOCK, &block_mask, NULL);
    sigemptyset(&empty_mask);  /* mask with nothing blocked */

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

    if (pid == 0) {
        /* CHILD: wait for parent's signal before proceeding */
        printf("[Child ] Waiting for parent to be ready...\n");

        /* Atomically: unblock SIGUSR1 and sleep until it arrives */
        sigsuspend(&empty_mask);

        printf("[Child ] Got SIGUSR1! Parent is ready. Proceeding.\n");
        _exit(0);
    }

    /* PARENT: do setup work */
    printf("[Parent] Doing setup (3 seconds)...\n");
    sleep(3);  /* simulate setup */
    printf("[Parent] Setup done. Signaling child.\n");

    /* Send SIGUSR1 to child */
    kill(pid, SIGUSR1);

    wait(NULL);
    printf("[Parent] Child finished.\n");
    return 0;
}
Output is deterministic: Child always waits until parent signals. No sleep() anti-patterns. sigsuspend() guarantees no signal is missed.

💻 Code Example 2: Child Notifies Parent When Ready
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

static volatile sig_atomic_t child_ready = 0;

void ready_handler(int sig) { (void)sig; child_ready = 1; }

int main(void)
{
    sigset_t block_mask, empty_mask;
    struct sigaction sa;

    setbuf(stdout, NULL);

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

    /* Block SIGUSR1 before fork */
    sigemptyset(&block_mask);
    sigaddset(&block_mask, SIGUSR1);
    sigprocmask(SIG_BLOCK, &block_mask, NULL);
    sigemptyset(&empty_mask);

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

    if (child_pid == 0) {
        /* Child: do initialization work */
        printf("[Child ] Initializing (2 seconds)...\n");
        sleep(2);
        printf("[Child ] Ready. Notifying parent.\n");

        /* Signal parent we are ready */
        kill(getppid(), SIGUSR1);

        /* Continue child work */
        printf("[Child ] Doing child work...\n");
        sleep(1);
        _exit(0);
    }

    /* Parent: wait for child to signal readiness */
    printf("[Parent] Waiting for child to be ready...\n");

    /* Block until SIGUSR1 arrives */
    sigsuspend(&empty_mask);

    printf("[Parent] Child is ready! Proceeding with parent work.\n");

    /* Re-block SIGUSR1 for cleanup */
    sigprocmask(SIG_BLOCK, &block_mask, NULL);

    wait(NULL);
    printf("[Parent] Done.\n");
    return 0;
}

💻 Code Example 3: Mutual Sync — Both Signal Each Other
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

static volatile sig_atomic_t sig_received = 0;
void sig_handler(int s) { (void)s; sig_received = 1; }

int main(void)
{
    sigset_t block_mask, empty_mask;
    struct sigaction sa;
    setbuf(stdout, NULL);

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

    sigemptyset(&block_mask);
    sigaddset(&block_mask, SIGUSR1);
    sigprocmask(SIG_BLOCK, &block_mask, NULL);
    sigemptyset(&empty_mask);

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

    if (child_pid == 0) {
        /* Phase 1: child setup */
        printf("[Child ] Phase 1: setup done. Signaling parent.\n");
        kill(getppid(), SIGUSR1);  /* tell parent phase 1 done */

        /* Wait for parent's phase 1 */
        sig_received = 0;
        sigsuspend(&empty_mask);
        printf("[Child ] Parent phase 1 done. Doing phase 2.\n");
        _exit(0);
    }

    /* Parent phase 1 */
    printf("[Parent] Phase 1: setup done. Signaling child.\n");
    kill(child_pid, SIGUSR1);  /* tell child phase 1 done */

    /* Wait for child's phase 1 */
    sig_received = 0;
    sigsuspend(&empty_mask);
    printf("[Parent] Child phase 1 done. Doing phase 2.\n");

    wait(NULL);
    return 0;
}
Pattern: Each process signals the other then waits. This implements a simple barrier synchronization: neither proceeds to phase 2 until both have completed phase 1.

🅾 Interview Questions
Q1: Why must you block the synchronization signal BEFORE calling fork()?

If the signal is not blocked before fork(), a race exists: the sender might send the signal before the receiver calls sigsuspend(). If the signal arrives before sigsuspend(), it is delivered to the default handler (or ignored) and the receiver will block in sigsuspend() forever. Blocking before fork() ensures the signal is queued and only delivered when sigsuspend() is called.

Q2: What is the difference between pause() and sigsuspend() for signal waiting?

pause() simply suspends the process until any signal arrives. sigsuspend(mask) atomically replaces the signal mask with mask and suspends the process. This is critical because with pause(), you must first unblock the signal then call pause() — and the signal could arrive in between (race). sigsuspend() makes unblock+wait atomic, eliminating the race.

Q3: What signals are typically used for process synchronization?

SIGUSR1 and SIGUSR2 are reserved for application-defined use and are the standard choice for process synchronization. They have no predefined meaning in the kernel. SIGCHLD can be used for parent-child synchronization but has additional semantics. Avoid using SIGTERM, SIGINT, or other standard signals for application sync.

Series Navigation
Topic 7 → Subtopic 1 of 2

← Previous Next: Full Implementation → Index

Leave a Reply

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