G27
Self-checkedTier 2 17 min reados.ipc

Inter-process communication

How processes with separate address spaces exchange data: the two models (shared memory and message passing), direct and indirect naming, blocking and non-blocking send and receive, mailbox capacity (rendezvous, bounded, unbounded), pipes and their end-of-file and broken-pipe behaviour, a shared-memory ring buffer that holds N-1 items, the lost update when a shared counter replaces it, signals and sockets, and a comparison of the mechanisms.

Why it exists

Two processes have separate address spaces on purpose: a bug or a crash in one cannot damage the other. The price is that they cannot simply share a variable. Yet processes often have to cooperate: to share information (a file or a database being used by several programs), to speed a computation up by dividing it, to keep a system modular (a web server, a database, a logger). Inter-process communication (IPC) is the set of mechanisms the operating system provides to let separate processes exchange data and coordinate, and the official syllabus names it as a topic of its own.

Prerequisites

You need the process model (address space, fork, process states) and system calls. The part on shared memory uses the ideas of race conditions, and the producer-consumer problem appears again in the classic synchronization problems.

Precise definitions

Independent and cooperating processes. A process is cooperating if it can affect or be affected by other processes.

The two basic models.

  • Shared memory. The kernel maps a region into the address spaces of two or more processes. After the set-up, reading and writing it are ordinary memory accesses, with no system call and no kernel involvement; the processes themselves must make sure that they do not interfere (synchronization).
  • Message passing. The kernel moves messages between processes. Two operations, send(message) and receive(message). Every message costs system calls and a copy, but the kernel does the synchronization.

Naming (message passing).

  • Direct communication: each process names the other: send(P, m), receive(Q, m). A link belongs to exactly one pair.
  • Indirect communication: messages go to and come from a mailbox (also called a port) that has an identity; several processes can use one mailbox: send(A, m), receive(A, m).

Synchronization of send and receive.

  • Blocking (synchronous): send waits until the message is received (or, with a buffer, until it is accepted into the buffer); receive waits until a message is available.
  • Non-blocking (asynchronous): send returns at once; receive returns a message or an indication that there is none.
  • If both are blocking and there is no buffer, the exchange is a rendezvous.

Buffering. The queue of messages of a link has one of three capacities: zero (no buffer, the sender must wait for the receiver: a rendezvous), bounded (a finite capacity; the sender blocks when the queue is full), unbounded (the sender never blocks).

Pipes. An ordinary (anonymous) pipe is a one-way channel with a read end and a write end, created with pipe(), and usually shared by fork: parent and child. For two-way communication two pipes are needed. A named pipe (FIFO) has a name in the file system, so unrelated processes can use it; on UNIX it is one-way as well.

Signals. A small asynchronous notification sent to a process (a number and nothing else). Sockets. The two ends of a communication channel, usable between processes of one machine (UNIX domain sockets) or across a network (see the socket API).

How it works

Message passing: blocking send and receive

Blocking send and receive with a bounded mailbox: start

Computed by a script, not typed
timeProducerMailboxConsumer
tick 0
tick 1
tick 2
tick 3
tick 4
tick 5
tick 6
tick 7
tick 8
tick 9
tick 10
tick 11
tick 12
tick 13
tick 14
tick 15

Step 0

A producer makes 5 messages (producing one takes 1 tick) and sends each with a blocking send; a consumer receives each with a blocking receive and then processes it (processing takes 3 ticks). The mailbox holds at most 2 messages. send and receive themselves take no time, but they wait if the mailbox is full or empty.

0 / 16
computed by script: a tick-by-tick simulation of message passing and an exhaustive search of the interleavings of a shared-memory buffer. python3 scripts/gen-os-ipc-traces.py

A producer makes messages, a consumer processes them, the mailbox holds at most two. While the mailbox has room the producer runs ahead of the consumer; when it is full, send does not return and the producer is blocked: the operating system puts it to sleep, and it uses no CPU. The consumer blocks in receive when the mailbox is empty (at tick 0, nothing has been produced yet). A mailbox is therefore also a means of synchronization: blocking operations make the processes wait for each other without a lock or a semaphore in the user's program.

What the capacity changes

What the mailbox capacity changes: start

