58 lines
3.0 KiB
Markdown
58 lines
3.0 KiB
Markdown
# 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.
|