Chapter 27.5 — Signals and exec()
Signal Handlers Reset · Signal Mask Preserved · SA_ONSTACK · EmbeddedPathashala
📌 Topic
Signals & exec()
Signals & exec()
🧠 Level
Intermediate
Intermediate
💻 Examples
3 Programs
3 Programs
❓ Q&A
7 Questions
7 Questions
Signals Across exec() — What Changes, What Stays
When exec() replaces your program, signal handlers can’t survive — the function pointers point to old code that’s now gone. But the signal mask (which signals are blocked) and pending signals do survive.
This has important practical consequences you need to understand for robust system programming.
Signal State Before and After exec()
| Signal Attribute | After exec() | Reason |
|---|---|---|
| Custom signal handlers | ❌ Reset to SIG_DFL | Handler function lived in old program code — now gone |
| Signals set to SIG_IGN | ✅ Stay as SIG_IGN | SIG_IGN is not a function pointer, just a flag |
| Signal mask (blocked signals) | ✅ Preserved | Stored in kernel, not in process memory |
| Pending signals | ✅ Preserved | Waiting to be delivered to new program |
| Alternate signal stack (sigaltstack) | ❌ Lost | Stack was in old process memory |
| SA_ONSTACK flag | ❌ Cleared for all signals | Alternate stack gone, flag meaningless |
⚠️ SIGCHLD Portability Warning
POSIX leaves it unspecified whether an ignored SIGCHLD remains ignored after exec(). Linux keeps it ignored, but Solaris resets it to SIG_DFL.
Best practice for portable code:
signal(SIGCHLD, SIG_DFL); /* Reset before exec for portability */
execve(path, argv, envp);
Example 1: Proving Handler is Reset After exec()
/* example1_signal_reset.c
* gcc -o ex1 example1_signal_reset.c
* gcc -o show_signal_disp show_signal_disp.c
* ./ex1 ./show_signal_disp
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
void my_sigint_handler(int sig)
{
printf("Custom SIGINT handler called in parent!\n");
}
int main(int argc, char *argv[])
{
struct sigaction sa_before, sa_after;
pid_t child;
int status;
if (argc != 2) {
fprintf(stderr, "Usage: %s <path-to-show_signal_disp>\n", argv[0]);
exit(1);
}
/* Install custom SIGINT handler */
struct sigaction sa = {0};
sa.sa_handler = my_sigint_handler;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGINT, &sa, NULL) == -1) { perror("sigaction"); exit(1); }
/* Check disposition before exec */
sigaction(SIGINT, NULL, &sa_before);
printf("Before exec: SIGINT handler = %s\n",
(sa_before.sa_handler == SIG_DFL) ? "SIG_DFL" :
(sa_before.sa_handler == SIG_IGN) ? "SIG_IGN" :
"Custom Handler");
/* Also ignore SIGQUIT */
signal(SIGQUIT, SIG_IGN);
printf("Before exec: SIGQUIT = SIG_IGN (explicitly ignored)\n\n");
printf("Exec'ing show_signal_disp to check signal dispositions...\n\n");
child = fork();
if (child == -1) { perror("fork"); exit(1); }
if (child == 0) {
char *argvec[] = { argv[1], NULL };
char *envp[] = { NULL };
execve(argv[1], argvec, envp);
perror("execve"); exit(127);
}
waitpid(child, &status, 0);
return 0;
}
/* ===== show_signal_disp.c ===== */
/* gcc -o show_signal_disp show_signal_disp.c */
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
int main(void)
{
struct sigaction sa;
int sigs[] = {SIGINT, SIGQUIT, SIGTERM, SIGUSR1, 0};
const char *names[] = {"SIGINT", "SIGQUIT", "SIGTERM", "SIGUSR1", NULL};
printf("=== Signal dispositions in exec'd program ===\n");
for (int i = 0; sigs[i]; i++) {
sigaction(sigs[i], NULL, &sa);
printf(" %-10s = %s\n", names[i],
(sa.sa_handler == SIG_DFL) ? "SIG_DFL (default)" :
(sa.sa_handler == SIG_IGN) ? "SIG_IGN (ignored)" :
"Custom handler");
}
return 0;
}
/* Expected output:
* Before exec: SIGINT handler = Custom Handler
* Before exec: SIGQUIT = SIG_IGN (explicitly ignored)
*
* Exec'ing show_signal_disp...
*
* === Signal dispositions in exec'd program ===
* SIGINT = SIG_DFL (default) ← custom handler GONE
* SIGQUIT = SIG_IGN (ignored) ← SIG_IGN PRESERVED
* SIGTERM = SIG_DFL (default)
* SIGUSR1 = SIG_DFL (default)
*/
Example 2: Signal Mask Survives exec()
/* example2_signal_mask.c
* gcc -o ex2 example2_signal_mask.c && ./ex2
*
* Block SIGUSR1 before exec. The exec'd program inherits the block.
* This can cause unexpected behavior in the child — demonstrates why
* SUSv3 warns: don't exec arbitrary programs with blocked signals.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
/* Helper: print current signal mask */
void print_sigmask(const char *label)
{
sigset_t mask;
sigprocmask(SIG_BLOCK, NULL, &mask); /* get current mask */
printf("%s: SIGUSR1 is %s\n", label,
sigismember(&mask, SIGUSR1) ? "BLOCKED" : "unblocked");
}
int main(void)
{
sigset_t block_set, old_set;
pid_t child;
int status;
print_sigmask("Parent (initial)");
/* Block SIGUSR1 */
sigemptyset(&block_set);
sigaddset(&block_set, SIGUSR1);
sigprocmask(SIG_BLOCK, &block_set, &old_set);
print_sigmask("Parent (after blocking SIGUSR1)");
printf("\n");
child = fork();
if (child == -1) { perror("fork"); exit(1); }
if (child == 0) {
/* Child inherits blocked SIGUSR1 */
print_sigmask("Child (inherited from parent)");
/* Best practice: unblock before execing unknown program */
sigprocmask(SIG_SETMASK, &old_set, NULL);
print_sigmask("Child (after restoring to original mask)");
/* Now exec with clean signal mask */
printf("\nExec'ing /bin/sleep 1...\n");
execlp("sleep", "sleep", "1", (char *)NULL);
perror("execlp"); exit(1);
}
waitpid(child, &status, 0);
/* Restore original mask in parent too */
sigprocmask(SIG_SETMASK, &old_set, NULL);
print_sigmask("Parent (after restore)");
return 0;
}
/* Expected output:
* Parent (initial): SIGUSR1 is unblocked
* Parent (after blocking SIGUSR1): SIGUSR1 is BLOCKED
*
* Child (inherited from parent): SIGUSR1 is BLOCKED
* Child (after restoring to original mask): SIGUSR1 is unblocked
* Exec'ing /bin/sleep 1...
* Parent (after restore): SIGUSR1 is unblocked
*/
Example 3: Correct Signal Setup Before exec() — Best Practice
/* example3_signal_best_practice.c
* gcc -o ex3 example3_signal_best_practice.c && ./ex3
*
* Shows the RIGHT way to set up signals before exec'ing another program:
* 1. Reset all custom handlers to SIG_DFL
* 2. Unblock all signals (restore default mask)
* 3. Then exec
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
void handler_sigterm(int sig) {
printf("SIGTERM received (custom handler)\n");
}
void handler_sigusr1(int sig) {
printf("SIGUSR1 received (custom handler)\n");
}
/* Reset all signals to defaults and unblock everything */
void prepare_signals_for_exec(void)
{
struct sigaction sa = {0};
sigset_t empty_mask;
sa.sa_handler = SIG_DFL;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
/* Reset signals we know we've customized */
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGUSR1, &sa, NULL);
sigaction(SIGCHLD, &sa, NULL); /* portability: reset SIGCHLD */
/* Unblock all signals */
sigemptyset(&empty_mask);
sigprocmask(SIG_SETMASK, &empty_mask, NULL);
printf("Signals reset to defaults, mask cleared\n");
}
int main(void)
{
sigset_t block_set;
pid_t child;
int status;
/* Set up some signal customizations */
signal(SIGTERM, handler_sigterm);
signal(SIGUSR1, handler_sigusr1);
/* Block some signals */
sigemptyset(&block_set);
sigaddset(&block_set, SIGTERM);
sigaddset(&block_set, SIGINT);
sigprocmask(SIG_BLOCK, &block_set, NULL);
printf("Parent: signals customized, some blocked\n");
child = fork();
if (child == -1) { perror("fork"); exit(1); }
if (child == 0) {
/* GOOD PRACTICE: clean up signal state before exec */
prepare_signals_for_exec();
/* Now exec with clean signal state */
execlp("env", "env", (char *)NULL); /* print environment */
perror("execlp"); exit(1);
}
waitpid(child, &status, 0);
printf("Child exited with %d\n", WEXITSTATUS(status));
return 0;
}
❓ Interview Questions — Signals and exec()
Q1. What happens to a custom signal handler (e.g., for SIGINT) after exec()?
Answer: It is reset to SIG_DFL (the default action). The handler was a function pointer into the old program’s code, which exec() discards. The kernel resets all signal dispositions from “custom function” to SIG_DFL. SIG_IGN dispositions are preserved.
Answer: It is reset to SIG_DFL (the default action). The handler was a function pointer into the old program’s code, which exec() discards. The kernel resets all signal dispositions from “custom function” to SIG_DFL. SIG_IGN dispositions are preserved.
Q2. Why is the signal mask preserved across exec() — and why can this be a problem?
Answer: The signal mask is stored in the kernel’s process table, not in user-space memory, so exec() doesn’t clear it. The problem: if the parent blocked signals before exec, the child program inherits that block. If the child is a third-party program that assumes no signals are blocked (a valid assumption per SUSv3), it may fail to receive critical signals.
Answer: The signal mask is stored in the kernel’s process table, not in user-space memory, so exec() doesn’t clear it. The problem: if the parent blocked signals before exec, the child program inherits that block. If the child is a third-party program that assumes no signals are blocked (a valid assumption per SUSv3), it may fail to receive critical signals.
Q3. SUSv3 says “signals should not be blocked or ignored across an exec of an arbitrary program.” When is it OK?
Answer: It’s acceptable when exec’ing a program you wrote yourself and whose behavior regarding blocked/ignored signals you know. For example, exec’ing your own daemon that expects SIGPIPE to be ignored, and you set it to SIG_IGN before exec.
Answer: It’s acceptable when exec’ing a program you wrote yourself and whose behavior regarding blocked/ignored signals you know. For example, exec’ing your own daemon that expects SIGPIPE to be ignored, and you set it to SIG_IGN before exec.
Q4. What happens to pending signals across exec()?
Answer: They are preserved. If a signal was pending (generated but blocked and not yet delivered) before exec(), it remains pending for the new program. Once the new program unblocks that signal (or if it was never blocked), the signal gets delivered.
Answer: They are preserved. If a signal was pending (generated but blocked and not yet delivered) before exec(), it remains pending for the new program. Once the new program unblocks that signal (or if it was never blocked), the signal gets delivered.
Q5. What happens to an alternate signal stack (sigaltstack) across exec()?
Answer: It is lost. The alternate signal stack was allocated in the old process’s memory, which exec() discards. Additionally, the SA_ONSTACK flag is cleared for all signals, since there’s no longer an alternate stack to use.
Answer: It is lost. The alternate signal stack was allocated in the old process’s memory, which exec() discards. Additionally, the SA_ONSTACK flag is cleared for all signals, since there’s no longer an alternate stack to use.
Q6. On Linux, if a process sets SIGCHLD to SIG_IGN and then calls exec(), what disposition does the new program see for SIGCHLD?
Answer: On Linux, SIGCHLD remains SIG_IGN after exec (it’s preserved). However, POSIX leaves this unspecified — on Solaris it gets reset to SIG_DFL. For maximum portability, always call signal(SIGCHLD, SIG_DFL) before exec’ing arbitrary programs.
Answer: On Linux, SIGCHLD remains SIG_IGN after exec (it’s preserved). However, POSIX leaves this unspecified — on Solaris it gets reset to SIG_DFL. For maximum portability, always call signal(SIGCHLD, SIG_DFL) before exec’ing arbitrary programs.
Q7. Before exec’ing a third-party program, what is the recommended cleanup for signal state?
Answer: (1) Reset all custom signal handlers to SIG_DFL using sigaction(). (2) Clear the signal mask using sigprocmask(SIG_SETMASK, &empty_mask, NULL) to unblock all signals. (3) If SIGCHLD was ignored, reset it to SIG_DFL for portability. The exec() itself will then reset the remaining custom handlers automatically.
Answer: (1) Reset all custom signal handlers to SIG_DFL using sigaction(). (2) Clear the signal mask using sigprocmask(SIG_SETMASK, &empty_mask, NULL) to unblock all signals. (3) If SIGCHLD was ignored, reset it to SIG_DFL for portability. The exec() itself will then reset the remaining custom handlers automatically.
Next: The system() Function
Run shell commands from C programs easily
