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
| 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 |
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)
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
| 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
| 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
| 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 |
/* 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
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.
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.
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.
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.
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.
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?
