Zephyr Memory Slabs: Size and Align Fixed Blocks for Predictable IPC Ownership

A memory slab becomes predictable when both its geometry and its outstanding-block budget are explicit. Verify the block stride, alignment and backing-buffer extent before counting free blocks. Then account for every allocated block, including blocks under construction, queued, being processed and retained for retry. A fixed-size allocator removes variable-size fragmentation within that slab; it does not prevent exhaustion, double release or misuse of a block’s storage.

This guide covers upstream Zephyr 3.7.0 k_mem_slab for ordinary CPU-accessed IPC records. It does not establish DMA accessibility, cache maintenance or peripheral alignment. Those requirements need a separate board-specific check. The arithmetic example is hypothetical, and the qualification steps have not been executed on Zephyr hardware.

Define geometry from the actual record

Start with the complete record size, including application headers, sequence identifiers and any IPC container fields. Do not size only the useful payload. Choose an alignment that satisfies the record type and the slab’s pointer-sized linkage. In the tagged implementation, free blocks store a pointer at their beginning, and free-list creation checks pointer-word alignment of both the buffer address and block size.

A practical design calculation is: choose a power-of-two alignment A that is at least sizeof(void *) and the record alignment for the supported target ABI; round the required record size upward to a multiple of A to obtain stride B; reserve at least B times N bytes for N nonzero blocks. Require B to be nonzero, at least sizeof(void *), and a multiple of both the pointer-word size and application alignment. The implementation uses a sizeof(void *) - 1 mask, rather than merely the C pointer-type alignment. Check multiplication and rounding for overflow before using runtime-provided sizes. This is an application validation procedure, not a claim that the kernel checks every malformed geometry.

The 3.7.0 slab declarations require each block to preserve the requested alignment. Their static macros also apply word rounding internally. Do not rely on that rounding to satisfy a larger application alignment automatically: explicitly make B a multiple of A. A buffer aligned to 32 bytes with a 40-byte stride does not make every block 32-byte aligned.

Slab verification has two independent checks: aligned base and stride define valid block addresses, while a block ledger sums producer, queue, consumer and retained ownership against total capacity.
Geometry proves where a valid block starts. The ownership ledger proves whether enough distinct blocks exist for the allowed overlap.

Check the instantiated object, not just the macro call

  1. Record target pointer width, record size, record alignment, requested stride, block count and linker section. Host values are not substitutes for the target ABI.
  2. Inspect the map or debugger to confirm the backing buffer’s address and extent. Verify every computed block start respects A and lies completely inside that extent.
  3. For dynamic initialization, validate arguments before calling and check the initialization result. Never continue with an object whose initialization failed.
  4. For a static definition, verify the resulting storage dimensions and build configuration. Do not copy a four-byte-alignment example into a target where pointer linkage needs more.
  5. Keep initialization separate from live operation. Reinitializing a slab while blocks are outstanding destroys the ownership accounting.

The versioned slab documentation explains fixed blocks and allocation waits. Some introductory wording uses four-byte examples; the target-aware API description and implementation are the better basis for a portable geometry audit. There is no useful universal statement that every block is merely “word aligned” without identifying that word size.

Budget outstanding blocks by mutually exclusive states

For a single slab, write a conservation equation: total blocks equal free plus producer-owned plus queued plus consumer-owned plus deliberately retained. A block appears in exactly one category at a time. If two consumers share references to one block, that still represents one allocated block, but release authority needs its own protocol. Counting references as allocations overstates capacity; treating each reference as independently releasable corrupts it.

Use maximum simultaneous occupancy of the states, rather than adding unrelated historical peaks blindly. If their maxima can coincide under the allowed schedule, provision for that combination. If the design relies on them being mutually exclusive, record the mechanism that enforces the exclusion. Queue depth alone omits records already removed by consumers and records not yet submitted by producers.

A hypothetical record needs 37 bytes and eight-byte alignment. Choosing a 40-byte stride and 12 blocks requires 480 bytes of backing storage, excluding the slab control object and other IPC objects. A proposed overlap budget is two producer blocks, six queued, two consumer blocks, one retained retry and one reserve. The reserve exists only if admission policy leaves it unused by ordinary traffic; naming a block “reserve” does not isolate it.

Treat allocation failure as a state transition

Process the allocation return value before using its output pointer. A no-wait exhausted allocation reports -ENOMEM; a finite wait that expires reports -EAGAIN. ISR calls must use K_NO_WAIT. With multithreading disabled, the documented API treats timeouts as no-wait. Select drop, retry or backpressure behavior deliberately, and avoid blocking while holding a resource needed by the eventual releaser.

A released block can pass directly to a waiting allocator in the 3.7.0 implementation. Therefore, observing a free count of zero after release does not prove a leak. Another owner may already hold that same block. Similarly, a sampled free count does not reserve capacity for the next allocation. Use successful allocation as the authority to write.

Qualify corruption and exhaustion separately

First allocate all N blocks without consumers, verify distinct valid addresses, and confirm the next no-wait allocation fails. Release one block, acquire again, then release all owned blocks exactly once. Repeat with a waiting allocator to observe transfer rather than expecting a visible free interval. Add producer failure and queue-submission failure paths to the ledger.

Track address, owner state and generation in a test-side table. Reject interior pointers, wrong-slab pointers and repeated frees in the application’s debug checks before calling the kernel. Kernel assertions are useful diagnostics, not a comprehensive ownership validator. Freeing storage permits the slab to overwrite its leading bytes with linkage; old payload contents and zero initialization are not guaranteed.

Finally, compare the allocation ledger with runtime statistics during a controlled drain. High-water usage, where enabled, helps characterize a tested workload but does not prove every future overlap fits. Close the review with both artifacts: a target-verified geometry sheet and an exhaustion policy that still makes progress when no block is available.

Similar Posts