Vasu Patel logoVasu Patel
← Under the Hood: Operating Systems
All chapters in this track
  1. 01Chapter 1: Why OS Fundamentals Matter to Application Engineers
  2. 02Chapter 2: Processes, Threads, and the Process Lifecycle
  3. 03Chapter 3: The Memory Hierarchy: Registers to Disk
  4. 04Chapter 4: CPU Scheduling Algorithms
  5. 05Chapter 5: Memory Management I: Paging and Segmentation
  6. 06Chapter 6: Memory Management II: Virtual Memory and Page Replacement
  7. 07Chapter 7: File Systems: How Data Is Actually Stored and Retrieved
  8. 08Chapter 8: Interprocess Communication: Pipes, Shared Memory, Message Queues, Sockets
  9. 09Chapter 9: I/O Management and Interrupts
  10. 10Chapter 10: Deadlocks at the OS Level: Conditions, Prevention, Detection, Recovery
  11. 11Chapter 11: Virtualization and Containers: A Conceptual Introduction
  12. 12Chapter 12: When OS Abstractions Leak: Failure Modes Application Engineers Actually Hit
  13. 13Chapter 13: Walkthrough: What Happens When You Run a Program
  14. 14Chapter 14: Common OS Interview Questions: Paging, Page Faults, and Deadlock Scenarios
  15. 15Chapter 15: OS Concepts in System Design and Production Debugging

Chapter 8 of 15

Chapter 8: Interprocess Communication: Pipes, Shared Memory, Message Queues, Sockets

Why this matters

Chapter 2 established that processes have isolated memory — a fork()'d child cannot see the parent's later changes. But real systems constantly need cooperating processes to exchange data: a shell piping one command's output into another, a web server talking to a database in a separate process, microservices communicating across machines entirely. Interprocess communication (IPC) is the set of OS-provided mechanisms that make this possible despite memory isolation, and each mechanism makes a different trade-off between simplicity, speed, and scope (same machine vs. network).

Pipes

[BEGINNER] A pipe is a unidirectional, in-kernel byte stream connecting two processes — most familiar as the shell's | operator, but usable directly from code too. An anonymous pipe only works between related processes (e.g., parent and a forked child, since the child inherits the parent's open file descriptors); a named pipe (FIFO) has a filesystem path, letting unrelated processes connect to it.

#include <unistd.h>
#include <stdio.h>

int main() {
    int fd[2];
    pipe(fd); // fd[0] = read end, fd[1] = write end

    if (fork() == 0) {
        close(fd[1]); // child only reads
        char buf[100];
        read(fd[0], buf, sizeof(buf));
        printf("Child received: %s\n", buf);
    } else {
        close(fd[0]); // parent only writes
        write(fd[1], "hello from parent", 18);
    }
    return 0;
}

When to use it: simple, one-directional data flow between related processes on the same machine — the exact model behind shell pipelines (cat file.txt | grep error | wc -l, each stage a separate process connected by anonymous pipes the shell sets up).

Trade-off: simple and fast, but strictly unidirectional (two pipes needed for bidirectional communication) and limited to a byte stream with no built-in message boundaries — the reader has to know how to parse the stream into meaningful units itself.

Shared Memory

[BEGINNER] Shared memory maps the same physical memory frames into multiple processes' virtual address spaces — once set up, reading and writing is exactly as fast as normal memory access, with no kernel involvement (no syscall) needed per read/write, unlike every other IPC mechanism in this chapter.

#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    int fd = shm_open("/my_shared_region", O_CREAT | O_RDWR, 0666);
    ftruncate(fd, 4096);
    void *addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    if (fork() == 0) {
        sleep(1); // give the parent time to write first
        printf("Child sees: %s\n", (char*)addr); // reads directly from shared physical memory
    } else {
        strcpy((char*)addr, "written by parent");
        wait(NULL);
    }
    return 0;
}

When to use it: high-frequency, low-latency data exchange between processes on the same machine (real-time systems, high-performance data pipelines) where the overhead of copying data through the kernel (as pipes, message queues, and sockets all do) would be too costly.

Trade-off: the fastest IPC mechanism by far, but provides zero built-in synchronization — concurrent access from multiple processes without an explicit synchronization primitive (a semaphore, a mutex in the shared region itself) produces the exact same race conditions covered for threads in the companion Concurrency book, just across process boundaries instead of within one process. Shared memory is "shared mutable state" in its most literal, unguarded form.

Message Queues

[BEGINNER] A message queue lets processes exchange discrete, structured messages (not a raw byte stream) via the kernel, with the kernel handling storage, ordering, and delivery — messages can even carry a priority or type, letting a receiver selectively read specific kinds of messages rather than strictly first-in-first-out.

#include <sys/msg.h>

struct message { long type; char text[100]; };

int main() {
    int msgid = msgget(IPC_PRIVATE, IPC_CREAT | 0666);

    if (fork() == 0) {
        struct message msg;
        msgrcv(msgid, &msg, sizeof(msg.text), 0, 0); // receive any message
        printf("Received: %s\n", msg.text);
    } else {
        struct message msg = {1, "hello via message queue"};
        msgsnd(msgid, &msg, sizeof(msg.text), 0);
        wait(NULL);
    }
    return 0;
}