Computed by a script, not typed
caseProducer finishesAll doneProducer blockedConsumer blockedLargest queue
slow consumer, capacity 0 (rendezvous)
slow consumer, capacity 1
slow consumer, capacity 2
slow consumer, capacity 4
slow consumer, capacity unbounded
slow producer, capacity 0 (rendezvous)
slow producer, capacity 1
slow producer, capacity 2
slow producer, capacity 4
slow producer, capacity unbounded

Step 0

Eight messages each time. With a slow consumer a bigger mailbox lets the producer run ahead (it blocks less), but the total time is fixed by the consumer. With a slow producer the consumer waits whatever the capacity.

0 / 10
computed by script: a tick-by-tick simulation of message passing and an exhaustive search of the interleavings of a shared-memory buffer. python3 scripts/gen-os-ipc-traces.py

With a slow consumer, a larger mailbox lets the producer finish earlier and block less, but the time at which everything is done does not move: it is 25 ticks for every capacity, set by the slow side. With a rendezvous (capacity 0) the producer is blocked most of the time, it must wait for the consumer at every message. With a slow producer the consumer waits for the same 17 ticks whatever the capacity, and no queue ever builds up in the mailbox (the consumer takes each message out at once). A buffer smooths out short-term differences in speed; it cannot help when one side is always slower.

Pipes

A pipe is a buffer inside the kernel, written through one descriptor and read through another. The program writes a message through a pipe from a parent to its child. The child reads until read returns 0.

#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
 
int main(void) {
    int fd[2];
    if (pipe(fd) != 0) return 1;
    pid_t pid = fork();
    if (pid < 0) return 1;
    if (pid == 0) {                          /* child: the reader */
        close(fd[1]);                        /* it never writes, so it closes the write end */
        char buf[16];
        ssize_t n, total = 0;
        while ((n = read(fd[0], buf, sizeof buf)) > 0) total += n;
        printf("child: read %zd bytes, then read returned %zd (end of file)\n", total, n);
        close(fd[0]);
        return 0;
    }
    close(fd[0]);                            /* parent: the writer */
    const char *msg = "hello through the pipe";
    if (write(fd[1], msg, strlen(msg)) < 0) return 1;
    close(fd[1]);                            /* the last write end is closed: the reader will see end of file */
    waitpid(pid, NULL, 0);
    printf("parent: wrote %zu bytes\n", strlen(msg));
    return 0;
}
child: read 22 bytes, then read returned 0 (end of file)
parent: wrote 22 bytes

The rules that follow from this:

  • read on an empty pipe blocks as long as some process still has a write end open; it returns 0 (end of file) only when all write ends are closed. This is why the child above closes its copy of the write end: if it did not, it would hold the pipe open itself and wait for ever.
  • write to a pipe whose read ends are all closed raises the signal SIGPIPE (the call fails with the error EPIPE if the signal is ignored).
  • write to a full pipe blocks. POSIX guarantees that a write of at most PIPE_BUF bytes (at least 512, 4096 on Linux) is not interleaved with writes of other processes.
  • A pipe carries a stream of bytes: the boundaries between writes are not kept.

Shared memory

After fork, the child has a copy of every ordinary variable: a change in one process is not seen by the other. A page mapped with MAP_SHARED is the same physical memory in both:

#define _DEFAULT_SOURCE 1
#include <stdio.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
 
#ifndef MAP_ANONYMOUS
#define MAP_ANONYMOUS MAP_ANON
#endif
 
int ordinary = 1;
 
