IPC (Pipes, Shared Memory, Signals)
Inter-process communication (IPC) is how two already-created, already-scheduled processes actually exchange data or events with each other, a separate problem from either creating or scheduling them: Multitasking covers getting more than one process running at all, but says nothing about how two of them, once running, talk to each other.
A pipe is a kernel-managed buffer exposed to userspace as two file descriptors, a read end and a write end, most commonly created as a pair inherited by two related processes across a fork (a shell connecting one command’s output to another’s input, ls | grep foo, is the textbook case). Data written to the write end is copied into the kernel’s own buffer, not directly into the reading process’s memory, and a read from the read end copies back out of that same buffer, in FIFO order, with no seeking or random access, only a strict first-in-first-out stream.
int fds[2];pipe(fds); // fds[0] = read end, fds[1] = write end
if (fork() == 0) { close(fds[0]); write(fds[1], "hello", 5);} else { close(fds[1]); char buf[5]; read(fds[0], buf, 5);}The buffer’s capacity is fixed (commonly 64 KB on Linux) rather than growing without bound: a writer that fills it blocks until a reader drains some of it, and a reader on an empty pipe blocks until a writer supplies more, which is what makes a pipe a synchronization mechanism as much as a data-transfer one, coordinating the pace of two otherwise-independent processes without either needing to poll the other. A read or write on a pipe with the other end already closed behaves distinctly rather than blocking forever: reading an empty pipe whose write end is closed returns end-of-file immediately, and writing to a pipe whose read end is closed raises SIGPIPE rather than blocking on a reader that will never arrive.
Shared memory
Section titled “Shared memory”Shared memory takes a different approach entirely: rather than copying data through the kernel on every transfer, it maps the exact same physical frames into two (or more) processes’ separate virtual address spaces at once, so a write one process makes is visible to the other immediately, with no copy and no kernel call involved in the transfer itself. This is the same physical-frame-to-multiple-mappings idea Paging already covers for other purposes (copy-on-write, for instance), applied here deliberately rather than as a fault-triggered side effect: both processes’ page tables point separate virtual addresses at identical physical frames from the moment the shared region is set up, not only after some later access forces it.
void *shm = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);// writes through *shm are visible to any other process// mapping the same shm_fd, with no read()/write() call at allBecause there’s no kernel mediation on each individual access, shared memory reintroduces exactly the race conditions Synchronization already covers for threads within one process, now between entirely separate processes instead: two processes writing to the same shared structure at once need the same locks, atomics, or other coordination a multithreaded program would, since the kernel enforces nothing about ordering or exclusivity on a shared mapping by itself. This is the core tradeoff against pipes: shared memory is dramatically faster for high-volume or low-latency transfer, since large data never has to be copied through the kernel at all, but it pushes every synchronization concern back onto the processes using it, where a pipe’s blocking read/write behavior handles a substantial part of that coordination automatically.
Signals
Section titled “Signals”A signal is an asynchronous notification delivered to a process, interrupting whatever it’s currently doing (much like a hardware interrupt interrupts whatever the CPU is currently executing) rather than requiring the process to actively read something to learn a signal arrived. SIGKILL terminates a process unconditionally with no way for the target to intervene; SIGTERM requests termination but can be caught and handled first, letting a process clean up before exiting; SIGCHLD notifies a parent that a child process has exited, commonly used to trigger a wait call rather than polling for child completion.
void handler(int sig) { // runs asynchronously, interrupting whatever the process was doing cleanup(); _exit(0);}
signal(SIGTERM, handler);A registered handler runs on the receiving process’s own stack, interrupting its normal control flow at essentially any point, which is what makes signal handlers restricted to async-signal-safe operations: code that was in the middle of, say, malloc’s own internal bookkeeping when a signal arrived and interrupted it cannot safely call malloc again from inside the handler, since the interrupted call may have left the allocator’s internal state only partially consistent. Delivering a signal to a process currently blocked in a system call (a read waiting on that same pipe from the section above, for instance) commonly interrupts the call early, returning an error the caller has to check for and handle rather than silently retrying, a detail that trips up code written without signals in mind from the start.
Implementation notes
Section titled “Implementation notes”A kernel implementing pipes needs the buffer itself to be a genuinely separate object from either process’s own memory, referenced by both descriptors’ file table entries rather than owned by whichever process happened to create it, since either end can outlive the process that opened it (inherited across further fork calls, for instance) and the buffer has to persist as long as at least one reference to either end remains open. Shared memory implementations have to track reference counts on the underlying physical frames similarly, tearing the mapping down (and freeing the frames) only once every process sharing it has unmapped or exited, not simply when the first one does. Signal delivery to a process currently descheduled, not running on any CPU at the moment a signal arrives, has to be recorded as pending and actually delivered the next time that process is scheduled to run, rather than requiring the process to be executing at the exact instant the signal is generated.
References
Section titled “References”- ^ W. R. Stevens and S. A. Rago, Advanced Programming in the UNIX Environment, Chapters 15 and 10: the standard reference on pipes, shared memory, and signal handling on Unix-like systems.
See also
Section titled “See also”- Multitasking: getting more than one process running at all, the prerequisite this article’s communication mechanisms build on.
- Synchronization: the same race conditions covered there for threads, reintroduced here between separate processes sharing memory.
- Paging & Virtual Memory: the multiple-mappings-to-one-frame mechanism shared memory is built directly on.
- Process Termination, Zombies, and wait(): what a parent actually does in response to the SIGCHLD signal described above.