# Notify-and-Pull IPC

Synchronous IPC has real virtues — deterministic behavior, no kernel-side queue allocation, no DoS via queue flooding, easier debugging. The problem appears the moment two server processes need to send to each other. Each sender blocks on the other being in receive state, and neither one ever gets there. Seiya Nuta's [[microkernel-ipc-design]] uses the canonical example of a TCP/IP server and an Ethernet driver: TX requests block on the driver's receive state, RX deliveries block on TCP/IP's receive state, deadlock.

The fix that synchronous-IPC microkernels (Resea, Hubris, FTL) converge on is a second, restricted asynchronous channel called a *notification*. A notification has no payload and no count — it's a single boolean flag per mailbox. Setting the flag never blocks. The receiver, on its next `ipc_receive`, gets either a normal message or a `NOTIFIED` status. There is no way for the notifier to know how many times it notified; the contract is just "the peer wants you to do something, come ask."

The full pattern is:

1. The server (e.g., Ethernet driver) gets data (e.g., an interrupt with an RX packet).
2. It enqueues the data in its own internal buffer.
3. It calls `ipc_notify(client_mbox)` — non-blocking.
4. The client (e.g., TCP/IP server) eventually wakes from `ipc_receive` with the `NOTIFIED` status.
5. The client sends a normal synchronous `RECEIVE_PACKET_MSG` request.
6. The server replies synchronously with the buffered data.

The asymmetry is what avoids the deadlock: the *notify* leg is async (and trivially small — one bit of state), the *pull* leg is the normal synchronous request-reply flow. Crucially, this requires that the system clearly designate a client side and a server side per channel, even when both processes are servers in some other sense. The notification points one direction; the synchronous request goes the other.

The notification's deliberate amnesia (no count, no payload) is the cheap part of the design. The kernel doesn't need a queue, just a flag. If a client misses ten notifications because it was busy, that's fine — when it finally checks the channel, it pulls whatever happened to be in the server's buffer. The notification is hint, not data.

## Relationship to other patterns

Pull-based IPC (see [[microkernel-ipc-design]] §"Pull, not Push") is the natural payload-side complement: the client pulls only when ready, and the server's queue is bounded by what fits in its own buffer rather than by the rate at which the kernel will accept enqueued messages. Together, notify-and-pull plus pull-based give you a fully synchronous payload path with one-bit asynchronous wake-up.

The Linux equivalent is roughly `eventfd` plus a synchronous read on a separate file descriptor — the kernel signals readiness, userspace pulls the bytes. `io_uring` extends the same idea with explicit completion queues. The shape recurs because backpressure is much easier when the consumer drives the dequeue.

This is one of the specific failure-mode fixes that messaging systems layer on top of plain message passing — relevant to the broader [[message-passing-vs-shared-memory]] argument that message passing doesn't eliminate concurrency bugs, it just renames them.

## See also

- [[microkernel-ipc-design]] — Nuta's full FTL writeup
- [[message-passing-vs-shared-memory]] — concurrency-coordination framing
- [[go-channel-bug-patterns]] — analogous deadlock failure mode in Go channels
