22.3 – 22.5 Sleep States, Hardware Signals & Sync/Async

 

22.3 – 22.5 Sleep States, Hardware Signals & Sync/Async
Why SIGKILL isn’t instant, why you can’t ignore SIGSEGV, and what “synchronous signal” really means

22.3 — Interruptible and Uninterruptible Sleep States

We said “SIGKILL always terminates a process immediately.” That is almost true — but there is a proviso. When the kernel puts a process to sleep waiting for something (like disk I/O), it can be in two different sleep states. Only one of those states can be interrupted by a signal.

The Three Kernel Sleep States

Process Sleep States in Linux Kernel
State ps STAT Waiting For Signal Delivery
TASK_INTERRUPTIBLE S Terminal input, pipe data, semaphore, timer Signal wakes process immediately — system call returns early
TASK_UNINTERRUPTIBLE D Disk I/O completion, NFS operation, certain kernel locks Signal is PENDING — not delivered until process leaves this state
TASK_KILLABLE (since 2.6.25) D Same as UNINTERRUPTIBLE but smarter Wakes up if a FATAL signal (like SIGKILL) arrives — other signals still pending
The D state problem: A process stuck in TASK_UNINTERRUPTIBLE state (shown as “D” by ps) cannot be killed with kill -9. This typically happens during NFS hangs, slow disk I/O, or kernel bugs. The only fix is usually to address the underlying I/O problem or reboot. TASK_KILLABLE was introduced specifically to help with this — NFS was one of the first subsystems converted to use it.
/* Demonstrate EINTR from an interruptible sleep */
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

void handler(int sig) {
    printf("Handler: caught signal %d\n", sig);
}

int main(void) {
    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;  /* No SA_RESTART -- system call will NOT restart */
    sigaction(SIGINT, &sa, NULL);

    printf("Sleeping for 60 seconds (press Ctrl+C to interrupt)\n");
    int ret = sleep(60);  /* TASK_INTERRUPTIBLE sleep */

    if (ret > 0) {
        printf("sleep() returned early! %d seconds remaining\n", ret);
    }

    printf("read() from stdin...\n");
    char buf[64];
    ssize_t n = read(STDIN_FILENO, buf, sizeof(buf));
    if (n == -1) {
        if (errno == EINTR)
            printf("read() was interrupted by signal (EINTR)\n");
        else
            printf("read() error: %s\n", strerror(errno));
    }

    return 0;
}
$ ./sleep_demo
Sleeping for 60 seconds (press Ctrl+C to interrupt)
^C
Handler: caught signal 2
sleep() returned early! 55 seconds remaining
read() from stdin...
^C
Handler: caught signal 2
read() was interrupted by signal (EINTR)
EINTR: When a signal interrupts a blocking system call like read(), write(), wait(), select(), the call returns -1 with errno = EINTR. Your code must handle this — either restart the call in a loop or treat it as a clean exit condition.
/* Safe read wrapper that handles EINTR automatically */
ssize_t safe_read(int fd, void *buf, size_t count) {
    ssize_t n;
    while ((n = read(fd, buf, count)) == -1 && errno == EINTR)
        continue;  /* restart if interrupted by signal */
    return n;     /* real error or success */
}

22.4 — Hardware-Generated Signals

Some signals are generated not by another process or user input, but directly by the CPU hardware when your program executes a bad instruction. These signals have special rules because they are tied to the exact instruction that caused the problem.

The Four Hardware Exception Signals

Hardware-Generated Signals
Signal Trigger Example
SIGSEGV Segmentation violation — invalid memory access Null pointer dereference, write to read-only memory, stack overflow
SIGBUS Bus error — misaligned memory access Accessing a 4-byte int at an odd address (on some architectures)
SIGFPE Floating point exception Integer divide by zero, overflow in integer arithmetic
SIGILL Illegal instruction Executing corrupted code, executing data as code

Why You Cannot Safely Ignore or Block These Signals

Three Wrong Approaches and Why They Fail
Approach What Happens Verdict
Return normally from signal handler Program resumes at the SAME faulting instruction → signal generated again → infinite loop of handler invocations until stack overflow WRONG
Ignore the signal (SIG_IGN) Linux forces delivery anyway (for hardware-generated signals). After Linux 2.6: process is killed immediately if signal is blocked. WRONG
Block the signal Linux 2.4: ignores the block, delivers signal anyway. Linux 2.6+: process is killed immediately with that signal, even if a handler was installed. WRONG
Accept default action (termination) Process terminates cleanly, possibly with core dump CORRECT
Install handler that calls siglongjmp() Jumps out of the faulting context to a safe recovery point. No normal return from handler. CORRECT (advanced)
/* sigsegv_handler_wrong.c - Demonstrates the infinite loop problem */
#include <stdio.h>
#include <signal.h>

