Subtopic 2
Pattern
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.
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);
}
}
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
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;
}
#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;
}
(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().
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.
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.
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.
