# IPC: message-passing channels Inter-process communication is the **backbone of a microkernel**. Once drivers and services run isolated in their own address spaces ([vision](vision.md)), they can't just call each other — a request becomes a **message**. In a microkernel, whatever was a function call across a monolithic kernel is IPC, so it's a first-class concern, not an afterthought. This first form is a **bounded blocking channel** (`src/kernel/ipc.zig`): a fixed-size ring buffer of messages with a producer/consumer rendezvous, built on the scheduler's [wait queues](scheduling.md). ## The channel `Channel(T, capacity)` is generic over the message type and buffer size. It holds a ring buffer, a count, and two wait queues: - **`send(msg)`** — if the channel is full, block on the *not-full* queue; otherwise write the message, bump the count, and wake a waiting receiver. - **`recv()`** — if the channel is empty, block on the *not-empty* queue; otherwise take a message, drop the count, and wake a waiting sender. Neither side busy-waits: a full channel parks the sender, an empty one parks the receiver, and each operation wakes the other side when it makes progress possible. Two details make it correct: - **Recheck in a loop.** A woken task re-tests the condition (`while (full) wait`) rather than assuming the slot is still available — another waiter may have taken it first. This is the standard guard against spurious or racing wakeups. - **One critical section.** `send`/`recv` run under `saveInterrupts` / `restoreInterrupts` (the composable form, see [scheduling.md](scheduling.md)), so checking the condition and committing the block/enqueue happen atomically with respect to the timer preempting mid-operation. `waitLocked` / `wakeLocked` are the variants that assume the caller already holds that critical section. ## Verifying it The `ipc` test (see [testing.md](testing.md)) runs a producer and a consumer passing **100 messages through a 4-slot channel**. The small buffer means the channel goes full and empty over and over, so both the blocking-send and blocking-recv paths are exercised heavily. The messages arrive intact and in order (their sum is the expected `5050`), and neither task busy-waits — they block and wake each other. ## What's next (not done here) - **Across address spaces.** Today both endpoints are kernel threads sharing the kernel's memory, so the message is copied within one address space. When user mode arrives, the same channel carries messages between *isolated* processes, copying the payload across the boundary — which is where IPC earns its place as the microkernel's backbone. - **Synchronous call/reply.** A request/response pattern (send-and-wait-for-reply) on top of channels, the shape most driver/service calls take. - **Interrupts as messages.** A hardware interrupt delivered to the driver task that owns the device, as an IPC message. - **Priority inheritance** through IPC, so a high-priority client blocked on a low-priority server doesn't suffer unbounded priority inversion.