Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions components/zkernel/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ set(srcs
"src/k_thread.c"
"src/k_work.c"
"src/ring_buf.c"
"src/k_mem_slab.c"
"src/sys_init.c"
"src/fatal.c"
)
Expand Down
137 changes: 137 additions & 0 deletions components/zkernel/include/boreas/zephyr/kernel.h
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,143 @@ void k_msgq_purge(struct k_msgq *msgq);
uint32_t k_msgq_num_used_get(struct k_msgq *msgq);
uint32_t k_msgq_num_free_get(struct k_msgq *msgq);

/* ----------------------------------------------------------------
* Memory Slab (fixed-size block allocator)
* ---------------------------------------------------------------- */

/* Free blocks are threaded through an intrusive singly-linked list whose
* next-pointer lives in each free block's first word (upstream scheme),
* so block_size must be >= sizeof(void *) and word-aligned. The block
* count and blocking ride the embedded counting semaphore (count = free
* blocks); the lock guards the free list and the usage counters. A
* K_MEM_SLAB_DEFINE'd slab threads its free list lazily on first use
* (upstream threads it from a PRE_KERNEL SYS_INIT, which does not run on
* the linux target). */
struct k_mem_slab {
uint8_t *buffer;
char *free_list; /* intrusive: next-ptr in each free block's first word */
size_t block_size; /* word-aligned */
uint32_t num_blocks;
uint32_t num_used;
uint32_t max_used; /* high-water mark (always tracked) */
struct k_sem avail; /* counts free blocks; blocks alloc when empty */
portMUX_TYPE lock; /* guards free_list, num_used, max_used, threaded */
bool threaded; /* lazy free-list init done (K_MEM_SLAB_DEFINE) */
};

/* Round up to a pointer-word boundary -- the free-list next pointer
* lives in each block's first word, so block_size and buffer alignment
* must be at least sizeof(void *). Matches upstream's WB_UP, so the
* same K_MEM_SLAB_DEFINE compiles on 32- and 64-bit targets (the linux
* test host) without the caller minding the word size. */
#define Z_MEM_SLAB_WB_UP(x) ROUND_UP((x), sizeof(void *))

/* The embedded sem is compile-time-initialized (count = num_blocks),
* exactly like K_SEM_DEFINE / K_TIMER_DEFINE's embedded sem -- so the
* only thing left to do lazily on first use is thread the free list
* (pure pointer work, IRAM-safe). k_sem_init never runs on a hot or ISR
* path for a DEFINE'd slab. */
#define Z_MEM_SLAB_INITIALIZER(name, _block_size, _num_blocks) \
{ \
.buffer = _k_mem_slab_buf_##name, \
.free_list = NULL, \
.block_size = Z_MEM_SLAB_WB_UP(_block_size), \
.num_blocks = (_num_blocks), \
.num_used = 0, \
.max_used = 0, \
.avail = Z_SEM_INITIALIZER(name.avail, _num_blocks, _num_blocks), \
.lock = portMUX_INITIALIZER_UNLOCKED, \
.threaded = false, \
}

#define Z_MEM_SLAB_BUF_DEFINE(name, _block_size, _num_blocks, _align) \
BUILD_ASSERT((_num_blocks) >= 1, "num_blocks must be >= 1"); \
BUILD_ASSERT(((_align) & ((_align) - 1)) == 0, "align must be a power of 2"); \
BUILD_ASSERT(((_block_size) % (_align)) == 0, "block_size must be a multiple of align"); \
static uint8_t __attribute__((aligned(Z_MEM_SLAB_WB_UP( \
_align)))) _k_mem_slab_buf_##name[(_num_blocks) * Z_MEM_SLAB_WB_UP(_block_size)]

/**
* Statically define and initialize a memory slab. Usable without an
* explicit k_mem_slab_init() -- the free list is threaded on first
* alloc/free (the embedded sem is initialized at compile time, so this
* lazy step is just IRAM-safe pointer work and is safe from any
* context, including an ISR). @p _block_size and the buffer alignment
* are rounded up to a pointer-word boundary (upstream WB_UP), so the
* stored block size may exceed @p _block_size.
*
* @param name Name of the slab.
* @param _block_size Size of each block in bytes (multiple of @p _align).
* @param _num_blocks Number of blocks (>= 1).
* @param _align Block/buffer alignment (power of 2).
*/
#define K_MEM_SLAB_DEFINE(name, _block_size, _num_blocks, _align) \
Z_MEM_SLAB_BUF_DEFINE(name, _block_size, _num_blocks, _align); \
struct k_mem_slab name = Z_MEM_SLAB_INITIALIZER(name, _block_size, _num_blocks)

