Signals & Inter-Process Communication
Understanding signals and IPC is essential for debugging, container orchestration, and building robust systems. This module covers the kernel implementation of these critical mechanisms.Key Topics: Signal delivery, handlers, shared memory, pipes, Unix sockets
Time to Master: 10-12 hours
Signals Overview
Signals are software interrupts for processes:Signal Types
Standard Signals (1-31)
Real-Time Signals (32-64)
Signal Kernel Implementation
Signal Data Structures
Signal Delivery Flow
Signal Frame (x86-64)
Signal Handling Best Practices
Async-Signal-Safe Functions
sigaction() vs signal()
signalfd for Event Loop Integration
Pipes
The simplest form of IPC:Pipe Internals
Named Pipes (FIFOs)
Unix Domain Sockets
For high-performance local IPC:Socket Pair for Related Processes
File Descriptor Passing
Unix sockets can pass file descriptors between processes:Shared Memory
The fastest IPC - memory is shared directly:POSIX Shared Memory
Kernel Implementation
Memory-Mapped Files
Message Queues
Structured messages with types:POSIX Message Queues
POSIX vs System V Message Queues
Semaphores
For process synchronization:POSIX Named Semaphores
Unnamed Semaphores (shared memory)
eventfd: Lightweight Notification
IPC Performance Comparison
Container IPC Considerations
IPC Namespaces
Sharing Between Containers
Interview Questions
Q: What happens when you send SIGKILL to a process?
Q: What happens when you send SIGKILL to a process?
- Cannot be caught, blocked, or ignored
- Kernel handles it directly
- Process is terminated immediately (after current syscall)
- Signal is queued to target process
TIF_SIGPENDINGflag is set- On next return to userspace (or wakeup), kernel checks flag
get_signal()sees SIGKILLdo_exit()called immediately- No handler, no cleanup - process dies
Q: How would you implement producer-consumer with IPC?
Q: How would you implement producer-consumer with IPC?
- Shared memory + semaphores (fastest):
- Unix socket pair (simpler):
- Pipe (if one direction only):
Q: Why are signal handlers tricky to write correctly?
Q: Why are signal handlers tricky to write correctly?
- Non-reentrant functions: malloc(), printf() use internal locks. If interrupted mid-call and handler calls same function → deadlock
- Non-atomic operations:
- errno clobbering: Handler might set errno, affecting interrupted code
- Use
volatile sig_atomic_tfor flags - Only call async-signal-safe functions
- Save/restore errno if needed
- Keep handlers minimal - just set flag
- Use
signalfdfor complex handling
Q: How does file descriptor passing work?
Q: How does file descriptor passing work?
- Sender uses
sendmsg()withSCM_RIGHTScontrol message - Kernel takes sender’s FD, finds underlying file object
- Kernel creates new FD in receiver’s FD table pointing to same file
- Receiver uses
recvmsg()to get new FD number
- Underlying file object is shared (same offset, flags)
- FD numbers may differ in sender/receiver
- Works across fork() and exec()
- Used by container runtimes, systemd socket activation
- Pass socket from parent to worker process
- Zero-downtime server restart (pass listening socket)
- Container file sharing
Debugging IPC
Summary
Next Steps
- Namespaces → - IPC namespace isolation
- Process Subsystem → - How signals interact with scheduling
- Networking Stack → - Sockets in depth
Interview Deep-Dive
Docker sends SIGTERM to a container's PID 1 process during 'docker stop', waits 10 seconds, then sends SIGKILL. Explain the kernel-level signal delivery for each step, and why some containers do not shut down gracefully.
Docker sends SIGTERM to a container's PID 1 process during 'docker stop', waits 10 seconds, then sends SIGKILL. Explain the kernel-level signal delivery for each step, and why some containers do not shut down gracefully.
- When Docker calls
kill(container_pid, SIGTERM), the kernel’sdo_send_sig_info()allocates asigqueuestructure, adds it to the target task’s pending signal queue, sets theTIF_SIGPENDINGflag on the task, and wakes the task if it is sleeping. The signal is delivered when the task returns to user space:exit_to_user_mode_prepare()callsdo_signal(), which dequeues the signal and either invokes the registered handler or performs the default action (terminate for SIGTERM). - The reason many containers do not shut down gracefully is that the application runs as PID 1 and does not register a SIGTERM handler. PID 1 in a PID namespace is special: the kernel does not deliver signals with default actions to PID 1 unless a handler is registered. This is because killing PID 1 would destroy the namespace, so the kernel protects it from accidental termination. If the application does not call
sigaction(SIGTERM, ...), SIGTERM is silently ignored. - After the 10-second timeout, Docker sends SIGKILL. SIGKILL cannot be caught, blocked, or ignored — not even by PID 1. The kernel’s
get_signal()function detects SIGKILL and callsdo_exit()immediately, bypassing any handler. The process is terminated, all its resources are cleaned up, and all other processes in the PID namespace receive SIGKILL as well (because PID 1 exited). - The fix is to either use an init system like
tini(Docker’s--initflag) that properly handles signals and forwards them to the application, or explicitly register a SIGTERM handler in the application code.
- If the process is in
TASK_UNINTERRUPTIBLEstate (typically waiting for disk I/O or a kernel lock), even SIGKILL cannot immediately terminate it. The kernel sets theTIF_SIGPENDINGflag and the signal remains queued, but the process does not check for signals until it transitions out of the D state. This is whykill -9sometimes appears to have no effect on processes stuck in D state. The process can only be killed when the I/O completes or the lock is released, at which point the signal delivery happens. Newer kernels introducedTASK_KILLABLE(a variant of uninterruptible sleep that responds to SIGKILL) to reduce the frequency of this problem for common code paths.
You need to implement a zero-downtime restart for a network service. Describe how you would use Unix domain sockets to pass the listening socket's file descriptor from the old process to the new one, and explain the kernel mechanisms involved.
You need to implement a zero-downtime restart for a network service. Describe how you would use Unix domain sockets to pass the listening socket's file descriptor from the old process to the new one, and explain the kernel mechanisms involved.
- The strategy is: start the new process, pass the listening socket’s fd to it via a Unix domain socket, have the new process start accepting connections, then gracefully drain and stop the old process.
- The fd-passing mechanism uses ancillary (control) messages on Unix domain sockets. The sender constructs a
msghdrwith acmsghdrcontainingSCM_RIGHTSand the fd number. Whensendmsg()processes this, the kernel does not just send the integer — it looks up the sender’sstruct file *for that fd, increments its reference count, and serializes a reference in the control message. - On the receiving side,
recvmsg()creates a new fd in the receiver’s file descriptor table pointing to the samestruct file. The receiving process now has an independent fd number (possibly different from the sender’s) that references the same underlying socket. Both processes can nowaccept()connections on the same listening socket. - The kernel mechanism in
scm_send()andscm_recv()innet/core/scm.chandles the fd translation: it callsfget()on the sender’s fd to get thestruct file *, stores it in thescm_cookie, and on the receiving side callsreceive_fd()to install the file in the receiver’s fd table with__receive_fd(). - For zero-downtime: the new process calls
accept()on the inherited socket. Both old and new processes can accept simultaneously during the transition window. The old process stops accepting, drains in-flight requests, and exits. The socket’s listen backlog is shared, so no connections are dropped during the transition.
- Already-accepted connections are individual sockets with their own file descriptors in the old process. These are not affected by passing the listening socket. The old process should stop accepting new connections (close or stop polling the listening fd), then continue processing in-flight requests on existing accepted sockets until they complete. It can set a deadline and, after the deadline, close remaining connections with a proper TCP FIN (graceful close). If the service uses HTTP, it should send
Connection: closeheaders on in-flight responses. For long-lived connections (WebSockets, gRPC streams), the application protocol needs its own graceful shutdown mechanism — the kernel-level fd passing only handles the listening socket.