int main(void) {
    int *shared = mmap(NULL, sizeof *shared, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (shared == MAP_FAILED) return 1;
    *shared = 1;
    pid_t pid = fork();
    if (pid < 0) return 1;
    if (pid == 0) {
        ordinary = 100;                      /* changes the child's own copy */
        *shared = 100;                       /* changes the page both processes see */
        return 0;
    }
    waitpid(pid, NULL, 0);
    printf("ordinary variable: %d\nshared page: %d\n", ordinary, *shared);
    munmap(shared, sizeof *shared);
    return 0;
}
ordinary variable: 1
shared page: 100

Shared memory is the fastest mechanism once it is set up, because data are not copied through the kernel; but nothing coordinates the processes, so they have to do it themselves. The classic use is the bounded buffer: a ring of NN slots, an index in where the producer writes and an index out from which the consumer reads. With only these two variables the buffer is full when (in+1) mod N=out(in + 1) \bmod N = out and empty when in=outin = out, so it holds at most N−1N - 1 items (one slot stays free to tell "full" from "empty"). Many textbooks add a shared counter count to use all NN slots; the exhaustive check shows what that costs:

Shared ring buffer: all interleavings: start

Computed by a script, not typed
variantSituations reachableViolation reachableMost items at onceStuck for ever reachableShortest path to a violation
in and out only, 2 slots
in and out only, 3 slots
in and out only, 4 slots
in and out only, 5 slots
with a shared counter, 2 slots
with a shared counter, 3 slots
with a shared counter, 4 slots
with a shared counter, 5 slots

Step 0

A producer and a consumer share a ring buffer in shared memory. Every interleaving of their single instructions was tried. Violation = a slot is overwritten before it was consumed, or the consumer reads an empty slot or the wrong item. Stuck = no process can move although the work is not finished.

0 / 8
computed by script: a tick-by-tick simulation of message passing and an exhaustive search of the interleavings of a shared-memory buffer. python3 scripts/gen-os-ipc-traces.py

For one producer and one consumer, the version with only in and out is correct over all interleavings (and never holds more than N−1N - 1 items). With the counter, a violation is reachable, and so is a stuck state. The reason is the lost update: count++ and count-- are each three instructions (load, add, store); if the two interleave, one update overwrites the other. This is the shortest interleaving that ends in a failure:

The lost update on the shared counter: start

Computed by a script, not typed
stepProcessInstructioninoutcountBuffer
step 1
step 2
step 3
step 4
step 5
step 6
step 7
step 8
step 9
step 10
step 11
step 12
step 13
step 14
step 15
step 16
step 17
step 18
step 19
step 20
step 21
step 22
step 23
step 24
step 25
step 26
step 27
step 28
step 29

Step 0

Two slots, three items. count++ is three instructions (load, add, store), and so is count--. Among all interleavings this is the shortest one that ends in a violation. Watch the counter: when the producer stores its old result, the consumer's decrement is lost.

0 / 29
computed by script: a tick-by-tick simulation of message passing and an exhaustive search of the interleavings of a shared-memory buffer. python3 scripts/gen-os-ipc-traces.py

(The in/out solution assumes that memory operations become visible in program order. Real processors can reorder them, so real code uses atomic operations or memory barriers; the model here assumes sequential consistency.) With more than one producer or consumer even the in/out version needs mutual exclusion; the semaphore solution of the bounded buffer uses three semaphores for that.

Signals

A signal is the cheapest form of IPC and carries almost no information: the number of the signal. The process installs a handler; the kernel runs it when the signal arrives. In this program the parent sends a signal to its child, which waits for it with sigsuspend. The signal is blocked before the fork, so it cannot arrive before the child is ready:

#define _POSIX_C_SOURCE 200809L
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
 
static volatile sig_atomic_t got = 0;
static void handler(int sig) { (void)sig; got = 1; }
 
int main(void) {
    sigset_t block, old;
    sigemptyset(&block);
    sigaddset(&block, SIGUSR1);
    sigprocmask(SIG_BLOCK, &block, &old);    /* blocked in the parent and inherited by the child */
    struct sigaction sa;
    memset(&sa, 0, sizeof sa);
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGUSR1, &sa, NULL);
    pid_t pid = fork();
    if (pid < 0) return 1;
    if (pid == 0) {
        while (!got) sigsuspend(&old);       /* unblocks the signal and sleeps, in one atomic step */
        printf("child: the signal arrived\n");
        return 0;
    }
    kill(pid, SIGUSR1);                      /* may be sent before the child sleeps: it stays pending */
    waitpid(pid, NULL, 0);
    printf("parent: child has finished\n");
    return 0;
}
child: the signal arrived
parent: child has finished

Sockets and the two-way channel

A pipe is one-way. A connected pair of UNIX-domain sockets is two-way: either end can read and write. The same idea, with network addresses instead of a local pair, is the socket API.

#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
 
static int read_all(int fd, char *buf, int n) {
    int got = 0;
    while (got < n) {
        ssize_t r = read(fd, buf + got, (size_t)(n - got));
        if (r <= 0) return -1;
        got += (int)r;
    }
    return got;
}
 
int main(void) {
    int sv[2];
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) != 0) return 1;
    pid_t pid = fork();
    if (pid < 0) return 1;
    if (pid == 0) {                          /* child: answers "ping" with "pong" */
        close(sv[0]);
        char buf[4];
        if (read_all(sv[1], buf, 4) < 0) return 1;
        if (write(sv[1], "pong", 4) != 4) return 1;
        close(sv[1]);
        return 0;
    }
    close(sv[1]);
    if (write(sv[0], "ping", 4) != 4) return 1;
    char reply[5] = {0};
    if (read_all(sv[0], reply, 4) < 0) return 1;
    waitpid(pid, NULL, 0);
    printf("parent got: %s\n", reply);
    return 0;
}
parent got: pong