/** As K_MEM_SLAB_DEFINE, but file-local (adds `static`). */
#define K_MEM_SLAB_DEFINE_STATIC(name, _block_size, _num_blocks, _align) \
Z_MEM_SLAB_BUF_DEFINE(name, _block_size, _num_blocks, _align); \
static struct k_mem_slab name = Z_MEM_SLAB_INITIALIZER(name, _block_size, _num_blocks)

/**
* Initialize a memory slab over a caller-provided buffer.
*
* @param slab Slab to initialize.
* @param buffer Backing storage, @p block_size * @p num_blocks bytes,
* word-aligned.
* @param block_size Size of each block (word-aligned, >= sizeof(void *)).
* @param num_blocks Number of blocks (>= 1).
*
* @retval 0 on success.
* @retval -EINVAL if @p block_size or @p buffer is not word-aligned,
* @p block_size or @p num_blocks is zero, or the size math
* overflows.
*/
int k_mem_slab_init(struct k_mem_slab *slab, void *buffer, size_t block_size, uint32_t num_blocks);

/**
* Allocate a block.
*
* @param slab Slab to allocate from.
* @param mem Set to the block address on success, NULL otherwise.
* @param timeout Wait period if no block is free.
*
* @retval 0 on success (@p mem is uninitialized memory -- not zeroed).
* @retval -ENOMEM if K_NO_WAIT and no block was free.
* @retval -EAGAIN if the timeout expired before a block became free.
* @retval -EINVAL if a DEFINE'd slab's parameters are invalid (caught on
* the first-use lazy init).
*
* @note ISR context: legal only with K_NO_WAIT (upstream contract).
* @note A thread blocked here must NOT be aborted (see k_sem_take).
*/
int k_mem_slab_alloc(struct k_mem_slab *slab, void **mem, k_timeout_t timeout);

/**
* Free a previously allocated block. ISR-safe. Wakes the highest-priority
* thread waiting in k_mem_slab_alloc, which receives this block.
*/
void k_mem_slab_free(struct k_mem_slab *slab, void *mem);

/** Number of blocks currently allocated. */
static inline uint32_t k_mem_slab_num_used_get(struct k_mem_slab *slab)
{
return slab->num_used;
}

/** Number of blocks currently free. */
static inline uint32_t k_mem_slab_num_free_get(struct k_mem_slab *slab)
{
return slab->num_blocks - slab->num_used;
}

/** Maximum number of blocks ever simultaneously allocated (high-water mark). */
static inline uint32_t k_mem_slab_max_used_get(struct k_mem_slab *slab)
{
return slab->max_used;
}

/* ----------------------------------------------------------------
* Event
* ---------------------------------------------------------------- */
Expand Down
168 changes: 168 additions & 0 deletions components/zkernel/src/k_mem_slab.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright 2026 Intercreate
*
* Fixed-size block allocator (k_mem_slab). Independent reimplementation
* of upstream Zephyr's API over the Boreas substrate: the free-block
* count and blocking ride the owned, notification-backed k_sem (count =
* free blocks), and a portMUX guards the intrusive free list and the
* usage counters. Reusing k_sem inherits its hardened wake protocol --
* a give targets the highest-priority waiter without bumping the count,
* so a freed block is reserved for the woken allocator (a racing
* K_NO_WAIT alloc sees no credit and returns -ENOMEM), matching
* upstream's direct hand-off semantics.
*
* Free-list scheme (upstream): blocks are threaded through an intrusive
* singly-linked list whose next-pointer lives in each free block's first
* word; alloc returns the raw (uninitialized) block. block_size must be
* >= sizeof(void *) and word-aligned.
*/

#include "zephyr/kernel.h"

#include <errno.h>
#include <stdint.h>

#include "sdkconfig.h"

#include "esp_attr.h"

#include "zkernel_internal.h"

/* Thread the free list back-to-front (ascending block order), validating
* the upstream alignment/overflow constraints. Caller serializes. */
static int K_ISR_SAFE z_mem_slab_create_free_list(struct k_mem_slab *slab)
{
if (slab->block_size == 0U || slab->num_blocks == 0U) {
return -EINVAL;
}
/* block_size and buffer base must both be word-aligned (the first
* word of each block holds the free-list next pointer). */
if (((slab->block_size | (uintptr_t)slab->buffer) & (sizeof(void *) - 1U)) != 0U) {
return -EINVAL;
}

size_t total;

if (__builtin_mul_overflow(slab->block_size, (size_t)slab->num_blocks, &total)) {
return -EINVAL;
}
uintptr_t end;

if (__builtin_add_overflow((uintptr_t)slab->buffer, (uintptr_t)total, &end)) {
return -EINVAL;
}

slab->free_list = NULL;
char *p = (char *)slab->buffer + (total - slab->block_size);

for (uint32_t i = 0; i < slab->num_blocks; i++) {
*(char **)p = slab->free_list;
slab->free_list = p;
p -= slab->block_size;
}
return 0;
}

