diff --git a/components/zkernel/CMakeLists.txt b/components/zkernel/CMakeLists.txt index f6355a2..06e06b7 100644 --- a/components/zkernel/CMakeLists.txt +++ b/components/zkernel/CMakeLists.txt @@ -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" ) diff --git a/components/zkernel/include/boreas/zephyr/kernel.h b/components/zkernel/include/boreas/zephyr/kernel.h index 60b23a6..c775dbb 100644 --- a/components/zkernel/include/boreas/zephyr/kernel.h +++ b/components/zkernel/include/boreas/zephyr/kernel.h @@ -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 * ---------------------------------------------------------------- */ diff --git a/components/zkernel/src/k_mem_slab.c b/components/zkernel/src/k_mem_slab.c new file mode 100644 index 0000000..cf410e2 --- /dev/null +++ b/components/zkernel/src/k_mem_slab.c @@ -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 +#include + +#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); +} diff --git a/test/main/CMakeLists.txt b/test/main/CMakeLists.txt index b04800e..99b3301 100644 --- a/test/main/CMakeLists.txt +++ b/test/main/CMakeLists.txt @@ -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" diff --git a/test/main/test_k_mem_slab.c b/test/main/test_k_mem_slab.c new file mode 100644 index 0000000..d057c50 --- /dev/null +++ b/test/main/test_k_mem_slab.c @@ -0,0 +1,361 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 Intercreate + */ + +#include +#include + +#include "unity.h" +#include "zephyr/kernel.h" +#include "sdkconfig.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#ifdef CONFIG_K_TIMER_DISPATCH_ISR +#include "esp_attr.h" +#endif + +/* block_size must be >= sizeof(void*) and word-aligned; use a generous + * 32-byte block so payloads are easy to scribble. */ +#define BLK_SIZE 32 +#define NUM_BLOCKS 4 + +static uint8_t slab_buf[BLK_SIZE * NUM_BLOCKS] __attribute__((aligned(sizeof(void *)))); + +/* ---------------------------------------------------------------- + * init + accounting + * ---------------------------------------------------------------- */ + +static void test_mem_slab_init_accounting(void) +{ + struct k_mem_slab slab; + + TEST_ASSERT_EQUAL(0, k_mem_slab_init(&slab, slab_buf, BLK_SIZE, NUM_BLOCKS)); + TEST_ASSERT_EQUAL(0, k_mem_slab_num_used_get(&slab)); + TEST_ASSERT_EQUAL(NUM_BLOCKS, k_mem_slab_num_free_get(&slab)); + TEST_ASSERT_EQUAL(0, k_mem_slab_max_used_get(&slab)); +} + +static void test_mem_slab_init_invalid_args(void) +{ + struct k_mem_slab slab; + + /* zero block_size / zero num_blocks */ + TEST_ASSERT_EQUAL(-EINVAL, k_mem_slab_init(&slab, slab_buf, 0, NUM_BLOCKS)); + TEST_ASSERT_EQUAL(-EINVAL, k_mem_slab_init(&slab, slab_buf, BLK_SIZE, 0)); + + /* non-word-aligned block_size (the free-list next-ptr needs alignment) */ + TEST_ASSERT_EQUAL(-EINVAL, k_mem_slab_init(&slab, slab_buf, BLK_SIZE + 1, NUM_BLOCKS)); + + /* non-word-aligned buffer base */ + TEST_ASSERT_EQUAL(-EINVAL, k_mem_slab_init(&slab, slab_buf + 1, BLK_SIZE, NUM_BLOCKS)); +} + +/* ---------------------------------------------------------------- + * alloc / free round-trip, distinctness, accounting + * ---------------------------------------------------------------- */ + +static void test_mem_slab_alloc_distinct_in_bounds(void) +{ + struct k_mem_slab slab; + void *blocks[NUM_BLOCKS] = {0}; + + TEST_ASSERT_EQUAL(0, k_mem_slab_init(&slab, slab_buf, BLK_SIZE, NUM_BLOCKS)); + + for (int i = 0; i < NUM_BLOCKS; i++) { + TEST_ASSERT_EQUAL(0, k_mem_slab_alloc(&slab, &blocks[i], K_NO_WAIT)); + TEST_ASSERT_NOT_NULL(blocks[i]); + + /* word-aligned, inside the buffer */ + TEST_ASSERT_EQUAL(0, ((uintptr_t)blocks[i]) & (sizeof(void *) - 1)); + TEST_ASSERT_TRUE((uint8_t *)blocks[i] >= slab_buf); + TEST_ASSERT_TRUE((uint8_t *)blocks[i] + BLK_SIZE <= slab_buf + sizeof(slab_buf)); + + /* distinct from every earlier block */ + for (int j = 0; j < i; j++) { + TEST_ASSERT_NOT_EQUAL(blocks[j], blocks[i]); + } + /* writable full width without clobbering neighbours */ + memset(blocks[i], 0xA0 + i, BLK_SIZE); + } + + TEST_ASSERT_EQUAL(NUM_BLOCKS, k_mem_slab_num_used_get(&slab)); + TEST_ASSERT_EQUAL(0, k_mem_slab_num_free_get(&slab)); + + /* The writes did not overlap: each block still holds its own pattern. */ + for (int i = 0; i < NUM_BLOCKS; i++) { + uint8_t *b = blocks[i]; + + for (int k = 0; k < BLK_SIZE; k++) { + TEST_ASSERT_EQUAL_UINT8(0xA0 + i, b[k]); + } + } + + /* Exhausted: K_NO_WAIT alloc fails with -ENOMEM and NULLs the out-ptr. */ + void *extra = (void *)0xdead; + + TEST_ASSERT_EQUAL(-ENOMEM, k_mem_slab_alloc(&slab, &extra, K_NO_WAIT)); + TEST_ASSERT_NULL(extra); + + /* Free all, pool is whole again and re-allocatable. */ + for (int i = 0; i < NUM_BLOCKS; i++) { + k_mem_slab_free(&slab, blocks[i]); + TEST_ASSERT_EQUAL(NUM_BLOCKS - 1 - i, k_mem_slab_num_used_get(&slab)); + } + TEST_ASSERT_EQUAL(NUM_BLOCKS, k_mem_slab_num_free_get(&slab)); + + void *again; + + TEST_ASSERT_EQUAL(0, k_mem_slab_alloc(&slab, &again, K_NO_WAIT)); + TEST_ASSERT_NOT_NULL(again); + k_mem_slab_free(&slab, again); + + /* High-water mark survived the frees. */ + TEST_ASSERT_EQUAL(NUM_BLOCKS, k_mem_slab_max_used_get(&slab)); +} + +static void test_mem_slab_alloc_timeout(void) +{ + struct k_mem_slab slab; + void *blocks[NUM_BLOCKS]; + + TEST_ASSERT_EQUAL(0, k_mem_slab_init(&slab, slab_buf, BLK_SIZE, NUM_BLOCKS)); + for (int i = 0; i < NUM_BLOCKS; i++) { + TEST_ASSERT_EQUAL(0, k_mem_slab_alloc(&slab, &blocks[i], K_NO_WAIT)); + } + + /* A blocking alloc on an exhausted slab times out cleanly. */ + void *late = (void *)0xdead; + uint32_t t0 = (uint32_t)k_uptime_get(); + + TEST_ASSERT_EQUAL(-EAGAIN, k_mem_slab_alloc(&slab, &late, K_MSEC(30))); + TEST_ASSERT_NULL(late); + TEST_ASSERT_GREATER_OR_EQUAL(25, (uint32_t)k_uptime_get() - t0); + + for (int i = 0; i < NUM_BLOCKS; i++) { + k_mem_slab_free(&slab, blocks[i]); + } +} + +/* ---------------------------------------------------------------- + * K_MEM_SLAB_DEFINE -- usable without an explicit init (lazy threading) + * ---------------------------------------------------------------- */ + +K_MEM_SLAB_DEFINE(declared_slab, 16, 3, 4); + +static void test_mem_slab_define_usable(void) +{ + void *a, *b, *c, *d; + + TEST_ASSERT_EQUAL(3, k_mem_slab_num_free_get(&declared_slab)); + + TEST_ASSERT_EQUAL(0, k_mem_slab_alloc(&declared_slab, &a, K_NO_WAIT)); + TEST_ASSERT_EQUAL(0, k_mem_slab_alloc(&declared_slab, &b, K_NO_WAIT)); + TEST_ASSERT_EQUAL(0, k_mem_slab_alloc(&declared_slab, &c, K_NO_WAIT)); + TEST_ASSERT_NOT_EQUAL(a, b); + TEST_ASSERT_NOT_EQUAL(b, c); + + /* Empty now. */ + TEST_ASSERT_EQUAL(-ENOMEM, k_mem_slab_alloc(&declared_slab, &d, K_NO_WAIT)); + + /* Scribble a sentinel, free, re-alloc: upstream returns the block + * uninitialized (never zeroed), so the bytes must survive. (The + * free path overwrites only the first word with the list link.) */ + memset((uint8_t *)b + sizeof(void *), 0x5A, 16 - sizeof(void *)); + k_mem_slab_free(&declared_slab, b); + TEST_ASSERT_EQUAL(0, k_mem_slab_alloc(&declared_slab, &d, K_NO_WAIT)); + TEST_ASSERT_EQUAL(b, d); /* the just-freed block came back */ + for (size_t i = sizeof(void *); i < 16; i++) { + TEST_ASSERT_EQUAL_UINT8(0x5A, ((uint8_t *)d)[i]); /* not zeroed */ + } + + k_mem_slab_free(&declared_slab, a); + k_mem_slab_free(&declared_slab, c); + k_mem_slab_free(&declared_slab, d); +} + +/* ---------------------------------------------------------------- + * Multi-thread: a blocking alloc is woken by a free. + * ---------------------------------------------------------------- */ + +K_THREAD_STACK_DEFINE(freer_stack, 4096); +static struct k_thread freer_thread; +static struct k_mem_slab mt_slab; +static void *mt_held[NUM_BLOCKS]; + +static void freer_entry(void *p1, void *p2, void *p3) +{ + (void)p2; + (void)p3; + k_msleep(30); + /* Return one block so the blocked allocator below can proceed. */ + k_mem_slab_free((struct k_mem_slab *)p1, mt_held[0]); +} + +static void test_mem_slab_blocking_alloc_woken_by_free(void) +{ + TEST_ASSERT_EQUAL(0, k_mem_slab_init(&mt_slab, slab_buf, BLK_SIZE, NUM_BLOCKS)); + for (int i = 0; i < NUM_BLOCKS; i++) { + TEST_ASSERT_EQUAL(0, k_mem_slab_alloc(&mt_slab, &mt_held[i], K_NO_WAIT)); + } + + k_thread_create(&freer_thread, freer_stack, K_THREAD_STACK_SIZEOF(freer_stack), freer_entry, + &mt_slab, NULL, NULL, 5, 0, K_NO_WAIT); + + /* Blocks until the freer returns mt_held[0] ~30 ms out. */ + void *got = NULL; + + TEST_ASSERT_EQUAL(0, k_mem_slab_alloc(&mt_slab, &got, K_FOREVER)); + TEST_ASSERT_EQUAL(mt_held[0], got); + + TEST_ASSERT_EQUAL(0, k_thread_join(&freer_thread, K_SECONDS(2))); + + k_mem_slab_free(&mt_slab, got); + for (int i = 1; i < NUM_BLOCKS; i++) { + k_mem_slab_free(&mt_slab, mt_held[i]); + } +} + +/* ---------------------------------------------------------------- + * Multi-thread: N>blocks allocators block; frees wake them; every + * waiter gets a distinct block (units conserved, no double-alloc). + * ---------------------------------------------------------------- */ + +#define N_WAITERS 6 + +/* Boreas has no K_THREAD_STACK_ARRAY_DEFINE; K_THREAD_STACK_DEFINE is a + * StackType_t[] sized in words, so an array of those is the 2D form. */ +static StackType_t waiter_stacks[N_WAITERS][4096 / sizeof(StackType_t)]; +static struct k_thread waiter_threads[N_WAITERS]; +static void *waiter_block[N_WAITERS]; +static volatile int waiter_ret[N_WAITERS]; + +static void waiter_entry(void *p1, void *p2, void *p3) +{ + (void)p3; + int idx = (int)(intptr_t)p1; + + waiter_ret[idx] = + k_mem_slab_alloc((struct k_mem_slab *)p2, &waiter_block[idx], K_SECONDS(2)); +} + +static void test_mem_slab_multi_waiter_conservation(void) +{ + TEST_ASSERT_EQUAL(0, k_mem_slab_init(&mt_slab, slab_buf, BLK_SIZE, NUM_BLOCKS)); + + /* Drain the pool so every waiter must block. */ + void *seed[NUM_BLOCKS]; + + for (int i = 0; i < NUM_BLOCKS; i++) { + TEST_ASSERT_EQUAL(0, k_mem_slab_alloc(&mt_slab, &seed[i], K_NO_WAIT)); + } + + for (int i = 0; i < N_WAITERS; i++) { + waiter_block[i] = NULL; + waiter_ret[i] = 0xbad; + k_thread_create(&waiter_threads[i], waiter_stacks[i], + K_THREAD_STACK_SIZEOF(waiter_stacks[i]), waiter_entry, + (void *)(intptr_t)i, &mt_slab, NULL, 5, 0, K_NO_WAIT); + } + k_msleep(20); /* all six parked in k_mem_slab_alloc */ + + /* Release the 4 seed blocks; exactly 4 of the 6 waiters should win, + * each with a distinct block, the other 2 time out. */ + for (int i = 0; i < NUM_BLOCKS; i++) { + k_mem_slab_free(&mt_slab, seed[i]); + } + + for (int i = 0; i < N_WAITERS; i++) { + TEST_ASSERT_EQUAL(0, k_thread_join(&waiter_threads[i], K_SECONDS(3))); + } + + int winners = 0; + + for (int i = 0; i < N_WAITERS; i++) { + if (waiter_ret[i] == 0) { + winners++; + TEST_ASSERT_NOT_NULL(waiter_block[i]); + /* distinct from every other winner */ + for (int j = 0; j < N_WAITERS; j++) { + if (j != i && waiter_ret[j] == 0) { + TEST_ASSERT_NOT_EQUAL(waiter_block[j], waiter_block[i]); + } + } + } else { + TEST_ASSERT_EQUAL(-EAGAIN, waiter_ret[i]); + TEST_ASSERT_NULL(waiter_block[i]); + } + } + TEST_ASSERT_EQUAL(NUM_BLOCKS, winners); + TEST_ASSERT_EQUAL(NUM_BLOCKS, k_mem_slab_num_used_get(&mt_slab)); + + /* Hand the won blocks back. */ + for (int i = 0; i < N_WAITERS; i++) { + if (waiter_ret[i] == 0) { + k_mem_slab_free(&mt_slab, waiter_block[i]); + } + } +} + +/* ---------------------------------------------------------------- + * FromISR free wakes a blocked allocator (HW only; prior art: the + * CONFIG_K_TIMER_DISPATCH_ISR tests in test_k_timer.c). + * ---------------------------------------------------------------- */ + +#ifdef CONFIG_K_TIMER_DISPATCH_ISR + +static struct k_mem_slab isr_slab; +static void *isr_held[NUM_BLOCKS]; +static volatile bool isr_free_was_isr; + +static void IRAM_ATTR slab_isr_free_cb(struct k_timer *timer) +{ + ARG_UNUSED(timer); + isr_free_was_isr = xPortInIsrContext(); + k_mem_slab_free(&isr_slab, isr_held[0]); +} + +static void test_mem_slab_isr_free_wakes_waiter(void) +{ + struct k_timer timer; + + TEST_ASSERT_EQUAL(0, k_mem_slab_init(&isr_slab, slab_buf, BLK_SIZE, NUM_BLOCKS)); + for (int i = 0; i < NUM_BLOCKS; i++) { + TEST_ASSERT_EQUAL(0, k_mem_slab_alloc(&isr_slab, &isr_held[i], K_NO_WAIT)); + } + isr_free_was_isr = false; + + k_timer_init(&timer, slab_isr_free_cb, NULL); + k_timer_start(&timer, K_MSEC(10), K_NO_WAIT); + + void *got = NULL; + + TEST_ASSERT_EQUAL(0, k_mem_slab_alloc(&isr_slab, &got, K_SECONDS(1))); + TEST_ASSERT_EQUAL(isr_held[0], got); + + k_timer_stop(&timer); + TEST_ASSERT_TRUE_MESSAGE(isr_free_was_isr, "free did not run in ISR context"); + + k_mem_slab_free(&isr_slab, got); + for (int i = 1; i < NUM_BLOCKS; i++) { + k_mem_slab_free(&isr_slab, isr_held[i]); + } +} + +#endif /* CONFIG_K_TIMER_DISPATCH_ISR */ + +void test_k_mem_slab_group(void) +{ + RUN_TEST(test_mem_slab_init_accounting); + RUN_TEST(test_mem_slab_init_invalid_args); + RUN_TEST(test_mem_slab_alloc_distinct_in_bounds); + RUN_TEST(test_mem_slab_alloc_timeout); + RUN_TEST(test_mem_slab_define_usable); + RUN_TEST(test_mem_slab_blocking_alloc_woken_by_free); + RUN_TEST(test_mem_slab_multi_waiter_conservation); +#ifdef CONFIG_K_TIMER_DISPATCH_ISR + RUN_TEST(test_mem_slab_isr_free_wakes_waiter); +#endif +} diff --git a/test/main/test_main.c b/test/main/test_main.c index ab5e7ab..e0ffe03 100644 --- a/test/main/test_main.c +++ b/test/main/test_main.c @@ -17,6 +17,7 @@ void test_byteorder_group(void); void test_atomic_group(void); void test_ring_buf_group(void); void test_k_sem_group(void); +void test_k_mem_slab_group(void); void test_k_mutex_group(void); void test_k_msgq_group(void); void test_k_event_group(void); @@ -58,6 +59,7 @@ void app_main(void) /* Layer 1: Kernel primitives */ test_k_sem_group(); + test_k_mem_slab_group(); test_k_mutex_group(); test_k_msgq_group(); test_k_event_group();