danos/docs/ipc.md

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/kernel/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/recv run under saveInterrupts / 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 / wakeLocked are 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.