int k_mem_slab_init(struct k_mem_slab *slab, void *buffer, size_t block_size, uint32_t num_blocks)
{
slab->buffer = buffer;
slab->block_size = block_size;
slab->num_blocks = num_blocks;
slab->num_used = 0;
slab->max_used = 0;
slab->lock = (portMUX_TYPE)portMUX_INITIALIZER_UNLOCKED;

int ret = z_mem_slab_create_free_list(slab);

if (ret != 0) {
return ret;
}

/* num_blocks >= 1 is guaranteed by create_free_list above. */
ret = k_sem_init(&slab->avail, num_blocks, num_blocks);
if (ret != 0) {
return ret;
}

__atomic_store_n(&slab->threaded, true, __ATOMIC_RELEASE);
return 0;
}

/* Thread the free list of a K_MEM_SLAB_DEFINE'd slab on first use, once,
* under the slab's (statically-initialized) lock. The embedded sem is
* already compile-time-initialized, so nothing here calls k_sem_init --
* this is pure pointer work and therefore IRAM/ISR-safe. A slab created
* via k_mem_slab_init() has threaded == true already and skips this. */
static int K_ISR_SAFE z_mem_slab_ensure_threaded(struct k_mem_slab *slab)
{
if (__atomic_load_n(&slab->threaded, __ATOMIC_ACQUIRE)) {
return 0;
}

int ret = 0;

z_kernel_lock(&slab->lock);
if (!__atomic_load_n(&slab->threaded, __ATOMIC_ACQUIRE)) {
ret = z_mem_slab_create_free_list(slab);
if (ret == 0) {
__atomic_store_n(&slab->threaded, true, __ATOMIC_RELEASE);
}
}
z_kernel_unlock(&slab->lock);
return ret;
}

int K_ISR_SAFE k_mem_slab_alloc(struct k_mem_slab *slab, void **mem, k_timeout_t timeout)
{
int ret = z_mem_slab_ensure_threaded(slab);

if (ret != 0) {
*mem = NULL;
return ret; /* -EINVAL: bad DEFINE parameters */
}

/* The sem count IS the free-block count; taking a credit reserves
* exactly one block, which the locked pop below always finds. */
ret = k_sem_take(&slab->avail, timeout);
if (ret != 0) {
*mem = NULL;
/* k_sem_take: -EBUSY (K_NO_WAIT, none free) -> -ENOMEM;
* -EAGAIN (timeout) propagates unchanged. */
return (ret == -EBUSY) ? -ENOMEM : ret;
}

z_kernel_lock(&slab->lock);
char *block = slab->free_list;

slab->free_list = *(char **)block;
slab->num_used++;
if (slab->num_used > slab->max_used) {
slab->max_used = slab->num_used;
}
z_kernel_unlock(&slab->lock);

*mem = block;
return 0;
}

void K_ISR_SAFE k_mem_slab_free(struct k_mem_slab *slab, void *mem)
{
/* Defensive: a correctly-used slab is always threaded by the time a
* caller holds a block to free, but a DEFINE'd slab freed before any
* alloc (caller error) would otherwise push onto an unthreaded list. */
(void)z_mem_slab_ensure_threaded(slab);

z_kernel_lock(&slab->lock);
*(char **)mem = slab->free_list;
slab->free_list = (char *)mem;
/* num_used is re-incremented by the woken allocator in the hand-off
* case, so it nets unchanged; the brief dip is a relaxed-stats
* window only (the sem count, not num_used, gates allocation). */
slab->num_used--;
z_kernel_unlock(&slab->lock);

/* Returns the credit. With a waiter pending, k_sem_give targets it
* (count stays 0), reserving the just-freed block for that waiter. */
k_sem_give(&slab->avail);
}
1 change: 1 addition & 0 deletions test/main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ set(test_srcs
"test_byteorder.c"
"test_atomic.c"
"test_ring_buf.c"
"test_k_mem_slab.c"
"test_k_sem.c"
"test_k_mutex.c"
"test_k_msgq.c"
Expand Down
Loading
Loading