Why Do Some Signals Have Special Rules?
Most signals can be caught, blocked, or ignored. But a few signals have hardcoded behaviour in the kernel — you cannot change it, no matter what your program does. These special-case signals exist to give the system administrator and the kernel a guaranteed way to control processes even when those processes are misbehaving.
Understanding these rules is essential for writing correct daemons, process managers, and embedded system supervisors.
SIGKILL and SIGSTOP — The Uncatchable Signals
There are exactly two signals that cannot be caught, blocked, or ignored: SIGKILL and SIGSTOP. Any attempt to change their disposition using signal() or sigaction() will fail with EINVAL.
| Property | SIGKILL | SIGSTOP |
|---|---|---|
| Can be caught (signal handler)? | NO | NO |
| Can be blocked (sigprocmask)? | NO | NO |
| Can be ignored? | NO | NO |
| Effect | Terminates process immediately | Stops (suspends) process |
| How to undo? | Cannot undo — process is dead | Send SIGCONT to resume |
/* sigkill_test.c - Demonstrate that SIGKILL cannot be caught */
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
int main(void) {
struct sigaction sa;
sa.sa_handler = SIG_IGN; /* try to ignore SIGKILL */
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
/* This call will FAIL */
if (sigaction(SIGKILL, &sa, NULL) == -1) {
printf("Cannot change SIGKILL: %s\n", strerror(errno));
}
/* Also try to block SIGKILL using sigprocmask */
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGKILL);
sigaddset(&mask, SIGSTOP);
/* sigprocmask silently ignores SIGKILL and SIGSTOP in the set */
sigprocmask(SIG_BLOCK, &mask, NULL);
/* Check if they are actually blocked */
sigset_t pending;
sigprocmask(SIG_SETMASK, NULL, &pending);
printf("SIGKILL blocked: %s\n",
sigismember(&pending, SIGKILL) ? "YES (impossible!)" : "NO (as expected)");
printf("Process PID=%d -- send SIGKILL to test\n", getpid());
pause();
return 0;
}
$ gcc -g sigkill_test.c -o sigkill_test
$ ./sigkill_test
Cannot change SIGKILL: Invalid argument
SIGKILL blocked: NO (as expected)
Process PID=5678 -- send SIGKILL to test
## In another terminal:
$ kill -9 5678 # SIGKILL (signal 9) always works
SIGCONT and the Stop Signals
There are four signals that stop (suspend) a process:
- SIGSTOP — cannot be caught/blocked; sent programmatically
- SIGTSTP — terminal stop (Ctrl+Z); can be caught
- SIGTTIN — background process tried to read from terminal
- SIGTTOU — background process tried to write to terminal
SIGCONT resumes a stopped process. It has special delivery behaviour:
| Situation | What Happens |
|---|---|
| Process is stopped, receives SIGCONT | Process ALWAYS resumes, even if SIGCONT is blocked or ignored |
| Process is stopped, receives any other signal | Signal stays pending until process is resumed with SIGCONT |
| Exception: stopped process receives SIGKILL | Process is killed immediately (SIGKILL always wins) |
| SIGCONT arrives while stop signal is pending | Pending stop signals are DISCARDED |
| Stop signal arrives while SIGCONT is pending | Pending SIGCONT is DISCARDED |
/* sigcont_demo.c - Show SIGTSTP and SIGCONT interaction */
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void handler(int sig) {
if (sig == SIGTSTP) {
printf("Caught SIGTSTP (Ctrl+Z) in handler\n");
/* Do cleanup, then actually stop ourselves */
signal(SIGTSTP, SIG_DFL); /* restore default */
raise(SIGTSTP); /* now actually stop */
}
if (sig == SIGCONT) {
printf("Resumed via SIGCONT\n");
/* Re-establish SIGTSTP handler for next time */
signal(SIGTSTP, handler);
}
}
int main(void) {
signal(SIGTSTP, handler);
signal(SIGCONT, handler);
printf("PID=%d. Press Ctrl+Z to stop, then 'fg' to resume\n", getpid());
while (1) {
printf("Working... (sleeping 2s)\n");
sleep(2);
}
return 0;
}
$ gcc -g sigcont_demo.c -o sigcont_demo
$ ./sigcont_demo
PID=7890. Press Ctrl+Z to stop, then 'fg' to resume
Working... (sleeping 2s)
Working... (sleeping 2s)
^Z <-- user presses Ctrl+Z
Caught SIGTSTP (Ctrl+Z) in handler
[1]+ Stopped ./sigcont_demo
$ fg <-- shell sends SIGCONT
Resumed via SIGCONT
Working... (sleeping 2s)
Example 2: Stopping and Resuming a Child Process
A parent process can stop and resume a child process using kill() with SIGSTOP and SIGCONT.
/* stop_resume_child.c - Parent controls child with SIGSTOP/SIGCONT */
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
pid_t child_pid = fork();
if (child_pid == 0) {
/* Child: print counter every second forever */
int count = 0;
while (1) {
printf("Child counting: %d\n", count++);
sleep(1);
}
}
/* Parent: stop child after 3s, wait 3s, resume it */
sleep(3);
printf("Parent: stopping child (PID=%d)\n", child_pid);
kill(child_pid, SIGSTOP);
sleep(3);
printf("Parent: resuming child\n");
kill(child_pid, SIGCONT);
sleep(3);
printf("Parent: killing child\n");
kill(child_pid, SIGKILL);
wait(NULL);
return 0;
}
$ gcc -g stop_resume_child.c -o stop_resume_child
$ ./stop_resume_child
Child counting: 0
Child counting: 1
Child counting: 2
Parent: stopping child (PID=8001)
<--- 3 seconds of silence (child is stopped)
Parent: resuming child
Child counting: 3 <--- resumes from where it left off
Child counting: 4
Child counting: 5
Parent: killing child
Terminal-Generated Signals and the SIG_IGN Convention
When a program is launched with the disposition of certain terminal-generated signals already set to SIG_IGN, the program should not try to reset them. This is a convention, not a kernel rule, but it is important for correct behaviour.
The signals affected are: SIGHUP, SIGINT, SIGQUIT, SIGTTIN, SIGTTOU, SIGTSTP.
/* correct_sigint_handling.c - Check disposition before setting handler */
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sigint_handler(int sig) {
printf("\nCaught SIGINT — cleaning up\n");
exit(0);
}
int main(void) {
struct sigaction old_sa, new_sa;
/* First, check the CURRENT disposition of SIGINT */
if (sigaction(SIGINT, NULL, &old_sa) == -1) {
perror("sigaction");
exit(1);
}
/* Only install our handler if SIGINT is NOT already ignored.
If it's SIG_IGN, the process was started in background or
in a context where Ctrl+C should be silently ignored. */
if (old_sa.sa_handler != SIG_IGN) {
new_sa.sa_handler = sigint_handler;
sigemptyset(&new_sa.sa_mask);
new_sa.sa_flags = 0;
sigaction(SIGINT, &new_sa, NULL);
printf("SIGINT handler installed\n");
} else {
printf("SIGINT was already SIG_IGN — not changing it\n");
}
printf("Press Ctrl+C to test (PID=%d)\n", getpid());
while(1) pause();
return 0;
}
## Normal run: handler is installed
$ ./correct_sigint_handling
SIGINT handler installed
Press Ctrl+C to test (PID=9000)
^C
Caught SIGINT — cleaning up
## Run with SIGINT pre-ignored (using 'nohup' or shell background tricks)
$ (trap '' INT; ./correct_sigint_handling)
SIGINT was already SIG_IGN — not changing it
Interview Questions — Special Signal Cases
No. SIGKILL cannot be caught, blocked, or ignored. This is a deliberate kernel design decision. If processes could block SIGKILL, a runaway or malicious process could make itself impossible to terminate without rebooting the system. SIGKILL gives the system administrator and the kernel a guaranteed way to terminate any process.
The signal is not delivered immediately — it stays in the pending state. It will only be delivered after the process receives a SIGCONT signal and resumes execution. The one exception is SIGKILL, which kills the process even if it is stopped.
They cancel each other out. If SIGCONT is delivered to a process, any pending stop signals are discarded. Conversely, if a stop signal arrives, any pending SIGCONT is discarded. This prevents the “undo problem” where a stop sent before SIGCONT was sent would delay the continuation.
If a program finds that SIGHUP, SIGINT, SIGQUIT, SIGTTIN, SIGTTOU, or SIGTSTP is already set to SIG_IGN when it starts (checked via sigaction), it should not change the disposition of those signals. This happens when the process is launched in a context where the terminal signals should be ignored (e.g. run via nohup, or in a background pipeline). Overriding SIG_IGN would break the expected behavior of the launching shell.
Both stop a process, but SIGSTOP cannot be caught, blocked, or ignored — the kernel always stops the process. SIGTSTP is the “terminal stop” signal (generated by Ctrl+Z) and CAN be caught by a signal handler. This allows programs like vim or bash to clean up before stopping (e.g. restoring terminal settings). After cleanup, the handler typically restores the default disposition and resends SIGTSTP to actually stop the process.
Next Topic
Learn about the two kernel sleep states and why SIGKILL is not always instant.
