SPEC-007 - Inter-Thread Communication: Message Queues, Pipes, Buffer Pools, Ports¶
Implementation and release scope: Project status records the implemented subset; roadmap defines release gates. Acceptance of this design does not establish implementation, verification, or support. Reconciled 2026-10-14.
Status: Accepted 2026-10-07 by the project owner (“do all”), with the defaults of §11. Specification work item 6b of the roadmap (07 §3), added because M2 delivers message queues.
Requirements: docs/requirements/KRN-IPC.md (KRN-IPC-001 to 005 from v0.1 §9.4 and 006 to 009 from 03 §6 restated; new from 010).
Builds on: SPEC-004 (wait protocol, hand-off), SPEC-006 §3 (binding), SPEC-001 §5, §6 (contexts, handles, storage), SPEC-005 §4 (the semaphore as the pattern a queue repeats twice), 03 §6.3 to §6.6, §9.2; ADR-020, ADR-027, ADR-032 (leases).
Research: R-001 §6: copy-by-value queues with direct copy into a waiting receiver (ThreadX, Zephyr, RTEMS) are the fast common case; FreeRTOS stream buffers show the lock-free single-producer single-consumer byte stream; Zephyr’s pipe rewrite shows partial transfers with per-call minimums; zero copy by ownership transfer (buffer pools) is the only zero-copy form that is safe inside one address space, and leases (Hubris) the only one that is safe across partitions.
1. Scope and terms¶
Primitive |
Target design |
Blocking ( |
ISR-safe ( |
Section |
|---|---|---|---|---|
Message queue |
yes |
|
|
§2 |
Pipe (byte stream) |
yes, |
|
|
§3 |
Buffer pool |
yes |
|
|
§4 |
Port |
FUTURE |
request, reply |
none |
§5 |
A message is a fixed-size item copied by value. A block is a fixed-size buffer from a pool whose ownership moves with the message that carries its pointer (zero copy). A stream is bytes without message boundaries. Everything is caller-provided storage (KRN-IPC-002, 03 §9.2), every blocking operation is a thin layer over SPEC-004, and every ready transition can signal a binding (SPEC-006 §3).
2. Message queue¶
2.1 Interface¶
typedef struct emb_msgq_attr {
const char *name;
void *storage; /* ring buffer: capacity * item_size bytes, aligned to EMB_MSGQ_ALIGN */
uint16_t item_size; /* 1 .. CONFIG_EMB_MSGQ_MAX_ITEM (default 64) */
uint16_t capacity; /* >= 1 */
uint8_t flags; /* EMB_OBJ_ABORT_WAITERS | EMB_OBJ_FIFO | EMB_MSGQ_OWNERSHIP */
emb_pool_t pool; /* EMB_MSGQ_OWNERSHIP: the pool whose blocks travel in this queue */
} emb_msgq_attr_t;
#define EMB_MSGQ_RING(name, item_size, capacity) /* declares aligned storage of the right size */
emb_status_t emb_msgq_init(emb_msgq_storage_t *storage, const emb_msgq_attr_t *attr, emb_msgq_t *out); /* @ctx prekernel thread */
emb_status_t emb_msgq_destroy(emb_msgq_t q);
emb_status_t emb_msgq_send(emb_msgq_t q, const void *item, emb_timeout_t timeout); /* @ctx thread; thread isr with EMB_NO_WAIT @time O(1) + copy */
emb_status_t emb_msgq_send_until(emb_msgq_t q, const void *item, emb_instant_t deadline);
emb_status_t emb_msgq_send_front(emb_msgq_t q, const void *item); /* @ctx thread isr EMB_NO_WAIT only: urgent, to the head */
emb_status_t emb_msgq_receive(emb_msgq_t q, void *out_item, emb_timeout_t timeout); /* @ctx thread; thread isr with EMB_NO_WAIT */
emb_status_t emb_msgq_receive_until(emb_msgq_t q, void *out_item, emb_instant_t deadline);
emb_status_t emb_msgq_peek(emb_msgq_t q, void *out_item); /* @ctx thread isr copies the head without removing it */
uint16_t emb_msgq_count(emb_msgq_t q); /* @ctx thread isr snapshot */
uint16_t emb_msgq_free(emb_msgq_t q);
emb_status_t emb_msgq_bind_notify(emb_msgq_t q, emb_thread_t t, emb_notify_bits_t bit); /* receive side: empty -> non-empty */
2.2 Semantics¶
Storage. The ring holds
capacityitems ofitem_sizebytes;EMB_MSGQ_RING()declares it with the alignment the kernel needs for word copies.item_sizeaboveCONFIG_EMB_MSGQ_MAX_ITEMisEMB_EINVAL: large payloads travel as block pointers (§4), never as large copies.Two wait queues: receivers (when empty) and senders (when full), both with the object’s wait policy (
PRIORITY_FIFOdefault,EMB_OBJ_FIFOoptional).Send. If a receiver waits: copy the item directly into the receiver’s
out_itemand wake it with hand-off (the ring is untouched; no binding bit, SPEC-004 §6.3). Else if the ring has room: copy into the tail slot; if the ring was empty, signal the binding. Else: block on the senders’ queue; the receiver that frees a slot copies this sender’s item into the freed slot and wakes it (slot hand-off), so a woken sender never re-checks. The copy happens inside the object’s critical section; its cost is bounded byCONFIG_EMB_MSGQ_MAX_ITEMand is part of the masked-time budget (R-003 T7).Send to front. Writes at the head (the next item received) and never blocks (
EMB_ETIMEDOUTwhen full); for urgent messages (03 §6.3).Receive. If the ring is non-empty: copy the head; then, if a sender waits, move its item into the freed tail slot and wake it. Else if a sender waits with the ring empty (capacity reached zero is impossible; this branch is for
capacityconsumed bysend_frontraces and is kept for completeness): hand the sender’s item directly. Else block on the receivers’ queue; the hand-off delivers the item intoout_itembefore the wake.Peek copies the head without removing it;
countandfreeare snapshots.Ownership queues. With
EMB_MSGQ_OWNERSHIP, items are block pointers ofpool:sendtransfers the block’s ownership to the queue andreceiveassigns it to the receiver (§4.3). In checked builds the owner field of the block is checked and updated; release builds copy the pointer.Destroy follows ADR-020: waiters wake with
EMB_EDESTROYEDonly withEMB_OBJ_ABORT_WAITERS; items still in the ring are simply dropped (withOWNERSHIP, their blocks are returned to the pool).ISR callers use
EMB_NO_WAIT(SPEC-001 §5.2); a full queue returnsEMB_ETIMEDOUT, never drops silently (RIOT’smsg_try_senddrop is the behavior to avoid, R-001 §6).
2.3 Why copy by value¶
A copy of up to 64 bytes under the lock is cheaper and simpler than a reservation protocol, and it keeps the kernel free of application pointers that may dangle. Zero copy is the explicit, ownership-tracked path of §4, never an implicit optimization.
3. Pipe (CONFIG_EMB_PIPE)¶
3.1 Interface¶
typedef struct emb_pipe_attr {
const char *name;
uint8_t *buffer; /* caller-provided ring, `size` bytes */
size_t size;
size_t notify_trigger; /* binding fires when fill rises to >= trigger (default 1) */
uint8_t flags; /* EMB_OBJ_ABORT_WAITERS | EMB_OBJ_FIFO | EMB_PIPE_SPSC */
} emb_pipe_attr_t;
emb_status_t emb_pipe_init(emb_pipe_storage_t *storage, const emb_pipe_attr_t *attr, emb_pipe_t *out);
emb_status_t emb_pipe_destroy(emb_pipe_t p);
emb_status_t emb_pipe_write(emb_pipe_t p, const void *data, size_t len, emb_timeout_t timeout, size_t *out_written); /* @ctx thread; thread isr with EMB_NO_WAIT */
emb_status_t emb_pipe_read(emb_pipe_t p, void *buf, size_t len, size_t min_len, emb_timeout_t timeout, size_t *out_read);
emb_status_t emb_pipe_close(emb_pipe_t p); /* @ctx thread isr: no more writes; readers drain then see EMB_ESTATE */
emb_status_t emb_pipe_reset(emb_pipe_t p); /* @ctx thread: discard contents, wake waiters with EMB_ESTATE, reopen */
size_t emb_pipe_fill(emb_pipe_t p);
emb_status_t emb_pipe_bind_notify(emb_pipe_t p, emb_thread_t t, emb_notify_bits_t bit);
3.2 Semantics¶
Write copies as much as fits, then, if
lenbytes were not all written and the timeout allows, blocks on the writers’ queue until space appears, repeating until all bytes are written or the timeout expires;out_writtenis always set; a partial write on timeout returnsEMB_ETIMEDOUTwith the partial count. WithEMB_NO_WAITit writes what fits and returnsEMB_OK(orEMB_ETIMEDOUTif nothing fit).Read returns as soon as at least
min_lenbytes are available (min_len <= len,min_len >= 1), reading up tolen; otherwise it blocks untilmin_lenis available or the timeout expires (out_readholds what was read, possibly nonzero on timeout whenmin_lenwas not reached but some bytes were). Afterclose, a read returns the remaining bytes withEMB_OKand, once drained,EMB_ESTATE.Chunking. Copies run inside the object’s critical section in chunks of at most
CONFIG_EMB_PIPE_CHUNKbytes (default 32); between chunks the section is released. A write of at most one chunk is atomic with respect to other writers; longer writes may interleave with other writers at chunk boundaries. A pipe is a byte stream, not a framing mechanism: framing uses a message queue, or theEMB_PIPE_SPSCflag.EMB_PIPE_SPSC. Exactly one writer and one reader are promised by the application (checked builds record and verify the identities). The data copy then runs without the critical section, publishing the head or tail index after the copy with release semantics (FreeRTOS stream buffer design, R-001 §6); only the wake-up registration takes the critical section. This is the fast path for UART-class producers and consumers (03 §6.4).Wake-ups. A write that raises the fill past a blocked reader’s
min_lenwakes it with hand-off of the available count (the reader then copies in its own context, so no data copy is attributed to the writer); a read that frees space wakes the first blocked writer. Readers are served in wait order; a blocked reader keeps its position until satisfied or timed out.Binding fires when the fill rises from below
notify_triggerto at least it, unless a blocked reader consumed the transition.Close stops writers (
EMB_ESTATE), wakes blocked writers withEMB_ESTATE, and lets readers drain. Reset discards the contents, wakes every waiter withEMB_ESTATE, and reopens the pipe.
4. Buffer pool¶
4.1 Interface¶
typedef struct emb_pool_attr {
const char *name;
void *storage; /* block_count * block_size bytes, aligned to EMB_POOL_ALIGN */
uint16_t block_size; /* >= sizeof(void *) and a multiple of EMB_POOL_ALIGN */
uint16_t block_count;
uint8_t flags; /* EMB_OBJ_ABORT_WAITERS | EMB_OBJ_FIFO */
} emb_pool_attr_t;
#define EMB_POOL_STORAGE(name, block_size, block_count)
emb_status_t emb_pool_init(emb_pool_storage_t *storage, const emb_pool_attr_t *attr, emb_pool_t *out);
emb_status_t emb_pool_destroy(emb_pool_t p);
emb_status_t emb_pool_alloc(emb_pool_t p, emb_timeout_t timeout, void **out_block); /* @ctx thread; thread isr with EMB_NO_WAIT @time O(1) */
emb_status_t emb_pool_alloc_until(emb_pool_t p, emb_instant_t deadline, void **out_block);
emb_status_t emb_pool_free(emb_pool_t p, void *block); /* @ctx thread isr @time O(1) */
emb_status_t emb_pool_transfer(emb_pool_t p, void *block, emb_thread_t to); /* @ctx thread isr checked builds: move ownership */
uint16_t emb_pool_available(emb_pool_t p);
emb_status_t emb_pool_bind_notify(emb_pool_t p, emb_thread_t t, emb_notify_bits_t bit); /* exhausted -> available */
4.2 Semantics¶
The free list is intrusive through the first word of each free block:
allocandfreeare O(1) (KRN-IPC-007).block_sizeis at least a pointer and a multiple of the alignment;EMB_POOL_STORAGE()declares correctly sized storage.allocon an exhausted pool blocks on the pool’s wait queue;freewith a waiter hands the block to the first waiter (hand-off, no free-list traffic, no binding bit);freewithout a waiter pushes the block and, if the pool was exhausted, signals the binding.freeof a pointer outside the pool’s storage, misaligned, or already free (checked builds keep a free bitmap) is misuse (EMB_FAULT_API_ARGUMENT,EMB_EINVAL).Destroy with blocks outstanding is misuse (
EMB_EBUSY): the kernel cannot reclaim memory it does not own.
4.3 Ownership (zero copy)¶
In checked builds every block carries an owner: the allocating thread, or
IN_QUEUEwhile it travels in anEMB_MSGQ_OWNERSHIPqueue, or the thread named byemb_pool_transfer.freeandtransferby a non-owner, andsendof a block the sender does not own, fault (EMB_FAULT_API_OWNER); release builds track nothing and pay nothing (KRN-IPC-006).The rule for application code is the one 03 §6.5 states: after sending a block, the sender no longer touches it; the receiver frees it or sends it on.
emb_pool_transfercovers hand-over paths that do not go through a queue (a callback that will free the block).A thread that terminates while owning blocks (checked builds) is a lifecycle fault in the same way as a mutex owner (KRN-SYNC-025 pattern); release builds leak the block, which the statistics of §8 expose as “blocks outstanding”.
5. Ports (FUTURE)¶
A port is the cross-partition and cross-core message endpoint of 03 §6.6. It is not specified for 1.0; this section fixes only what earlier specifications already decided so that the queue API does not drift away from it:
A port presents
send,receive, timeout, wake, and binding semantics identical to a message queue (KRN-IPC-008); within one image and partition a port is a message queue.A request may carry up to
CONFIG_EMB_PORT_MAX_LEASESlease descriptors over the client’s memory; the server accesses leased memory through validating kernel calls while the client is blocked in the request; the client’s wake revokes every lease (ADR-032, KRN-IPC-009). Messages up to the copy threshold (default 64 bytes, the sameCONFIG_EMB_MSGQ_MAX_ITEM) are copied.A server may run on the client’s budget across a port (ADR-029 donation, FUTURE).
Cross-core transport: a shared-memory ring plus a doorbell interrupt (04 §8.2).
6. Choosing among them¶
Need |
Use |
|---|---|
Completion flag from an ISR |
notification (SPEC-006) |
Small fixed records, several producers or consumers |
message queue |
Records larger than 64 bytes, or a DMA destination |
buffer pool block, pointer through an ownership queue |
Byte stream from a UART-class source |
pipe, |
Several sources into one consumer |
bindings on each source, one notification wait |
Across partitions or cores |
port (FUTURE) |
7. Misuse and checked-build diagnostics¶
Condition |
Checked build |
Release build |
|---|---|---|
|
fault |
|
blocking form from an ISR without |
fault |
|
|
fault |
|
second writer or reader on an |
fault |
undefined (documented) |
write to a closed pipe; read of a closed, drained pipe |
runtime condition |
|
destroy with waiters without |
fault |
|
ring or free-list corruption detected |
fault |
undefined |
8. Observability¶
Trace events: msgq_send(q, handed_to | slot), msgq_receive(q, from_slot | handed_from), msgq_full_block(q, thread), pipe_write(p, n), pipe_read(p, n), pipe_close(p), pool_alloc(p, block, thread), pool_free(p, block), pool_transfer(p, block, to). Statistics (optional): per queue and pipe, high-water occupancy, blocked-send count; per pool, minimum availability and blocks outstanding; the debug descriptor exports the ring and free-list layout so a debugger can list a queue’s contents and a pool’s owners.
9. Tiny profile¶
The target tiny configuration makes message queues and pools available once implemented (uint8_t counts when CONFIG_EMB_IPC_SMALL=y); pipes are off by default; CONFIG_EMB_MSGQ_MAX_ITEM defaults to 16; ownership tracking exists only in checked builds and costs nothing otherwise.
10. Reference model¶
Message queues repeat the semaphore pattern on two wait queues with hand-off in both directions; they are added to tools/model when the conformance tests for them are written (M2), with the property “every sent item is received exactly once or remains in the ring” mirroring the semaphore’s unit conservation. Pools likewise (“every block is owned by exactly one party or free”). Pipes are not modeled: their correctness properties are byte conservation and ordering, which the conformance suite checks directly.
11. Decisions taken at acceptance (2026-10-07)¶
Message queues copy by value with the copy inside the critical section, bounded by
CONFIG_EMB_MSGQ_MAX_ITEM(default 64, tiny 16). Larger payloads travel as pool blocks by pointer.Direct copy into a waiting receiver and slot hand-off to a waiting sender; a woken thread never re-checks.
send_frontisEMB_NO_WAITonly.Pipes copy in chunks of
CONFIG_EMB_PIPE_CHUNK(default 32) under the critical section; writes longer than a chunk may interleave with other writers;EMB_PIPE_SPSCgives the lock-free copy path with one promised writer and reader.Pipe reads take a per-call
min_len; the binding trigger is a per-pipe attribute.A closed pipe drains then returns
EMB_ESTATE; reset wakes waiters withEMB_ESTATEand reopens.Buffer pools are intrusive O(1) free lists; ownership is tracked in checked builds only, including the
IN_QUEUEstate andemb_pool_transfer.No mailbox type: a message queue of pointers, or an ownership queue, is the mailbox.
Ports stay FUTURE; §5 fixes only the compatibility constraints already decided.
Message queues and pools join the reference model with their conformance tests in M2; pipes are not modeled.