IPC: Pipes, Shared Memory, and Unix Domain Sockets¶
Processes in Unix are isolated by design. Their address spaces don’t overlap; a wild pointer in one can’t corrupt another. That’s the safety story. The performance story is that when two processes need to cooperate — an inference worker pool talking to a request router, a Python service shelling out to a C helper, nginx worker processes sharing configuration — they need a channel. That channel is IPC.
There are five IPC mechanisms you’ll actually encounter in 2026: pipes, named pipes (FIFOs), POSIX shared memory, Unix domain sockets, and message queues. This file walks each in turn and tells you when to pick which. Punchline first: for new code in 2026, Unix domain sockets are the default. Pipes are fine for parent-child. POSIX shm is for high-bandwidth data planes. System V IPC (msgget, shmget, semget) is mostly dead — you’ll see it in legacy code and should learn to recognize it, not write it.
Pipes — the parent-child channel¶
pipe(int fds[2]) creates a unidirectional channel: fds[0] is the read end, fds[1] is the write end. Because it’s just an fd pair, it inherits across fork — which is the entire point. This is how ls | wc -l works in a shell.
int fds[2];
pipe2(fds, O_CLOEXEC); // pipe2 lets you set flags atomically
pid_t pid = fork();
if (pid == 0) {
close(fds[0]); // child doesn't read
dup2(fds[1], STDOUT_FILENO);
close(fds[1]);
execlp("ls", "ls", NULL);
_exit(127);
} else {
close(fds[1]); // parent doesn't write
char buf[4096];
ssize_t n;
while ((n = read(fds[0], buf, sizeof buf)) > 0)
write(STDOUT_FILENO, buf, n);
close(fds[0]);
waitpid(pid, NULL, 0);
}
Things you’ll trip on:
Pipe capacity is 64KB on Linux by default (tunable with
fcntl(F_SETPIPE_SZ)). Writers block when full; readers block when empty.SIGPIPE: if you write to a pipe whose read end has closed, you getSIGPIPE(default action: terminate). Installsignal(SIGPIPE, SIG_IGN)at program start, or passMSG_NOSIGNALon socket sends, or usesend()with the flag. This bites every C programmer once.Half-closed pipe = EOF. Reader sees
read() == 0when all writers have closed their write end. Writer seesSIGPIPE/EPIPEwhen all readers have closed.Atomicity: writes up to
PIPE_BUF(4096 bytes on Linux) are atomic — interleaved writers won’t tear each other’s messages. Beyond that, you must frame your own messages.
Named pipes (FIFOs)¶
Same semantics as pipes but with a name in the filesystem, so unrelated processes can connect. mkfifo(path, mode) creates one; then open(path, O_RDONLY) and open(path, O_WRONLY) from two processes give you the two ends.
Uses in 2026:
Rare. Almost anything you’d use a FIFO for, a Unix domain socket does better (bidirectional, discoverable via
SOCK_STREAM/SOCK_DGRAM, supportssendmsg/recvmsgwith fd passing).Still useful for shell scripts (
mkfifothen background writer + foreground reader).
POSIX shared memory (shm_open + mmap)¶
When you need to share a large region of memory between processes and syscall/copy overhead matters, shm_open creates a named memory object in /dev/shm (a tmpfs). Then ftruncate to size, mmap in each process, and you have shared memory.
/* producer */
int fd = shm_open("/myregion", O_CREAT | O_RDWR, 0600);
ftruncate(fd, 1 << 20); // 1MB
void *p = mmap(NULL, 1 << 20, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
/* write into p; consumer maps the same name and reads */
/* consumer */
int fd = shm_open("/myregion", O_RDONLY, 0);
void *p = mmap(NULL, 1 << 20, PROT_READ, MAP_SHARED, fd, 0);
Synchronization is on you. shm_open just gives you memory; coordinating who reads and writes what, when, requires either (a) POSIX semaphores (sem_open) placed in the shared region, (b) pthread_mutex with PTHREAD_PROCESS_SHARED attribute, or (c) atomic operations if you can design a lock-free protocol.
When to use: high-bandwidth data planes. Examples in the wild: NVIDIA CUDA’s IPC handles, PyTorch DataLoader shared-memory queues, Redis’s MEMORY USAGE scanning, some inference-server request-response paths.
When not: low-bandwidth control planes or anything cross-machine. Sockets are simpler and network-portable.
System V IPC (shmget, msgget, semget)¶
The old Unix IPC family. You’ll see it in legacy code and possibly on study questions. Key characteristics:
Identified by numeric keys (
ftok) instead of paths — clumsy.ipcsandipcrmcommands to inspect and clean up.Persists across process exit unless explicitly removed (a common leak in old code).
Global namespace — keys can collide across unrelated software.
2026 status: mostly dead. POSIX shm + mmap replaces shmget. Unix domain sockets replace msgget. POSIX semaphores replace semget. Learn to recognize the pattern in old code; don’t reach for it in new code.
Unix domain sockets — the 2026 default¶
A Unix domain socket looks exactly like a network socket (socket, bind, listen, accept, connect, send, recv) but the address is a filesystem path instead of an IP+port, and the packets never leave the kernel. That gives you:
Full BSD sockets API you already need to learn anyway.
Datagram (
SOCK_DGRAM) or stream (SOCK_STREAM) semantics — pick.Fd passing: you can send an open fd from one process to another via
sendmsgwith aSCM_RIGHTScontrol message. There is no other portable way to do this. Every serious multi-process server on Linux uses this trick.Peer credentials:
SO_PEERCREDtells you the PID/UID/GID of the process on the other end. Great for authorization.Speed: roughly 2× the throughput of a localhost TCP connection because it skips the TCP stack entirely.
/* server */
int sfd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
struct sockaddr_un addr = { .sun_family = AF_UNIX };
strncpy(addr.sun_path, "/tmp/mysock", sizeof addr.sun_path - 1);
unlink(addr.sun_path);
bind(sfd, (struct sockaddr*)&addr, sizeof addr);
listen(sfd, 128);
int c = accept(sfd, NULL, NULL);
/* client */
int cfd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
connect(cfd, (struct sockaddr*)&addr, sizeof addr);
Real-world examples in 2026:
Docker daemon:
/var/run/docker.sock.systemd, D-Bus, PulseAudio/PipeWire, X11, Wayland — all Unix sockets.
PostgreSQL local connections default to a Unix socket for lower latency.
gRPC and HTTP/2 client-server on the same box: Unix socket transport gives measurably lower p99.
Choosing between them — the decision table¶
Situation |
Pick |
|---|---|
Parent → child, one-way |
Pipe |
Two related processes, bidirectional |
|
Unrelated processes on same box, request/response |
Unix domain socket |
Two processes sharing gigabytes of read-mostly data |
POSIX shm + mmap |
Passing an fd between processes |
Unix domain socket + |
Cross-machine |
TCP/UDP socket (Phase 5) |
“I need message queues!” |
POSIX message queues ( |
What most people get wrong about this¶
They reach for System V IPC because it’s what the old Stevens book teaches and half the tutorials online. In 2026, shmget/msgget/semget are historical baggage. Every new IPC design should start with a Unix domain socket and only reach for shared memory when profiling actually shows the copy cost mattering. Doing it the other way — shm first, sockets later — gives you a synchronization bug tour you didn’t sign up for.
Practice this week¶
Reimplement
ls | wc -lin C — fork twice, wire a pipe, exec both sides. Do it without looking at the example above.Build a Unix-socket “echo server” — accepts connections, echoes lines. Use
socketpairto test with a client in the same program.Two processes, one shm region: producer writes a counter every 100ms, consumer reads and prints. Use a POSIX semaphore for synchronization. Verify with
ls /dev/shm.Send an open file descriptor over a Unix socket via
SCM_RIGHTS. Have the receiverread()from it. Once you’ve done this, you truly understand fds.
References¶
man 7 pipe,man 7 fifo,man 7 unix,man 3 shm_open,man 2 socket,man 3 cmsg(for SCM_RIGHTS).APUE ch. 15-17.
TLPI ch. 43-59 (IPC section — very comprehensive).
Beej’s Guide to Unix Interprocess Communication (beej.us) — the friendly companion to the network guide.
Return to README.md · Next: 05_the_linux_specifics.md