22.2 — Special Cases for Signal Delivery

 

22.2 — Special Cases for Signal Delivery
SIGKILL, SIGSTOP, SIGCONT — signals with unique kernel-level rules

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.

SIGKILL and SIGSTOP — Rules Summary
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
Design reason: If processes could block SIGKILL, a buggy or malicious process could make itself impossible to kill — requiring a full system reboot. The kernel must always have a last resort.
/* 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:

SIGCONT Special Delivery Rules
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
Why discard the opposite signal? Imagine: you stop a process, then immediately continue it. If both signals were delivered, the net effect should be “nothing changed.” Linux prevents the signals from undoing each other by discarding the earlier one.
/* 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

Q1. Can you catch or block SIGKILL? Why or why not?
Answer:

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.

Q2. What happens when a stopped process receives a signal other than SIGCONT or SIGKILL?
Answer:

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.

Q3. What happens when SIGCONT arrives while a stop signal (SIGSTOP) is already pending, and vice versa?
Answer:

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.

Q4. What is the convention for terminal-generated signals and SIG_IGN?
Answer:

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.

Q5. What is the difference between SIGSTOP and SIGTSTP?
Answer:

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.

22.3 — Sleep States → ← Back to Index

Leave a Reply

Your email address will not be published. Required fields are marked *