Signal Sync
of 2
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.
The correct pattern for using signals to synchronize after fork() has 3 critical steps:
Use
sigprocmask(SIG_BLOCK, ...) to block SIGUSR1 before forking. This ensures the signal isn’t delivered before the receiver sets up sigsuspend().The “sender” (parent or child) does its setup work, then calls
kill(pid, SIGUSR1) to notify the other process it is ready.The “waiter” calls
sigsuspend() which atomically unblocks SIGUSR1 and sleeps until it arrives. Using pause() has a race window — sigsuspend() doesn’t.#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;
}
#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;
}
#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;
}
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.
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.
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.