int counter = 0;

void bad_handler(int sig) {
    counter++;
    printf("Handler called (count=%d) — returning normally\n", counter);
    if (counter > 5) {
        /* Force exit to avoid running forever in this demo */
        printf("Stopping demo at count 5\n");
        _exit(1);
    }
    /* Normal return here re-executes the faulting instruction */
}

int main(void) {
    signal(SIGSEGV, bad_handler);

    printf("About to dereference null pointer...\n");
    int *p = NULL;
    *p = 42;   /* SIGSEGV -- handler called, returns, re-executes, loops! */
    return 0;
}
/* sigsegv_recovery.c - Correct: use siglongjmp to escape the fault */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <setjmp.h>

static sigjmp_buf recovery_point;

void safe_handler(int sig) {
    printf("Caught signal %d — jumping to recovery point\n", sig);
    siglongjmp(recovery_point, 1);  /* jump OUT of handler safely */
}

int main(void) {
    struct sigaction sa;
    sa.sa_handler = safe_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGFPE,  &sa, NULL);

    /* Set up the recovery point */
    if (sigsetjmp(recovery_point, 1) == 0) {
        /* Normal path: try risky operation */
        printf("Attempting risky operation...\n");
        int *p = NULL;
        *p = 42;  /* causes SIGSEGV */
        printf("This line is never reached\n");
    } else {
        /* Recovery path: siglongjmp brought us here */
        printf("Recovered from hardware fault. Continuing safely.\n");
    }

    printf("Program continues after recovery\n");
    return 0;
}
$ gcc -g sigsegv_recovery.c -o sigsegv_recovery
$ ./sigsegv_recovery
Attempting risky operation...
Caught signal 11 — jumping to recovery point
Recovered from hardware fault. Continuing safely.
Program continues after recovery
/* sigfpe_demo.c - Integer divide by zero */
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
#include <stdlib.h>

static sigjmp_buf jmp_env;

void fpe_handler(int sig) {
    printf("SIGFPE: arithmetic exception (divide by zero?)\n");
    siglongjmp(jmp_env, 1);
}

int main(void) {
    signal(SIGFPE, fpe_handler);

    int a = 100, b = 0;

    if (sigsetjmp(jmp_env, 1) == 0) {
        printf("Dividing %d / %d\n", a, b);
        int result = a / b;  /* triggers SIGFPE */
        printf("Result: %d\n", result);
    } else {
        printf("Division failed, using default value 0\n");
    }

    printf("Done.\n");
    return 0;
}

22.5 — Synchronous and Asynchronous Signal Generation

It seems like signals always arrive at unpredictable times. But that is not always true. The key is whether the signal is generated by the process itself (synchronous) or by an external event (asynchronous).

Synchronous vs Asynchronous

Synchronous vs Asynchronous Signal Generation
Type Generated By Delivery Predictable?
Asynchronous External: another process, kernel event, user pressing Ctrl+C Next kernel→user mode switch (slight delay possible) NO — process cannot predict when
Synchronous Process itself: executing a bad instruction (hardware exception), or calling raise()/kill(self) Immediately — before the triggering instruction/call returns YES — delivery is deterministic
Important: Synchronous/asynchronous is a property of how the signal was generated, not of the signal itself. Any signal (even SIGINT) can be generated synchronously if the process sends it to itself. Any signal can be asynchronous if sent from another process.
/* sync_vs_async.c - Demonstrate synchronous signal delivery */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handler(int sig) {
    printf("Handler: got signal %d\n", sig);
}

int main(void) {
    signal(SIGUSR1, handler);

    printf("Before raise(SIGUSR1)\n");
    raise(SIGUSR1);   /* SYNCHRONOUS: handler runs NOW, before raise() returns */
    printf("After raise(SIGUSR1) -- handler already ran\n");

    /* Compare: sending to another process (asynchronous) */
    /* The receiving process handles it "sometime later" */
    printf("\nSending SIGUSR1 to shell (PID=%d)\n", getppid());
    kill(getppid(), SIGUSR1);  /* shell handles it asynchronously */
    printf("kill() returned -- shell may not have handled it yet\n");

    return 0;
}
$ ./sync_vs_async
Before raise(SIGUSR1)
Handler: got signal 10
After raise(SIGUSR1) -- handler already ran
Sending SIGUSR1 to shell (PID=1234)
kill() returned -- shell may not have handled it yet
/* kill_self.c - Send signals to own process group */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