When to use it: structured, discrete messages between processes that don't need shared-memory-level speed, and benefit from the kernel handling message boundaries and (optionally) priority ordering directly, rather than the application parsing a raw byte stream itself.

Trade-off: more overhead than shared memory (data is copied through the kernel), but the message-boundary and ordering guarantees remove a class of parsing bugs a raw pipe/stream would leave to the application.

Sockets

[BEGINNER] A socket is the most general and most widely used IPC mechanism, because unlike every mechanism above, it works identically whether the two communicating processes are on the same machine (a Unix domain socket) or on entirely different machines across a network (a TCP/IP socket) — this uniformity is exactly why virtually all client-server and microservice communication is built on sockets specifically.

#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>
#include <unistd.h>

int main() {
    int server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, "/tmp/my_socket");

    bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(server_fd, 5);
    // A client (separate process, possibly on a different connection but same API
    // family) would connect() to "/tmp/my_socket" — or to an IP:port for a network socket,
    // with nearly identical code otherwise.
    return 0;
}

When to use it: any communication that might ever cross a machine boundary (essentially all client-server, microservice, and database-connection communication), or same-machine communication where a network-like request/response or streaming model fits naturally (Unix domain sockets get most of a TCP socket's semantics with lower overhead, since no actual network stack processing is needed for a same-machine connection).

Trade-off: more overhead and complexity than pipes for the simple same-machine case, but the only mechanism in this chapter that scales naturally from same-machine to distributed systems without an architectural rewrite — this is precisely why it's the default choice for anything that might need to scale beyond one machine (the full mechanics of sockets, TCP/UDP, and the protocols built on them are covered in depth in the companion Networking book).

Choosing an IPC mechanism

[SENIOR INSIGHT] The decision, laid out as a single table:

NeedBest fitWhy
Simple, one-way data flow, related processes, same machinePipeSimplest option, matches the shell-pipeline model directly
Maximum speed, same machine, willing to manage synchronization yourselfShared MemoryNo per-access kernel overhead — but you own the synchronization problem entirely
Structured/discrete messages, same machine, kernel-managed orderingMessage QueueKernel handles message boundaries and optional priority, at some copy overhead
Anything that might cross machines, or a request/response modelSocketThe only option that scales from same-machine to distributed without a rewrite

Common mistakes

  1. Using shared memory without any synchronization mechanism, assuming the OS provides safety guarantees it explicitly does not — shared memory is deliberately the most "get out of my way" mechanism in this chapter, and that includes getting out of the way of race conditions.
  2. Choosing a pipe for bidirectional communication without realizing a single pipe is one-directional — two pipes (or a socket) are needed for two-way communication.
  3. Building same-machine-only communication on TCP sockets by default when a Unix domain socket would provide the same programming model with less overhead (no network stack processing needed for same-machine connections).
  4. Not planning for the possibility of scaling beyond one machine when choosing pipes/shared memory/message queues for a same-machine design that might later need to become distributed — sockets are worth defaulting to specifically when that future isn't clearly ruled out.

Discussion / interview questions

  1. "Compare pipes and shared memory as IPC mechanisms — what's the fundamental trade-off?" Strong answer: Pipes are simple, kernel-mediated, and safe (no explicit synchronization needed for basic use) but limited to a unidirectional byte stream; shared memory is far faster (no per-access kernel overhead, direct memory access) but provides zero built-in synchronization, making the caller responsible for avoiding race conditions.

  2. "Why are sockets used for almost all client-server and microservice communication, even on the same machine?" Strong answer: Sockets work identically whether communicating processes are on the same machine or across a network, so a system built on sockets can scale from one machine to distributed without an architectural rewrite — no other IPC mechanism in this chapter has that property.

  3. "What are the risks of using shared memory for interprocess communication?" Strong answer: Concurrent, unsynchronized access from multiple processes produces race conditions exactly analogous to unsynchronized multi-threaded access to shared memory — the OS provides no default protection, so an explicit synchronization primitive (semaphore, mutex in the shared region) is required.

  4. "When would you choose a Unix domain socket over a TCP socket for same-machine IPC?" Strong answer: When communication is guaranteed to stay on one machine — Unix domain sockets provide a similar programming model to TCP sockets with lower overhead, since no actual network stack processing is required for a purely local connection.

Chapter summary / cheat sheet

  • Pipes: simple, unidirectional byte stream, related processes, same machine — the shell-pipeline model.
  • Shared Memory: fastest (no per-access kernel overhead), but zero built-in synchronization — the caller owns race-condition prevention entirely.
  • Message Queues: structured, discrete messages with kernel-managed ordering/priority, at some copy overhead.
  • Sockets: the only mechanism that scales naturally from same-machine to distributed — the default choice for client-server and microservice communication (full protocol mechanics in the companion Networking book).
  • Choice depends on: does it need to cross a machine boundary (sockets), does raw speed dominate over safety (shared memory + manual synchronization), or is simple/structured same-machine exchange enough (pipes/message queues)?