Abstractions 3: IPC, Pipes and Sockets
Source: UC Berkeley CS162, Fall 2020/2021 - Prof. John Kubiatowicz, Lecture 5
Processes are isolated by design - so how do two of them talk? This lecture builds
inter-process communication (IPC) on the file abstraction from the last
lecture: a pipe connects two processes on one machine, a socket connects
two processes across a network, and both are used through the same
read/write calls. It ends by building a real client/server.
The key idea: communication looks like file I/O
The whole lecture rests on one idea: sending data between processes should look
exactly like writing to and reading from a file. One side does write(wfd, ...),
the other does read(rfd, ...), whether they are on the same machine or opposite
sides of the world.

Why processes need a channel: isolation
Recall why this is even a problem. Each process has its own address space; the hardware translation tables guarantee one process cannot see another's memory. That isolation is exactly what makes direct communication impossible - so the OS must provide an explicit channel.

Pipes
A pipe is a one-directional, in-kernel byte queue with two ends: you write
to one file descriptor and read from the other. pipe() returns both
descriptors at once.

Within a single process you can see the mechanics: create the pipe, write to
pipe_fd[1], read from pipe_fd[0].
![Single-process pipe example: int pipe_fd[2]; if (pipe(pipe_fd) == -1) fail; write(pipe_fd[1], msg, ...); read(pipe_fd[0], buf, BUFSIZE); close(pipe_fd[0]); close(pipe_fd[1])](/assets/images/l4-single-process-pipe-690ee0d4b8890bf3cdc342e3c471623a.png)
Pipes between processes
The real use is pipe then fork: the child inherits the descriptor table,
so both processes hold both ends of the same pipe. Each closes the end it does not
use, leaving a one-way channel from parent to child (or the reverse).


After fork, every pipe end is open in both processes. If a reader leaves the
write end open, it will never see end-of-file, because some descriptor could
still write. Each process must close the ends it doesn't use, or the pipe hangs.
EOF on a pipe
That closing discipline is what makes EOF meaningful: a read on a pipe returns
0 (end of file) only once all write ends are closed.

pipe()returns two descriptors:[0]is the read end,[1]is the write end.- A pipe is one-directional and kernel-buffered;
readblocks when it's empty,writeblocks when it's full. readreturns 0 (EOF) only when all write ends are closed - so unused ends must be closed.
Once you can communicate, you need a protocol
A channel moves bytes; it says nothing about what they mean. A protocol is the agreement on how to communicate - the syntax (message format and order) and semantics (what each message means, and what to do on a timeout).

Client/server: cross-network IPC
Scale the idea up and you get the client/server model: many clients talk to one server over the network. The channel is now a socket.

A socket behaves just like a file descriptor with two queues: write appends to
the outgoing queue, read drains the incoming one. Unlike a pipe it is
bidirectional.

Setting up a connection over TCP/IP
Before you can read/write, the two ends must be connected. The server creates
a listening socket on a well-known port; a client connects to it; the connection
is identified by a 5-tuple (source IP/port, destination IP/port, protocol).

- A connection is a 5-tuple: source IP, source port, destination IP, destination port, and protocol (TCP here).
- Server ports are well-known: 80 (web), 443 (HTTPS), 25 (mail); client ports are usually assigned at random by the OS.
acceptreturns a new socket per client, so the original listening socket keeps accepting more.
The socket API
The two sides use complementary calls. A client creates a socket and
connects. A server creates a socket, binds it to a port, listens, and
accepts connections in a loop.


Putting both sides together gives the full lifecycle, and a common design choice: fork a child per connection so a crash or compromise in one is isolated.

Handling many clients: process, thread, or pool
Forking per connection is safe but heavy. Spawning a thread per connection is cheaper but drops the isolation. Both are unbounded - too many clients exhausts resources - so real servers use a thread pool: a fixed set of workers pulling connections off a queue.

- Fork a process per connection - maximum isolation (a crash or exploit is contained), highest cost.
- Spawn a thread per connection - cheaper to create and switch, but no isolation and still unbounded.
- Thread pool - a fixed worker set draining a queue; bounds resource use under load. Pick this when you expect many clients.
Recap
- IPC is built on file I/O:
read/writemove bytes between processes, because process isolation makes direct memory sharing impossible. - A pipe is a one-way, kernel-buffered channel;
pipe+forkputs its ends in two processes, and EOF only arrives once all write ends close. - A protocol is the agreement (syntax + semantics) layered on the channel.
- A socket is a bidirectional, networked file descriptor; a connection is a
5-tuple, set up with the
socket/bind/listen/accept(server) andsocket/connect(client) calls. - Servers handle many clients by forking, threading, or - best under load - a thread pool.
- That completes the foundation: threads, address spaces, processes, dual mode, and the file/IPC abstractions. Next comes making concurrent access to shared state correct - Synchronization.