void handler(int sig) {
    printf("  [Handler invoked for signal %d]\n", sig);
}

int main(void) {
    signal(SIGUSR1, handler);
    signal(SIGUSR2, handler);

    /* Three ways to send a signal to yourself synchronously */

    /* Method 1: raise() */
    printf("1. Using raise(SIGUSR1)\n");
    raise(SIGUSR1);
    printf("   Back in main after raise\n");

    /* Method 2: kill() with own PID */
    printf("2. Using kill(getpid(), SIGUSR2)\n");
    kill(getpid(), SIGUSR2);
    printf("   Back in main after kill\n");

    /* Method 3: killpg() to send to own process group */
    printf("3. Using killpg(0, SIGUSR1) -- sends to own process group\n");
    killpg(0, SIGUSR1);
    printf("   Back in main after killpg\n");

    return 0;
}

Interview Questions — Sleep States, Hardware Signals, Sync/Async

Q1. A process shows state “D” in ps output and cannot be killed with kill -9. Why?
Answer:

The process is in TASK_UNINTERRUPTIBLE sleep, waiting for a specific kernel event (typically disk I/O or NFS). In this state, signals — including SIGKILL — are not delivered until the process emerges from that state. This is usually caused by a hung NFS mount, a stuck disk, or a kernel bug. Since kernel 2.6.25, some kernel code uses TASK_KILLABLE instead, which does respond to fatal signals, but legacy drivers may still use TASK_UNINTERRUPTIBLE.

Q2. Why does a signal handler for SIGSEGV that returns normally cause an infinite loop?
Answer:

When a hardware exception (like a null pointer dereference) triggers SIGSEGV, the signal is generated by the specific machine-language instruction that caused the fault. After the handler returns normally, the CPU attempts to resume execution at the same instruction — which immediately causes another SIGSEGV, calling the handler again. This loop continues until the stack overflows. The correct fix is to either accept the default (termination) or use siglongjmp() to jump to a safe recovery point.

Q3. What is the difference between synchronous and asynchronous signal generation?
Answer:

Asynchronous: signal is generated by an external event (another process calls kill(), user presses Ctrl+C, timer fires). The receiving process cannot predict when it will arrive. Synchronous: signal is generated by the process itself — either by executing a bad instruction (hardware exception) or by calling raise()/kill(getpid(), sig). Delivery is immediate and predictable. Note: synchronous/asynchronous describes how the signal was generated, not the signal number itself.

Q4. What is EINTR and how should you handle it?
Answer:

When a blocking system call (read, write, wait, select, etc.) is interrupted by a signal handler, it returns -1 with errno set to EINTR. This is not a real error — it just means the call was cut short. You should either restart the call in a loop (while ((n = read(...)) == -1 && errno == EINTR) continue;) or use the SA_RESTART flag in sigaction() to make the kernel restart eligible system calls automatically.

Q5. Can you block SIGFPE? What happens in Linux 2.6+ if you try?
Answer:

You can add SIGFPE to the signal mask, but the kernel ignores the block for hardware-generated instances. In Linux 2.4 and earlier, the signal was delivered anyway and the handler or default ran. In Linux 2.6+, if the signal is blocked, the process is immediately killed by that signal even if a handler was installed. This stricter behavior was introduced because the old behavior hid bugs and could cause deadlocks in multi-threaded programs.

Q6. What is TASK_KILLABLE and why was it introduced?
Answer:

TASK_KILLABLE was introduced in Linux 2.6.25 to solve the “stuck in D state” problem. It behaves like TASK_UNINTERRUPTIBLE (normal signals don’t wake it) but it DOES wake up if a fatal signal (one that would kill the process) is received. This means SIGKILL can terminate a TASK_KILLABLE process even while it is waiting for I/O. The NFS subsystem was one of the first to be converted to use TASK_KILLABLE, preventing hung NFS mounts from requiring a system reboot.

Next Topic

When exactly are pending signals delivered, and in what order?

22.6 — Timing & Order → ← Back to Index

Leave a Reply

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