Processes are isolated on purpose: each has its own address space, so one can’t read or corrupt another’s memory. Interprocess communication (IPC) is the set of deliberate holes in that wall. The interview question usually sounds like “how would these two processes talk?”, and a strong answer picks a mechanism from the shape of the data and the trust between the two sides before worrying about speed. Speed still matters, though, and it’s easy to measure, so this note does both.
The menu
The common options on Unix-like systems:
- Pipes. A one-way byte stream, usually between a parent and child (
ls | grep). Named pipes (FIFOs) give one a path in the filesystem so unrelated processes can open it. - Unix domain sockets. Two-way, stream or datagram, addressed by a filesystem path, and usable by unrelated processes on the same host. They also carry extras that TCP can’t: the peer’s user and process IDs (
SO_PEERCREDon Linux,getpeereidon macOS) and open file descriptors (SCM_RIGHTS). Docker’s API socket and PostgreSQL’s local connections use them. - TCP over loopback. The same API as a network connection, so the code works unchanged when the other side moves to another machine. More protocol overhead, and anything that can reach the port can connect.
- Shared memory. Both processes map the same physical pages (
shm_openplusmmap, or Python’smultiprocessing.shared_memory). No copies through the kernel, and no kernel help with coordination either. - Others. POSIX message queues (on Linux; macOS doesn’t implement them), signals for tiny notifications, plain files, and higher-level systems built on these: gRPC, D-Bus on Linux desktops, XPC on macOS.
Moving 64 MiB four ways
To compare costs, a parent forks a child, and the child sends 64 MiB of random bytes to the parent, which receives them into a pre-allocated buffer and checks they arrived intact. The child waits for a start signal so process creation isn’t timed. Shared memory gets the same treatment: the child writes straight into a shared segment, then sends one byte on a pipe to say the data is ready, and the parent reads the data in place.
import os, socket, time
from multiprocessing import shared_memory
TOTAL = 64 << 20 # move 64 MiB from a child process to its parent
CHUNK = 256 << 10
source = os.urandom(TOTAL) # the child inherits this (copy-on-write)
dest = bytearray(TOTAL); dest[:] = bytes(TOTAL) # pre-faulted destination in the parent
def stream(label, make_pair):
send, recv_into = make_pair()
src, dst = memoryview(source), memoryview(dest)
go_r, go_w = os.pipe()
pid = os.fork()
if pid == 0:
os.read(go_r, 1) # wait for the start signal
for off in range(0, TOTAL, CHUNK): send(src[off:off + CHUNK])
os._exit(0)
time.sleep(0.2) # let the child finish starting up
t0 = time.perf_counter(); os.write(go_w, b"g")
got = 0
while got < TOTAL:
got += recv_into(dst[got:])
dt = time.perf_counter() - t0
os.waitpid(pid, 0)
assert dest == source
print(f"{label:24} {TOTAL / dt / 2**30:5.2f} GiB/s")
def unix_pair(bufsize=None):
p, c = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
if bufsize:
for s in (p, c):
s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, bufsize)
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, bufsize)
return c.sendall, lambda buf: p.recv_into(buf[:CHUNK])
# pipe_pair() and tcp_pair() return the same (send, recv_into) pair for a pipe
# and a loopback TCP connection; shm() times the shared-memory version.One run on the M1, with Python 3.14:
pipe buffer before writer blocks: 65536 bytes
pipe 0.72 GiB/s
Unix socket, default buf 0.68 GiB/s
Unix socket, 1 MiB buf 1.77 GiB/s
TCP over loopback 1.64 GiB/s
shared memory 4.82 GiB/sAcross five more runs the ranges were: pipe 0.75 to 1.90 GiB/s, Unix socket with default buffers 0.69 to 0.82, with 1 MiB buffers 1.73 to 3.09, TCP 1.53 to 1.70, and shared memory 2.87 to 3.76. This laptop was under memory pressure at the time, so treat the numbers as a ranking, not a spec sheet.
Reading the results
Every stream mechanism copies twice. write() copies from the sender’s memory into a kernel buffer, and read() copies from the kernel buffer into the receiver’s. For bulk data, that double copy sets a ceiling.
Buffer size decides how often each side sleeps. A pipe’s writer blocks when the kernel buffer is full, which on this Mac happened at 65,536 bytes. A Unix domain socket on macOS defaults to 8,192-byte send and receive buffers (sysctl net.local.stream.sendspace and the socket’s SO_SNDBUF both report 8192). With buffers that small, the sender fills them, blocks, the receiver wakes, drains, blocks, and the two processes spend their time handing the CPU back and forth. Raising the buffers to 1 MiB with setsockopt made the same socket 2 to 4 times faster in every run. Linux uses larger defaults, so the gap there is smaller (I didn’t run the Linux comparison). TCP over loopback did better than the default Unix socket for the same reason: bigger buffers, despite running a whole protocol stack.
Shared memory did one copy and almost no system calls. The child wrote straight into pages the parent could see, and one byte on a pipe announced completion. If the producer generated the data in place, even that copy would disappear. That’s why video pipelines, browsers’ compositors and machine-learning data loaders move large buffers through shared memory and send only small control messages through sockets.
What shared memory makes you responsible for
The speed comes from the kernel stepping out of the way, which means everything the kernel was doing for you is now your job.
Synchronization. Nothing tells the reader when data is complete. The test used a pipe message; real systems use process-shared mutexes or semaphores placed in the segment, or a lock-free ring buffer with head and tail indices updated with atomic operations and the right memory ordering. Get it wrong and the reader sees half-written records.
Crash consistency. If a writer dies halfway through an update, the segment holds a torn record and possibly a lock nobody will release. Linux’s robust mutexes report EOWNERDEAD to the next locker so it can repair the state; with semaphores you get no such notice.
Validation. A process that maps memory writable by another process can’t trust anything in it. A classic bug is the double fetch: the reader checks a length field, then reads it again to use it, and a hostile writer changes it in between. Copy fields into private memory before validating them, or use sockets across trust boundaries.
Cleanup. A named segment outlives the processes that created it until someone unlinks it. On Linux, leaked segments sit in /dev/shm using RAM until reboot. Python’s multiprocessing runs a resource tracker that warns about segments left behind at shutdown.
Trust boundaries favour sockets
When the two processes have different privileges, say a root daemon and an unprivileged client, a socket is usually the right answer even if it’s slower. Every message arrives as a separate copy the receiver can validate before acting on it. The receiver can check who’s calling with peer credentials, and the socket file’s permissions decide who may connect at all. File descriptor passing even lets a privileged process open something on a client’s behalf and hand over just that descriptor, which is how privilege-separated designs give out narrow capabilities instead of broad access.
Interview practice: moving images to worker processes
A Python service runs eight worker processes for image inference. The parent receives 4 MB images and passes each one to a worker through a multiprocessing.Queue; results come back the same way. Profiling shows about 30% of total CPU in pickling and copying. How would you redesign the data path, and what new problems would you have to handle?
Answer
multiprocessing.Queue pickles each object and pushes the bytes through a pipe, so every image is serialised, copied into the kernel, copied out, and unpickled: several full copies of 4 MB per request, plus the small-buffer ping-pong measured above. The data is large and the processes trust each other, which is the case shared memory is for.
I’d allocate a shared memory pool of fixed-size slots at start-up, say 32 slots of 8 MB. The parent writes each incoming image into a free slot and sends only the slot number (a few bytes) through the queue. The worker reads the image in place, ideally viewing it as a NumPy array over the shared buffer so even the decode step avoids a copy, writes its result into the same slot or a result area, and sends the slot number back. The queue messages now double as the synchronization: whoever holds a slot number owns that slot, and ownership changes only when a message changes hands.
The new problems: oversized images need a fallback path or a bigger slot class; a worker that crashes while holding a slot must be detected (the parent watches worker exit codes) and its slot reclaimed; the pool must be unlinked on shutdown, including on crashes; and slot reuse must wait until the parent has finished with a result. I’d measure before and after, since in the runs above shared memory moved data between about 1.5 and 7 times faster than a pipe, but the real gain depends on how much of that 30% was copying versus pickling.
See also
- Operating Systems interview questions
- Operating Systems MCQs
- Processes Versus Threads and Shared State
- Producer-Consumer Queues and Backpressure
- pipe(7) on man7.org, on pipe capacity, atomic writes and blocking behaviour