Managing Signal Dispositions Across Fork and Exec
Learn how signal dispositions are inherited during fork() and reset during exec(), and how to use SIG_IGN to preserve process behavior across binary replacements.
15 Nov 2025, 09:57 UTC

The Problem: Signal Inheritance and Execution
When a Unix process creates a child using fork() and then replaces that child's memory image with a new program via exec(), the signal dispositions (how the process reacts to a signal) do not always persist as expected. If you register a custom signal handler in the parent, the child inherits it, but the exec() call resets any handler that points to a memory address in the previous program. This often leads to programs crashing or ignoring critical signals because the handler was wiped during the transition to the new binary.
The takeaway: To ensure a child process handles a signal consistently after an exec(), you must set the signal disposition to SIG_IGN (ignore) or SIG_DFL (default) before calling exec(). If a custom behavior is required in the new program, that program must register its own handlers upon startup.
How Signal Dispositions Behave
Understanding the transition requires distinguishing between the two system calls:
- fork(): The child process is a near-exact duplicate. It inherits the parent's signal mask and the current disposition of all signals.
- exec(): The process image is replaced. Because the custom handler function existed in the parent's code segment (which is now gone), any signal set to a custom function is reset to
SIG_DFL. However, signals set toSIG_IGNremain ignored across theexec()boundary.
Worked Example: Preserving Signal State
The following C example demonstrates how to ensure a child process ignores SIGUSR1 even after executing a different binary. This is a common pattern used to prevent a child from being interrupted by parent-driven signals during its initialization phase.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// CHILD PROCESS
printf("Child: Ignoring SIGUSR1 before exec...\n");
// Set disposition to ignore. This persists across exec().
signal(SIGUSR1, SIG_IGN);
// Replace process image with /bin/sleep for 10 seconds
char *args[] = {"/bin/sleep", "10", NULL};
execv(args[0], args);
// If execv returns, it failed
perror("execv failed");
_exit(EXIT_FAILURE);
} else {
// PARENT PROCESS
sleep(1); // Give child time to exec
printf("Parent: Sending SIGUSR1 to child (PID %d)...\n", pid);
// This signal will be ignored by the child because of SIG_IGN
kill(pid, SIGUSR1);
wait(NULL); // Reap child to prevent zombie process
printf("Parent: Child finished.\n");
}
return 0;
}
Implementation Details
| Action | Required Permissions | Expected Result |
|---|---|---|
Compile with gcc -Wall -o sig_test sig_test.c |
User write access to directory | Binary sig_test created |
Run ./sig_test |
Execute permission | Child ignores signal; parent reaps child |
Run strace -f ./sig_test |
Root or CAP_SYS_PTRACE | Trace shows rt_sigaction call in child |
Critical Constraints and Common Mistakes
Async-Signal Safety
If you implement a custom handler (before exec() or within the new program), you must only call async-signal-safe functions. These are functions that can be safely interrupted and called again without causing deadlocks or memory corruption.
- Safe:
write(),_exit(),wait(). - Unsafe:
printf(),malloc(),free(). These use internal locks or global state that can lead to a deadlock if the signal interrupts the process while it is already inside one of those functions.
The Zombie Process Trap
A common engineering failure is forgetting to handle SIGCHLD or calling wait(). When a child process terminates, it enters a "zombie" state (marked as Z in ps). It remains in the system process table so the parent can read its exit status. If the parent ignores this, the process table can fill up, preventing the system from spawning new processes.
Verification and Diagnostics
To verify the current signal disposition of a running process, use the ps command or inspect the /proc filesystem on Linux:
# Replace [PID] with the actual child PID
ps -o pid,stat,cmd -p [PID]
If the status is Z, the process is a zombie and the parent has failed to reap it. To verify that a signal was ignored or handled, strace -f is the most reliable method, as it logs the exact signal delivery and the resulting action taken by the kernel.
Rollback and Recovery
Because exec() replaces the entire process image, there is no "rollback" to the previous state. To recover a signal disposition to its default state within a program, use:
signal(SIGUSR1, SIG_DFL);0 replies
A thoughtful contribution can make all the difference. Be the first to share one.