3.0 KiB
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), 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/ipc.zig): a fixed-size
ring buffer of messages with a producer/consumer rendezvous, built on the
scheduler's wait queues.
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/recvrun undersaveInterrupts/restoreInterrupts(the composable form, see scheduling.md), so checking the condition and committing the block/enqueue happen atomically with respect to the timer preempting mid-operation.waitLocked/wakeLockedare the variants that assume the caller already holds that critical section.
Verifying it
The ipc test (see 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.