The mechanisms compared (quoted from the textbooks, not computed)

MechanismKernel involved for each transfer?DirectionTypical useSynchronization
Shared memoryno, only to set it upanylarge data, fastestthe processes must do it themselves
Message passing (mailbox, message queue)yes, one system call per send and receiveany, by designsmall messages, also across machinesbuilt in (blocking send and receive)
Pipeyesone waya chain of programs, parent and childbuilt in: read blocks when empty, write blocks when full
Named pipe (FIFO)yesone way on UNIXunrelated processes on one machinethe same
Socketyestwo waysclient and server, across a networkbuilt in
Signalyesto one processa short notification (stop, timer, user request)asynchronous, no data

Worked examples

EX 1Which mechanism?

Easy

(a) The shell runs ls | wc -l: the output of one program is the input of the next, one way: a pipe. (b) Two programs on different machines exchange requests and answers: a socket. (c) A video decoder hands large frames to a display process on the same machine; copying every frame is too slow: shared memory (plus a way to synchronize). (d) A user presses Ctrl-C: the kernel sends the signal SIGINT to the foreground process. (e) A thread of a process wants to share a variable with another thread of the same process: nothing is needed, threads already share the address space (see threads).

EX 2Capacity and blocking

Medium

A producer needs 1 tick to make a message and a consumer 3 ticks to process one, 8 messages (the buffering table). With a mailbox of capacity 4 the producer makes 4 messages quickly and puts them in; it is blocked only for 2 ticks in all and finishes at tick 10. The consumer needs 8×3=248 \times 3 = 24 ticks plus the first tick of waiting for the first message: it finishes at tick 25 whatever the capacity. A bigger buffer helps the producer only. The same run without a buffer (rendezvous) blocks the producer for 14 ticks.

EX 3How many items does the ring hold?

Medium

A ring buffer with N=8N = 8 slots, with in and out only. It is empty when in=outin = out and full when (in+1) mod 8=out(in + 1) \bmod 8 = out. Starting from in=out=0in = out = 0 the producer can write 7 items (slots 0 to 6); in is then 7, and (7+1) mod 8=0=out(7 + 1) \bmod 8 = 0 = out: full. The consumer reads one item: out=1out = 1; now the producer may write slot 7, and in becomes 0. The buffer is full again with 7 items, although all 8 slots have been used over time. With the extra count variable all 8 could be used, but then the lost-update problem appears (the exhaustive search above).

EX 4Typical questions

GATE level

(a) "In which IPC mechanism does the kernel not take part in the transfer of each message?" Shared memory. (b) "What does read return on a pipe when all write ends have been closed and the pipe is empty?" 0. (c) "A process writes to a pipe that nobody can read." SIGPIPE. (d) "Parent creates a pipe, then forks. Both processes write to the pipe, and both read from it: can there be confusion?" Yes, a message written by one can be read by the same process: for two-way communication use two pipes (or a socket pair), each used in one direction. (e) "A blocking send with zero capacity is called?" A rendezvous. (f) "Which of these is faster for large data: message passing or shared memory?" Shared memory, after the set-up. (g) "A forked child changes a global variable. Does the parent see it?" No, unless the variable lives in memory mapped as shared.

Cheat box

Cheat box

  • Two models: shared memory (no kernel per transfer, you synchronize) and message passing (send and receive, kernel copies and synchronizes).
  • Naming: direct (name the process) or indirect (mailbox, port). Send/receive: blocking or non-blocking. Capacity: zero (rendezvous), bounded, unbounded.
  • Pipe: one-way byte stream, parent and child; read returns 0 only when all write ends are closed; write with no reader gives SIGPIPE; two-way needs two pipes. FIFO: a pipe with a name.
  • Ring buffer with in and out: full when (in+1) mod N=out(in+1) \bmod N = out, empty when in=outin = out, holds N−1N - 1 items.
  • A shared counter updated with count++ and count-- has a lost update (load, add, store interleave).
  • A signal carries only its number. Sockets are two-way and also work across a network.

Traps and GATE patterns

