Keywords
Why IPC Is Needed
Processes on Linux run in separate virtual address spaces. By design, one process cannot read or write another’s memory. This isolation is a safety feature — but real applications constantly need to share data, coordinate actions, and synchronise timing with other processes. The kernel provides seven IPC (Interprocess Communication) mechanisms to make this possible.
The Seven IPC Mechanisms
The simplest IPC: a unidirectional byte stream between related processes (parent/child). Created by pipe() which returns two file descriptors: pipefd[0] for reading, pipefd[1] for writing. Kernel buffer is ~64 KB. Writing blocks when full; reading blocks when empty — automatic flow control. When all write ends close, reading returns EOF. This is how shell pipelines like ls | wc -l work internally.
A pipe with a name in the filesystem — any process knowing the path can open it, not just related processes. Created with mkfifo(); appears as type p in ls -l. Opening blocks by default until both ends are open. Once open, behaves exactly like a pipe: unidirectional byte stream.
mkfifo /tmp/my_fifo # create from shell
mkfifo("/tmp/my_fifo", 0644); # create from C code
The most versatile IPC — bidirectional and can span machines. Two main types:
- UNIX Domain Sockets (AF_UNIX) — exist as a filesystem file, fast (no TCP overhead), used by D-Bus, Docker, PostgreSQL, systemd. Can transfer open file descriptors between processes.
- Internet Sockets (AF_INET/AF_INET6) — use TCP (reliable, ordered) or UDP (lightweight, unordered). Work across machines on a network.
Prevents concurrent writes from corrupting shared data.
- flock() — BSD-style whole-file locks.
LOCK_SH(shared/read, multiple holders OK),LOCK_EX(exclusive/write, one holder only),LOCK_UN(unlock). - fcntl() record locks — POSIX-style. Lock specific byte ranges within a file. Auto-released when the process dies.
Note: Linux file locks are advisory by default — they work only if all cooperating processes actually check for them.
Exchange discrete, typed messages. Unlike pipes, message boundaries are preserved — one read always returns one complete message. Messages have a type number; readers can selectively retrieve only messages of a given type.
- System V:
msgget()/msgsnd()/msgrcv()— identified by a numeric key - POSIX:
mq_open()/mq_send()/mq_receive()— identified by a name like/jobqueue
A kernel-maintained integer for synchronisation (not data transfer). Two atomic operations:
- wait / P / down — decrements. If result would be negative, the caller blocks. POSIX:
sem_wait() - signal / V / up — increments, waking any blocked waiters. POSIX:
sem_post()
A binary semaphore (initial value 1) acts as a mutex. A counting semaphore (initial value N) limits to N concurrent accesses — useful for a pool of N database connections.
The fastest IPC mechanism. Two processes map the same physical RAM pages into their virtual address spaces. A write by one process is immediately visible to the other with no system call needed for the transfer.
- POSIX:
shm_open()+ftruncate()+mmap()— preferred, integrates with file descriptors - System V:
shmget()/shmat()/shmdt()
Critical: Shared memory has no built-in synchronisation. Two processes writing simultaneously produce a data race. Always protect with semaphores or mutexes.
Signals — Software Interrupts
Signals deliver asynchronous notifications to a process. At any point the kernel can interrupt the process, run a handler function, and resume. Think of them as software interrupts.
Sources of signals:
- Hardware exceptions — divide by zero → SIGFPE; null pointer → SIGSEGV
- Terminal keys — Ctrl-C → SIGINT; Ctrl-Z → SIGTSTP
- Another process — via
kill(pid, signum)(subject to permission checks) - Kernel events — SIGPIPE (broken pipe), SIGALRM (timer), SIGCHLD (child exited)
Common Signals Reference
| Signal | Default | Description |
|---|---|---|
SIGKILL (9) |
Terminate | Unconditional kill. Cannot be caught, ignored, or blocked. Always works. |
SIGTERM (15) |
Terminate | Polite termination request. Catchable — programs should clean up then exit. |
SIGINT (2) |
Terminate | Interactive interrupt. Generated by Ctrl-C at the terminal. |
SIGSEGV (11) |
Core dump | Segmentation fault — null pointer dereference, stack overflow, bad address. |
SIGFPE (8) |
Core dump | Arithmetic exception — integer divide by zero. |
SIGCHLD (17) |
Ignore | Child process stopped or exited. Parent should call wait(). |
SIGHUP (1) |
Terminate | Terminal hangup. Daemons use it as a reload-config signal. |
SIGPIPE (13) |
Terminate | Write to pipe/socket with no readers. Typically ignored in servers. |
SIGSTOP (19) |
Stop | Unconditional stop. Cannot be caught or ignored. |
SIGUSR1/2 |
Terminate | User-defined — no kernel meaning, use for application-level events. |
Installing Signal Handlers
Use sigaction() — more portable and reliable than the older signal():
void my_handler(int sig) {
write(STDOUT_FILENO, "Caught!\n", 8); /* only safe async-signal-safe calls */
}
struct sigaction sa;
sa.sa_handler = my_handler;
sigemptyset(&sa.sa_mask); /* no extra signals blocked during handler */
sa.sa_flags = SA_RESTART; /* restart interrupted system calls */
sigaction(SIGINT, &sa, NULL);
volatile sig_atomic_t flag in the handler; do the real work in your main loop when the flag is set.POSIX Threads (pthreads)
Threads are execution units that live inside one process and share its memory, file descriptors, and signal handlers. Creating a thread is much cheaper than fork() because no address space is duplicated.
Shared
- Virtual address space (code + data + heap)
- Open file descriptors
- Signal handlers
- Environment variables
- PID and PPID
Per-Thread Private
- Stack (local variables)
- CPU registers and program counter
- Signal mask
- Thread ID (TID)
- errno (thread-local)
Creating and Joining Threads
#include <pthread.h> /* compile with: gcc ... -lpthread */
void *worker(void *arg) {
printf("Thread %ld running\n", (long)arg);
return NULL;
}
int main(void) {
pthread_t tid;
pthread_create(&tid, NULL, worker, (void *)42L); /* create thread */
pthread_join(tid, NULL); /* wait for it to finish */
return 0;
}
Mutexes — Preventing Data Races
A data race occurs when two threads access the same memory simultaneously with at least one write and no synchronisation — result is undefined. A mutex ensures only one thread runs a critical section at a time:
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;
void increment(void) {
pthread_mutex_lock(&lock); /* acquire — blocks if another thread holds it */
counter++; /* only one thread here at a time */
pthread_mutex_unlock(&lock); /* release */
}
Condition Variables — Efficient Waiting
A mutex alone cannot handle “wait until a condition is true.” Busy-waiting wastes CPU. A condition variable atomically releases a mutex and puts the thread to sleep until signalled:
pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
int ready = 0;
/* Consumer thread */
pthread_mutex_lock(&mu);
while (!ready) /* MUST be while, not if */
pthread_cond_wait(&cv, &mu); /* atomically releases mu and sleeps */
/* process data ... */
pthread_mutex_unlock(&mu);
/* Producer thread */
pthread_mutex_lock(&mu);
ready = 1;
pthread_cond_signal(&cv); /* wake one waiter */
pthread_mutex_unlock(&mu);
Interview Questions
Answer: (1) Pipes — shell pipelines, parent-child streaming. (2) FIFOs — communication between unrelated processes via a filesystem path. (3) Sockets — client-server networking (TCP) or fast local IPC (UNIX domain). (4) File locking — prevent two processes updating the same database record simultaneously. (5) Message queues — typed job dispatch with priority. (6) Semaphores — limit concurrent access to a pool of N resources. (7) Shared memory — highest-throughput IPC such as sharing a video frame buffer between processes.
Answer: These signals are the kernel’s unconditional last-resort tools. If SIGKILL could be caught, a malicious or runaway process could refuse to die, leaving administrators with no way to recover the system. If SIGSTOP could be caught, a process could prevent itself from being suspended, breaking job control entirely. The kernel delivers these signals directly without consulting the process’s signal handler table — no user-space code runs in response. This is a deliberate design decision guaranteeing administrators always retain ultimate control.
Answer: A mutex has ownership — only the thread that locked it may unlock it. It is specifically for mutual exclusion: protecting a critical section so only one thread runs it at a time. A semaphore has no ownership — any thread or process can call sem_post() regardless of who called sem_wait(). This makes semaphores suitable for signalling between threads (producer posts when data is ready; consumer waits) and for counting (allow up to N concurrent accesses). Rule: use mutex for mutual exclusion; use semaphore for signalling or resource counting.
Answer: A deadlock occurs when two or more threads each hold a lock and wait for another held by the other, forming a cycle — no thread can proceed. Prevention: (1) lock ordering — impose a global consistent acquisition order; every thread always acquires locks in that order; (2) try-lock with backoff — use pthread_mutex_trylock(), release all held locks and retry if it fails; (3) avoid holding multiple locks simultaneously by redesigning data structures; (4) document the lock hierarchy and enforce it in code review.
Answer: The kernel delivers SIGPIPE to the writing process. If no handler is installed, the default action terminates the process. The write() call returns -1 with errno set to EPIPE. Servers almost always either ignore SIGPIPE via signal(SIGPIPE, SIG_IGN) or use the MSG_NOSIGNAL flag with send(), then check return values for EPIPE. A client disconnecting should never kill the server process.
Continue to Chapter 06
Next: Process groups, sessions, /proc filesystem, realtime, and the master interview question bank.
