Table of Contents
Signals
Signals are a simple, asynchronous way for the kernel and processes to notify a process that “something happened.” They are not data channels; they are more like software interrupts.
Common Signals and Their Meanings
Linux defines many signals; a few are especially important:
SIGINT(2): Sent when you pressCtrl+Cin a terminal. Default action: terminate the process.SIGTERM(15): Generic request to terminate. Default action: terminate. Meant to be polite: “please exit.”SIGKILL(9): Non‑catchable, non‑ignorable kill. Default action: immediate termination; cannot be handled by the process.SIGSTOP(19 on many archs): Non‑catchable stop (suspend) the process.SIGTSTP(20): Sent byCtrl+Zin a terminal. Default: stop (suspend) the process, can be handled.SIGCONT(18): Resume a stopped process.SIGHUP(1): Historically “hangup” when a terminal disconnected; often used by daemons to reload configuration.SIGCHLD(17): Sent to parent when a child changes state (exits, stops, continues).SIGALRM(14): Generated when a process’s timer (viaalarm()orsetitimer()) expires.SIGSEGV(11): Segmentation fault (invalid memory access).SIGBUS,SIGFPE,SIGILL: Hardware/CPU/illegal instruction related.
The exact numeric values differ across architectures; use the symbolic names.
Signal Dispositions
Each signal has a disposition for a process:
- Default action (terminate, core dump, ignore, stop, continue)
- Ignore (process chooses to ignore, if allowed)
- Catch via a signal handler function
Two special constants for setting dispositions:
SIG_DFL— default actionSIG_IGN— ignore (if allowed)
Some signals (SIGKILL, SIGSTOP) cannot be caught, blocked, or ignored. The kernel enforces this.
Sending Signals
From the Shell
kill PIDsendsSIGTERMby default.kill -SIGKILL PIDorkill -9 PIDsendsSIGKILL.kill -SIGUSR1 PIDsends a user‑defined signal 1.
You can also send signals by job control:
Ctrl+C→SIGINTto foreground jobCtrl+Z→SIGTSTPto foreground jobfg,bguseSIGCONTunder the hood.
From Code: `kill(2)` and `raise(3)`
To send a signal from one process to another:
#include <signal.h>
#include <unistd.h>
int kill(pid_t pid, int sig);pid > 0: send to that specific process.pid == 0: send to all processes in the caller’s process group.pid == -1: send to all processes the caller can signal.pid < -1: send to all processes in process group-pid.
To send a signal to yourself:
#include <signal.h>
int raise(int sig); // equivalent to kill(getpid(), sig)Real‑time Signals
Traditional (standard) signals are non‑queued: multiple occurrences can coalesce into one. POSIX real‑time signals (numbered SIGRTMIN to SIGRTMAX) are queued and can carry more detailed info (see sigqueue()).
Use them carefully; they are more complex and less portable between systems.
Handling Signals in User Space
Installing a Simple Handler: `signal()` (Legacy)
The historical API:
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);Usage:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void sigint_handler(int sig) {
write(STDOUT_FILENO, "Caught SIGINT\n", 14);
}
int main(void) {
signal(SIGINT, sigint_handler);
while (1) {
pause(); // sleep until a signal is received
}
}
signal() has historical quirks and portability issues; in serious code, use sigaction().
Robust Handling: `sigaction()`
sigaction() provides fine‑grained control:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
Key parts of struct sigaction:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
};sa_handler: simple handler taking justint signum.sa_sigaction: extended handler with extra info whenSA_SIGINFOis set insa_flags.sa_mask: additional signals to block while the handler runs (prevents reentrancy on those signals).sa_flags: options such as:SA_RESTART: restart some interrupted system calls.SA_SIGINFO: usesa_sigactioninstead ofsa_handler.SA_NOCLDWAIT,SA_NOCLDSTOPforSIGCHLDbehavior, etc.
Example:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
static void handler(int sig, siginfo_t *info, void *ucontext) {
(void)ucontext;
printf("Received signal %d from PID %d\n", sig, info->si_pid);
}
int main(void) {
struct sigaction sa = {0};
sa.sa_sigaction = handler;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sigaction(SIGUSR1, &sa, NULL);
printf("My PID: %d\n", getpid());
while (1) {
pause();
}
}Async‑Signal‑Safe Functions
Signal handlers run asynchronously relative to the main flow. Many library functions are not safe to call from within a handler (they may use non‑reentrant state, global variables, etc.).
POSIX defines async‑signal‑safe functions, which can be safely called from handlers. Examples:
_Exit,_exitwrite,sig_atomic_toperationssignal,sigaction,sigprocmask(with care)- A small subset of others (check
signal-safety(7)).
Avoid calling:
malloc,free,printf,sprintf,std::cout,pthread_*etc.
Use global volatile sig_atomic_t flags to communicate with the main code:
#include <signal.h>
#include <stdatomic.h>
volatile sig_atomic_t got_sigint = 0;
void handler(int sig) {
got_sigint = 1;
}
int main(void) {
signal(SIGINT, handler);
for (;;) {
if (got_sigint) {
// handle it in main flow, using safe APIs
got_sigint = 0;
}
// do regular work
}
}Signal Masking and Pending Signals
Each thread has a signal mask: a set of signals that are blocked and will not be delivered until unblocked.
Manipulating the Mask: `sigprocmask()`
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);how:SIG_BLOCK: add signals insetto the mask.SIG_UNBLOCK: remove them.SIG_SETMASK: replace mask.
Helpers:
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
Example: momentarily block SIGINT:
sigset_t block, old;
sigemptyset(&block);
sigaddset(&block, SIGINT);
sigprocmask(SIG_BLOCK, &block, &old);
/* critical section: SIGINT won't be delivered here */
sigprocmask(SIG_SETMASK, &old, NULL);Pending Signals and `sigpending()`
Signals sent to a process while blocked become pending until they can be delivered.
Check pending signals:
sigset_t pending;
sigpending(&pending);
if (sigismember(&pending, SIGINT)) {
// signal is pending
}Standard signals coalesce (only one instance pending per type); real‑time signals queue.
Synchronous Waiting: `sigsuspend()` / `sigwaitinfo()`
To atomically unblock some signals and pause:
#include <signal.h>
int sigsuspend(const sigset_t *mask);
sigwaitinfo() and sigtimedwait() can synchronously wait for specific signals and return detailed info (similar to messages).
Inter‑Process Communication (IPC)
Signals provide only event notifications. For data exchange and richer coordination, Linux offers multiple IPC mechanisms. At a high level they differ in:
- Communication pattern (one‑to‑one vs many‑to‑many)
- Directionality (half‑duplex vs full‑duplex)
- Scope (local machine vs across network)
- Lifetime (tied to processes vs independent objects)
- Kernel API vs filesystem interface
Below is an overview tailored to understanding how they relate to signals and to each other.
Pipes and FIFOs
Anonymous Pipes
Anonymous pipes are the simplest IPC. They connect a writer and a reader through a unidirectional byte stream.
- Created with
pipe(int fds[2]). - Requires a parent‑child or related processes (descriptors are inherited or passed).
- Used heavily by shells for command pipelines (
ls | grep foo).
Characteristics:
- Byte‑oriented, no message boundaries (can be layered with protocols).
- Local to a machine.
- Lifetime tied to file descriptors; when both ends close, data is discarded.
Anonymous pipes are not named; they do not appear as filesystem nodes.
Named Pipes (FIFOs)
FIFOs extend pipes across unrelated processes via a name in the filesystem:
- Created with
mkfifo(const char *pathname, mode_t mode). - Opened with
open()like regular files.
Characteristics:
- Same semantics as pipes (unidirectional, byte stream).
- Any process can open and communicate via the FIFO path.
- Lifetime persists as long as the filesystem entry exists.
Use FIFOs for simple local IPC with minimal setup, especially for glue scripts or legacy systems.
UNIX Domain Sockets
UNIX domain sockets are IPC endpoints that behave like network sockets but are confined to one host and addressable via filesystem paths or abstract names.
Advantages:
- Support
SOCK_STREAM(reliable byte stream) andSOCK_DGRAM(datagram) modes. - Full‑duplex communication.
- Can pass file descriptors and credentials (
SCM_RIGHTS,SCM_CREDENTIALS). - Zero network stack overhead (usually faster than TCP for local IPC).
Typical uses:
- Communication between daemons and client tools (e.g.,
systemd’s control socket, X11 sockets). - Local RPC, control APIs (e.g., Docker’s Unix socket
/var/run/docker.sock).
Example topology:
- Server:
socket(AF_UNIX, SOCK_STREAM, 0)→bind()to path →listen()→accept(). - Client:
socket()→connect()to same path.
UNIX sockets are flexible and a good general “default choice” for structured local IPC.
Message Queues
Linux supports two major message queue styles:
- System V message queues (
msgget,msgsnd,msgrcv). - POSIX message queues (
mq_open,mq_send,mq_receive).
System V Message Queues
- Identified by integer keys (
key_t) and IDs. - Support messages with a type (long integer) and arbitrary data.
- Provide priority via types and ordering rules.
- Global namespace, kernel‑managed; can accumulate garbage if not cleaned up.
They’re powerful but considered somewhat old‑fashioned and cumbersome.
POSIX Message Queues
- Named with strings like
/myqueue(similar to files). - Support priorities directly per message.
- Controlled via file descriptor‑like handles.
- Can be integrated with
select,poll,epoll(implementation‑dependent).
Characteristics:
- Preserve message boundaries.
- Kernel enforces maximum message size and queue length.
- Suitable for multiple producers/consumers with prioritization.
Signal integration: POSIX message queues can be configured to send a signal (e.g., SIGEV_SIGNAL) when new messages arrive, connecting queues with signal‑driven I/O.
Shared Memory
Shared memory is the fastest IPC mechanism for large data volumes. Multiple processes map the same region of physical memory into their address spaces.
Variants:
- System V shared memory (
shmget,shmat,shmdt). - POSIX shared memory (
shm_open,mmap).
Characteristics:
- Zero‑copy between processes once mapped.
- Requires explicit synchronization; you must layer locks or atomic operations.
- Errors (like out‑of‑bounds writes) can corrupt all participants.
Common synchronization tools:
- Semaphores (System V or POSIX).
- Mutexes and condition variables in shared mappings (
pthread_mutexattr_setpshared()). - Atomics (
stdatomic.h/C11 atomics).
Typical pattern:
- One process creates the shared segment.
- Others open/map it.
- All coordinate access via locks or lock‑free algorithms.
Shared memory is ideal when throughput matters, such as video buffers, large telemetry feeds, or high‑throughput IPC.
Semaphores and Synchronization Primitives
Semaphores are counters used to control access to shared resources or to signal events between processes/threads.
Variants:
- System V semaphores (
semget,semop). - POSIX named semaphores (
sem_open). - POSIX unnamed semaphores (
sem_init) — for related processes using shared memory or threads within a process.
Typical use patterns:
- Binary semaphore: acts like a mutex (0 or 1).
- Counting semaphore: controls a pool of resources (value equals available count).
In multi‑process scenarios, semaphores and shared memory are often combined:
- Shared memory holds the data.
- Semaphores gate access or signal new data availability.
Other synchronization options:
- Futexes (fast userspace mutexes; raw kernel API, commonly hidden behind
pthread). - File locks (
flock,fcntllocks) for coordinating via the filesystem.
Signals vs Other IPC Mechanisms
Signals are distinct from other IPC in purpose and semantics:
- Signals:
- Asynchronous notifications, tiny payload (if any).
- Limited to signal numbers and maybe a few integers (
siginfo_t). - Unreliable ordering for standard signals; coalescing can lose events.
- Great for: termination, simple “wake up” events, “reconfigure now,” “child exited.”
- Other IPC (pipes, sockets, queues, shared memory):
- Designed to carry data.
- Provide ordering guarantees and explicit buffers.
- Often support multiplexing and structured protocols.
A common pattern:
- Use a signal to wake a process that waits on a blocking system call, then:
- Use a richer IPC channel (socket, pipe, shared memory) to exchange the actual data.
For example, a daemon might:
- Wait for clients on a UNIX domain socket.
- Get
SIGHUPwhen it should reload config. - Use shared memory + semaphores to efficiently share large data with worker processes.
IPC Design Considerations
When choosing an IPC method, consider:
- Complexity vs requirements:
- Simple one‑directional text or logs → pipe/FIFO.
- Request/response, multiple clients, richer protocol → UNIX domain sockets.
- High throughput, large binary data → shared memory + synchronization.
- Broadcast events, priority queues → POSIX message queues, perhaps with signal notifications.
- Lifespan and management:
- Named objects (FIFOs, message queues, shared memory, sockets) need cleanup and naming conventions.
- Security and isolation:
- Filesystem‑named IPC respects Unix permissions.
- Namespaces (PID, mount, network, IPC) can partition IPC for containers and sandboxes.
- Debugging:
- Pipes and sockets can be inspected with tools like
strace,ss,lsof. - Shared memory and System V IPC can be examined with
ipcs,ipcrm.
Selecting the right combination is part of advanced Linux system design; signals are one piece of this broader IPC toolkit, mainly for control flow and notifications rather than bulk data transfer.