Connections

IPC between processes is the counterpart of the sharing that threads get for free: threads share an address space, so they communicate through variables and need critical sections; processes must ask the kernel. The blocking behaviour of send, receive and pipes is the same idea as the semaphore: the process is moved to the waiting state of the process life cycle until the other side acts. The buffering of I/O between a device and a program is the same producer-consumer structure.

PYQs

No PYQs attached to this concept yet

Previous year questions are being added section by section from your PYQ book, each with my own re-checked answer. Nothing is shown from memory.

Self-check

Question 1

A pipe is created and a process forks. The child only reads and the parent only writes, but neither closes the end it does not use. The parent writes some bytes and closes its write end. Will the child's read ever return 0?

Show answer

No. The child still holds its own copy of the write end, so one write end is open and read blocks when the pipe is empty. The child must close the write end it does not use.

Question 2

Producer and consumer with a mailbox of capacity 3, the consumer much slower. After a long time, how often is the producer blocked?

Show answer

Almost always: the mailbox fills up and from then on the producer can send one message each time the consumer takes one. The throughput is the consumer's.

Question 3

Why does count make the shared-memory buffer incorrect even for one producer and one consumer?

Show answer

count++ by the producer and count-- by the consumer are each load, add, store. If both load the same value, the later store overwrites the other's change. The counter then differs from the real number of items, and the consumer may read an empty slot or the producer overwrite a full one.

Revision card

One-screen revision

  • Shared memory: fast, no kernel per transfer, you synchronize. Message passing: send/receive, kernel copies, built-in blocking.
  • Direct or indirect (mailbox); blocking or not; capacity 0 (rendezvous), bounded, unbounded.
  • Pipe: one-way; read returns 0 when all write ends closed; write without reader gives SIGPIPE; two-way needs two pipes.
  • Ring with in/out: N−1N - 1 items; with count: lost update.
  • Signals: number only. Sockets: two-way, also across machines.

References

  • ·Silberschatz, Galvin, Gagne, Operating System Concepts (10th edition), Chapter 3 Processes (Interprocess communication: shared memory, message passing, pipes, examples)
  • ·Tanenbaum, Bos, Modern Operating Systems (4th edition), Chapter 2 Processes and threads (Interprocess communication)
  • ·Stevens, Rago, Advanced Programming in the UNIX Environment (3rd edition), Chapters 15 and 17 (Interprocess communication, advanced IPC)

Verification

scripts/enum-checks/os_ipc_check.py: (1) the message-passing numbers (tick-by-tick simulation with a zero-time closure) agree with a closed-form recurrence (send i completes when the message is produced and a slot is free, receive i returns when the consumer is ready and the message was sent) on every combination of 1 to 9 messages, production and processing times 0 to 5 and capacities 0 to 7 and unbounded (thousands of cases); (2) the ring buffer (scripts/os_ipc.py) is compared with a second encoding of the two processes written as separate generators: same number of reachable situations, same verdict (no violation with in and out only, for 2 to 5 slots and up to 7 items; a violation with the shared counter, with the same shortest length 29), the buffer holds at most N-1 items without the counter; (3) the C programs printed on the page are compiled and run and their output is compared with the page; (4) pipe facts are tried on real pipes (read returns 0 at end of file, write after the reader closed fails with EPIPE, a read on an empty pipe with a writer open would block, shown with a non-blocking read). tests/os-ipc-traces.test.ts recomputes the message-passing tables with the recurrence and the ring buffer with a breadth first search written again in TypeScript. CONVENTIONS: send and receive take no time themselves; a message is available to the consumer at the tick at which the send completes; the consumer's slot is freed when its receive returns; the producer does not start producing the next message before its send has returned. The model of the ring buffer assumes sequentially consistent memory: on real processors the in and out updates need atomic operations or memory barriers. The slot is cleared after reading only to make 'empty slot' visible to the model. NOT COMPUTED, quoted from the textbooks: the comparison table of the mechanisms, remote procedure calls, the behaviour of signals other than the one program shown. Chapter references are from memory and need verification. SECOND PASS (2026-11-02, done by the assistant, not a human check): every Self-check question was solved before the printed answer was read and all agreed; the definitions, worked examples, cheat box and traps were read claim by claim and the numbers re-derived by hand. One loose statement about the mailbox contents (slow producer) fixed to match the trace. Status raised to self-checked; verified needs a textbook or human check.

last verified: never