DEV Community

Cover image for Why kill -9 Cannot Be Trapped: The Kernel's Unstoppable SIGKILL Path
Syed Anzar
Syed Anzar

Posted on

Why kill -9 Cannot Be Trapped: The Kernel's Unstoppable SIGKILL Path

You write a graceful shutdown handler in Node.js, Python, or Go. You catch SIGTERM and SIGINT, close your database connections, flush open files, and exit cleanly.

Then someone runs kill -9 <pid> (or Kubernetes kills your container after a timeout).

Your cleanup handlers never run. Your open files aren't flushed. Your database connection drops abruptly.

Why? Why can't a process catch, block, or ignore SIGKILL? What actually happens inside the Linux kernel when signal 9 is delivered?


1. How Regular Signals Work vs SIGKILL

To understand why SIGKILL is unstoppable, you first have to understand how standard signals (like SIGTERM or SIGINT) work.

Every Linux process has a struct task_struct in kernel space. Inside that struct lives a signal handler table (struct sighand_struct):

[User Space Process]
       ▲
       │ 3. Kernel switches execution to your custom handler
[Linux Kernel]
       ▲
       │ 2. Kernel checks sighand_struct -> points to your function
   sys_kill(pid, SIGTERM) 
       │ 1. Signal posted to pending queue
Enter fullscreen mode Exit fullscreen mode

When you call signal(SIGTERM, handler) or signal.signal(signal.SIGTERM, my_func):

  1. You register a memory address pointing to your custom function inside your process's virtual memory.
  2. When SIGTERM arrives, the kernel queues it in the process's pending signal mask.
  3. On the next context switch back to user space, the kernel sets up a stack frame and redirects the CPU to execute your handler.

Your application code is in charge of responding.


2. The SIGKILL Exception in Kernel Space

SIGKILL (signal 9) and SIGSTOP (signal 19) operate on a completely different path.

When kill(pid, 9) is invoked, it enters the kernel via sys_kill() and reaches complete_signal() in kernel/signal.c.

The kernel checks the signal number:

/* Simplified Linux Kernel Logic in kernel/signal.c */
if (sig == SIGKILL) {
    /* 1. Bypass sighand_struct entirely */
    /* 2. Mark the thread group with SIGNAL_GROUP_EXIT */
    /* 3. Immediately set task state to TASK_DEAD / exit */
    do_group_exit(sig);
}
Enter fullscreen mode Exit fullscreen mode

The key architectural differences:

Feature Standard Signal (SIGTERM / 15) Force Kill (SIGKILL / 9)
Handling Process user-space handler runs Bypasses user space completely
Blockable? Yes (via sigprocmask) No (Kernel ignores mask)
Catchable? Yes (signal() / sigaction()) No (Kernel returns EINVAL)
Cleanup Flushes buffers, closes DB pools Instant kernel task destruction
Delivery Delivered on next return to user-space Executed immediately by scheduler

3. What Happens to Resources on SIGKILL?

Because user-space code never runs again, developers often worry about resource leaks. Here is what the Linux kernel actually guarantees:

  1. Virtual Memory Reclaimed: All page tables and virtual memory regions allocated to the process are freed immediately.
  2. File Descriptors Closed: The kernel calls close() on all open file descriptors. Sockets send a FIN or RST packet to remote endpoints.
  3. Locks Released: POSIX file locks held by the process are automatically released.

What is NOT cleaned up:

  • Unwritten User Buffers: Data sitting in application-level buffers (e.g. standard I/O streams not yet flushed to disk) is lost.
  • Temporary Files: Files created on /tmp without an auto-delete wrapper stay on disk.
  • Database Transactions: In-flight database transactions are aborted by the database server when the socket closes, triggering rollback.

4. Practical Takeaways for Docker and Kubernetes

Understanding SIGKILL explains how container termination lifecycles work:

  1. The Grace Period: When Kubernetes stops a pod, it sends SIGTERM first.
  2. The Countdown: The application has terminationGracePeriodSeconds (default: 30s) to catch SIGTERM, finish in-flight requests, and exit.
  3. The Guillotine: If the process is still alive after the grace period, the container runtime sends SIGKILL.
docker stop -> SIGTERM (15) -> [ 10s grace period ] -> SIGKILL (9)
Enter fullscreen mode Exit fullscreen mode

Golden Rule:

  • Always listen for SIGTERM to perform graceful cleanup.
  • Never design a system that relies on catching SIGKILL — by POSIX design and kernel mechanics, SIGKILL belongs to the operating system, not your application.

Top comments (1)

Collapse
 
mrsaynothing profile image
Mr Say Nothing •

The diagram of the handler-table path is the part most SIGKILL explainers skip — SIGTERM is a request delivered through your process, SIGKILL is the kernel deleting the task from outside it. One footnote worth adding from the ops side: the closest thing to catching it is the parent watching with waitpid/WIFSIGNALED, which is why supervising processes (systemd, container runtimes) can still clean up after a SIGKILL even though the victim cannot. And the zombie edge case people hit in practice is usually the reverse problem — SIGKILL to a child whose parent never reaps.