Map

Notify-and-Pull IPC

Wiki conceptipcmicrokernelconcurrencyoperating-systems โ†ณ show in map Markdown
title
Notify-and-Pull IPC
type
concept
summary
Pattern that combines synchronous message passing with non-blocking notifications to avoid server-to-server deadlock
tags
ipc, microkernel, concurrency, operating-systems
created
2026-05-06
updated
2026-05-06

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