Linux
Inter-Process & Inter-Thread Communication
The instant work is split across multiple processes or threads, they need a way to actually talk to each other — share data, signal that something happened, or protect a resource both are touching. Linux provides several mechanisms, each shaped for a different kind of problem: pipes, POSIX message queues, shared memory, semaphores, and mutexes.
- What it is
- The mechanisms Linux/POSIX provide for separate processes or threads to exchange data and coordinate access to shared state.
- Mental model
- A pipe is a hose; a message queue is a mailbox for discrete letters; shared memory is a whiteboard both sides can see directly; a semaphore/mutex is the talking stick that says whose turn it is.
- Key numbers
- Shared memory is the fastest option — no kernel copy for the actual data — but buys none of its own synchronization; pipes and message queues are safer by default at a real throughput cost.
- Reach for it when
- Splitting work across processes or threads that then need to exchange data or coordinate access to something they both touch.
Processes vs. threads: what's actually shared
This distinction decides which tool actually applies. A process has its own private address space by default — one process literally cannot read another's memory without an explicit IPC mechanism set up for exactly that purpose. Threads within one process already share the same address space — any global variable or heap allocation is directly visible to every thread in that process, with no setup required at all. That's why the "communication" problem is different for each: processes need an explicit channel just to exchange any data, while threads already share data automatically and instead need help coordinating safe access to it.
Pipes and named pipes (FIFOs)
An anonymous pipe (the pipe() syscall)
is a unidirectional byte stream, typically shared between a parent
process and a child it just fork()ed — exactly what a
shell's | operator sets up between two commands. A
named pipe (FIFO, created with mkfifo())
is the same idea with a filesystem path, so unrelated processes that
never shared a common ancestor can still open and use it.
int fds[2];
pipe(fds); // fds[0] = read end, fds[1] = write end
pid_t pid = fork();
if (pid == 0) {
close(fds[1]); // child only reads
char buf[64];
read(fds[0], buf, sizeof(buf));
} else {
close(fds[0]); // parent only writes
write(fds[1], "hello", 5);
}
POSIX message queues
A message queue moves discrete, labeled messages instead of a raw
byte stream — each mq_send() preserves its own boundary
and can carry a priority, and (unlike a pipe) the queue itself
persists as a kernel object even while no process currently has it
open.
mqd_t mq = mq_open("/sensor_readings", O_CREAT | O_WRONLY, 0644, NULL);
mq_send(mq, (const char *)&reading, sizeof(reading), /*priority=*/0);
mq_close(mq);
// ... in the reading process ...
mqd_t mq = mq_open("/sensor_readings", O_RDONLY);
struct sensor_reading reading;
mq_receive(mq, (char *)&reading, sizeof(reading), NULL);
Shared memory
Shared memory maps the same physical memory into two or more processes' address spaces — once set up, reading or writing it costs nothing extra beyond a normal memory access, with no data actually copied through the kernel the way a pipe or message queue requires. That speed is also exactly why it needs a separate synchronization mechanism: shared memory alone guarantees nothing about who's allowed to touch it when.
int fd = shm_open("/frame_buffer", O_CREAT | O_RDWR, 0644);
ftruncate(fd, BUFFER_SIZE);
void *addr = mmap(NULL, BUFFER_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
// `addr` now points at memory every process that mapped
// this same shared object can read and write directly.
Semaphores and mutexes
A semaphore is a counter: sem_wait()
blocks until the count is above zero then decrements it,
sem_post() increments it — usable between either
processes or threads, and general enough to signal an event, not
just protect a resource. A mutex
(pthread_mutex_t) is more specific: it protects exactly
one resource, has a notion of ownership, and is primarily a
thread tool (though a pthread_mutex_t placed in
shared memory with the PTHREAD_PROCESS_SHARED attribute
can protect a cross-process shared-memory region too).
pthread_mutex_lock(&shared_data_mutex);
shared_data.value = new_value; // safe: only one thread here at a time
pthread_mutex_unlock(&shared_data_mutex);
This is precisely the same shared-state hazard covered in Interrupts and RTOS — a mutex on Linux and a mutex inside an RTOS solve the exact same problem, just at a different layer of the stack.
Choosing between them
| Mechanism | Data model | Built-in sync? | Scope |
|---|---|---|---|
| Pipe / FIFO | Byte stream, no message boundaries | Ordering only | Processes |
| Message queue | Discrete, labeled, priority-ordered messages | Message-level ordering | Processes |
| Shared memory | Raw shared bytes | None — you add it | Processes |
| Semaphore | A counter | Is itself the primitive | Processes or threads |
| Mutex | A lock with ownership | Is itself the primitive | Threads (usually) |
Real-world applications
- Sensor logging pipelines — a sensor-reading process pushing samples to a separate logging process over a message queue, decoupling the two so one can be restarted without losing the other's state.
- High-throughput video/audio — capture and processing running as separate processes exchange large frames over shared memory specifically to avoid copying them through the kernel on every frame.
- Multi-threaded embedded applications — a shared sensor-data struct protected by a mutex is the userspace-Linux version of the exact same race condition ISR/main-loop shared state creates on bare metal.
- Init systems — systemd and similar use pipes to capture logging output from the child services they supervise.
Gotchas
- Shared memory without synchronization is a race condition waiting to happen — "shared" says nothing at all about who's allowed to touch it when.
- Forgetting to unlink a named pipe, message queue, or shared-memory segment leaks it as a persistent kernel object (visible under
/dev/mqueueor/dev/shm) until something explicitly removes it. - A standard (non-recursive) mutex must be unlocked by the same thread that locked it — unlocking from a different thread is undefined behavior, a genuinely subtle bug class to track down.
- Pipes have a limited kernel buffer size — a writer can block indefinitely if the reader isn't keeping up, which silently turns "asynchronous" code into a deadlock if both ends end up waiting on each other.