简洁低消耗字节内存池设计

静态字节内存池

这里记录一个嵌入式固定字节池设计:将函数局部的大型临时数组迁移到静态 BSS,按容量类别管理固定槽位,并在真实数据区尾部放置 Magic,用于辅助发现顺序写越界。它只管理通用字节存储,不绑定上层对象结构。

一个较大的局部数组,再叠加格式化、日志和网络调用链,很容易把任务栈推到边界。字节池解决的是这部分大型临时数组的栈占用,不代表所有栈问题都会随之消失。

概览

                      byte_pool_init()
                              │
                              ▼
┌──────────────────────────────────────────────────────────────┐
│                       fixed byte pool                        │
│                                                              │
│  small class         medium class         large class         │
│  bitmap              bitmap               bitmap              │
│    │                   │                    │                 │
│    ▼                   ▼                    ▼                 │
│  slot × N            slot × N             slot × N            │
│  [data][magic]       [data][magic]        [data][magic]       │
└───────────┬───────────────────┬───────────────────┬──────────┘
            │                   │                   │
    acquire(class, span)    check(span)       release(span)
            │                   │                   │
   锁内占位、锁外清零      校验位图与 Magic      锁内归还槽位
            │                                       │
            └──────────── slot 数据独占 ────────────┘

                     byte_pool_deinit()
             引用归零后检查残留占用和 Magic

函数行为:

函数 行为 失败条件
byte_pool_init() 首次调用时初始化位图与 Magic;后续调用增加引用计数 引用计数溢出
byte_pool_acquire(class, &span) 锁内选择并占用槽位,锁外清零,返回视图 类型无效、池满或 Magic 异常
byte_pool_check(&span) 根据 slot 定位真实槽位,核对地址、容量、位图和 Magic 视图无效、状态不一致或尾部损坏
byte_pool_release(&span) 锁内执行 USED → FREE,成功后清空调用方视图 重复释放、视图无效或 Magic 损坏
byte_pool_deinit() 引用归零后检查是否仍有占用或损坏 生命周期泄漏或 Magic 异常

它解决什么

改造前:任务栈
┌────────────────────────────────────────┐
│ 调用帧 │ 大型局部数组 │ 格式化/日志/网络调用帧 │
└────────────────────────────────────────┘

改造后:
任务栈:┌──────────────────────────┐
        │ 调用帧 │ byte_span_t 视图 │
        └──────────────────────────┘

静态 BSS:┌──────────────────────────────┐
          │ 固定槽位数组:[data][magic] │
          └──────────────────────────────┘

这项改造只迁移大型数组。格式化函数、日志、网络栈等嵌套调用仍会消耗任务栈,需要结合 stack high-water mark、崩溃寄存器和 map/ELF 单独分析。

对外接口

调用者只指定容量类别,不传任意大小。capacity 由池填写,是调用者进行长度检查的依据;slot 已经是归还和诊断所需的稳定标识,不再公开重复的 id

#ifndef BYTE_POOL_H
#define BYTE_POOL_H

#include <stdbool.h>
#include <stdint.h>

typedef enum {
    BYTE_POOL_SMALL = 0,
    BYTE_POOL_MEDIUM,
    BYTE_POOL_LARGE,
    BYTE_POOL_CLASS_COUNT
} byte_pool_class_t;

typedef struct {
    uint8_t *data;
    uint16_t capacity;
    uint16_t slot;
} byte_span_t;

bool byte_pool_init(void);
bool byte_pool_deinit(void);

bool byte_pool_acquire(byte_pool_class_t class_id, byte_span_t *span);
bool byte_pool_check(const byte_span_t *span);
bool byte_pool_release(byte_span_t *span);

static inline bool byte_span_valid(const byte_span_t *span)
{
    return span != NULL && span->data != NULL;
}

#endif

不同类别的 slot 可以各自从零开始,但这样需要在 span 中额外携带类别。本文改用全局唯一的槽位编号,由内部映射还原类别和局部索引,因此公开视图不需要 classid 字段。

容量规划

类别可以按用途与容量组合划分,但对外仍只暴露通用枚举。每个类别应根据两项数据配置:

  • 单次处理的最大报文长度;
  • 该类缓冲区的真实并发数量。
BSS 占用 ≈ Σ [slot_count × (capacity + sizeof(magic))]

如果所有用途都使用最大容量槽,少量大槽也可能迅速吃满目标内存区。按类别分别设置容量和数量,可以把上限固化在编译期,也方便通过 map 文件审查实际占用。

具体数值应来自报文上限和并发分析,不应直接照搬其他工程。

运行期不使用 malloc/free。槽位数量很少,位图扫描范围固定,申请和释放的成本可预测。

Magic 紧贴真实数据尾部

正确:真实池槽

低地址                                              高地址
┌──────────────────────────────────────┬──────────────┐
│                 data                 │    magic     │
│              capacity 字节           │  uint32_t    │
└──────────────────────────────────────┴──────────────┘
                                       ▲
                              写过 data 尾部会先破坏它


错误:在 span 末尾放 Magic

┌─────────────── 真实数据区 ───────────────┐       ┌──────── span ────────┐
│                  data                    │       │ data 指针 │ magic    │
└──────────────────────────────────────────┘       └───────────┴──────────┘
                 两者并不相邻 ────────────────────────────┘

槽位必须定义为 [data][magic]

typedef struct {
    uint8_t data[BYTE_POOL_SMALL_SIZE];
    volatile uint32_t magic;
} byte_pool_small_slot_t;

_Static_assert(
    offsetof(byte_pool_small_slot_t, magic) == BYTE_POOL_SMALL_SIZE,
    "magic must immediately follow data"
);

_Static_assert(
    sizeof(byte_pool_small_slot_t) ==
        BYTE_POOL_SMALL_SIZE + sizeof(uint32_t),
    "unexpected slot padding"
);

Magic 放在 data 前面只能检测头部附近的破坏,不能作为尾部顺序越界的第一道哨兵。

位图与 Magic

#define BYTE_POOL_MAGIC_FREE  UINT32_C(0x5A31C7E9)
#define BYTE_POOL_MAGIC_USED  UINT32_C(0xA6D8421F)
FREE                         USED
bitmap = 0                   bitmap = 1
magic  = FREE                magic  = USED
   │                            ▲
   └────── acquire ─────────────┘
          同一临界区内切换

   ▲                            │
   └────── release ─────────────┘
          同一临界区内切换

位图用于快速分配,尾部 Magic 用于状态一致性和越界诊断。两者职责不同,不能互相替代。

锁与 slot 所有权

acquire
  锁内:扫描空闲位 → bitmap 占位 → FREE 改为 USED
  锁外:清零整个 data → 返回 span

使用期间
  slot 由调用者独占,读写 data 不持池锁

release
  锁内:校验 span/bitmap/magic → USED 改为 FREE → bitmap 清位
  锁外:清空调用方 span

不需要为每个槽位配置 mutex。per-slot mutex 无法消除“寻找空闲槽”时对共享分配状态的竞争,还会增加 RAM、句柄和调度开销。

正确的职责划分是:全局临界区保护分配状态,slot 提供独占的数据所有权。临界区不覆盖 memset、格式化、网络收发等重操作。

为什么在 acquire 后清零

成功占用槽位后,先退出临界区,再执行:

memset(span->data, 0, span->capacity);

此时槽位已经是 USED,不会被另一个任务申请;清零又不占用全局临界区。这样可以提供明确的“申请后全零”契约,并消除上一个使用者留下的数据。

不要在 release 中先标记 FREE 再锁外清零:

任务 A:标记 FREE ─────── 清零同一槽位
                         ▲
任务 B:        acquire ─┘  此时发生同槽竞争

如果必须在释放时擦除敏感数据,需要增加 CLEARING 状态:

USED ──锁内──▶ CLEARING ──锁外清零──▶ CLEARING ──锁内──▶ FREE

普通临时缓冲区通常不需要这层复杂度。

参考实现

下面的容量和槽位数量仅用于展示结构。接入时应根据实际报文上限、并发数和内存预算重新配置。临界区宏由平台适配层提供。

展开完整参考实现
#include "byte_pool.h"

#include <stddef.h>
#include <stdint.h>
#include <string.h>

#define BYTE_POOL_SMALL_SIZE     512u
#define BYTE_POOL_MEDIUM_SIZE   1024u
#define BYTE_POOL_LARGE_SIZE    2048u

#define BYTE_POOL_SMALL_COUNT      3u
#define BYTE_POOL_MEDIUM_COUNT     2u
#define BYTE_POOL_LARGE_COUNT      1u

#define BYTE_POOL_MAGIC_FREE  UINT32_C(0x5A31C7E9)
#define BYTE_POOL_MAGIC_USED  UINT32_C(0xA6D8421F)

/* 由平台适配层提供。 */
#define BYTE_POOL_LOCK()    platform_enter_critical()
#define BYTE_POOL_UNLOCK()  platform_exit_critical()

typedef struct {
    uint8_t data[BYTE_POOL_SMALL_SIZE];
    volatile uint32_t magic;
} small_slot_t;

typedef struct {
    uint8_t data[BYTE_POOL_MEDIUM_SIZE];
    volatile uint32_t magic;
} medium_slot_t;

typedef struct {
    uint8_t data[BYTE_POOL_LARGE_SIZE];
    volatile uint32_t magic;
} large_slot_t;

#define ASSERT_SLOT_LAYOUT(type, size)                              \
    _Static_assert(offsetof(type, magic) == (size),                 \
                   "magic must immediately follow data");         \
    _Static_assert(sizeof(type) == (size) + sizeof(uint32_t),      \
                   "unexpected slot padding")

ASSERT_SLOT_LAYOUT(small_slot_t, BYTE_POOL_SMALL_SIZE);
ASSERT_SLOT_LAYOUT(medium_slot_t, BYTE_POOL_MEDIUM_SIZE);
ASSERT_SLOT_LAYOUT(large_slot_t, BYTE_POOL_LARGE_SIZE);

_Static_assert(BYTE_POOL_SMALL_COUNT <= 32u, "bitmap overflow");
_Static_assert(BYTE_POOL_MEDIUM_COUNT <= 32u, "bitmap overflow");
_Static_assert(BYTE_POOL_LARGE_COUNT <= 32u, "bitmap overflow");
_Static_assert(BYTE_POOL_LARGE_SIZE <= UINT16_MAX,
               "capacity does not fit byte_span_t");

enum {
    SMALL_SLOT_BASE = 0,
    MEDIUM_SLOT_BASE = SMALL_SLOT_BASE + BYTE_POOL_SMALL_COUNT,
    LARGE_SLOT_BASE = MEDIUM_SLOT_BASE + BYTE_POOL_MEDIUM_COUNT,
    BYTE_POOL_SLOT_COUNT = LARGE_SLOT_BASE + BYTE_POOL_LARGE_COUNT
};

/* 大块缓冲区未显式初始化,位于静态 BSS。 */
static small_slot_t s_small_slots[BYTE_POOL_SMALL_COUNT];
static medium_slot_t s_medium_slots[BYTE_POOL_MEDIUM_COUNT];
static large_slot_t s_large_slots[BYTE_POOL_LARGE_COUNT];

static uint32_t s_used_bitmap[BYTE_POOL_CLASS_COUNT];
static uint16_t s_init_ref_count;
static bool s_initialized;

typedef struct {
    uint8_t *data;
    uint16_t capacity;
    volatile uint32_t *magic;
    byte_pool_class_t class_id;
    uint16_t local_slot;
} slot_ref_t;

static uint16_t pool_class_count(byte_pool_class_t class_id)
{
    static const uint16_t counts[BYTE_POOL_CLASS_COUNT] = {
        BYTE_POOL_SMALL_COUNT,
        BYTE_POOL_MEDIUM_COUNT,
        BYTE_POOL_LARGE_COUNT
    };

    return class_id >= BYTE_POOL_SMALL &&
           class_id < BYTE_POOL_CLASS_COUNT
               ? counts[class_id]
               : 0u;
}

static uint16_t pool_slot_id(byte_pool_class_t class_id,
                             uint16_t local_slot)
{
    static const uint16_t bases[BYTE_POOL_CLASS_COUNT] = {
        SMALL_SLOT_BASE,
        MEDIUM_SLOT_BASE,
        LARGE_SLOT_BASE
    };

    return (uint16_t)(bases[class_id] + local_slot);
}

static bool pool_get_slot(uint16_t slot, slot_ref_t *ref)
{
    if (ref == NULL || slot >= BYTE_POOL_SLOT_COUNT) {
        return false;
    }

    if (slot < MEDIUM_SLOT_BASE) {
        ref->class_id = BYTE_POOL_SMALL;
        ref->local_slot = (uint16_t)(slot - SMALL_SLOT_BASE);
        ref->data = s_small_slots[ref->local_slot].data;
        ref->capacity = BYTE_POOL_SMALL_SIZE;
        ref->magic = &s_small_slots[ref->local_slot].magic;
        return true;
    }

    if (slot < LARGE_SLOT_BASE) {
        ref->class_id = BYTE_POOL_MEDIUM;
        ref->local_slot = (uint16_t)(slot - MEDIUM_SLOT_BASE);
        ref->data = s_medium_slots[ref->local_slot].data;
        ref->capacity = BYTE_POOL_MEDIUM_SIZE;
        ref->magic = &s_medium_slots[ref->local_slot].magic;
        return true;
    }

    ref->class_id = BYTE_POOL_LARGE;
    ref->local_slot = (uint16_t)(slot - LARGE_SLOT_BASE);
    ref->data = s_large_slots[ref->local_slot].data;
    ref->capacity = BYTE_POOL_LARGE_SIZE;
    ref->magic = &s_large_slots[ref->local_slot].magic;
    return true;
}

static bool pool_span_matches(const byte_span_t *span,
                              slot_ref_t *ref)
{
    if (span == NULL ||
        !pool_get_slot(span->slot, ref)) {
        return false;
    }

    return span->data == ref->data &&
           span->capacity == ref->capacity;
}

static void pool_reset(void)
{
    uint16_t slot;

    memset(s_used_bitmap, 0, sizeof(s_used_bitmap));

    for (slot = 0u; slot < BYTE_POOL_SLOT_COUNT; ++slot) {
        slot_ref_t ref;
        (void)pool_get_slot(slot, &ref);
        *ref.magic = BYTE_POOL_MAGIC_FREE;
    }
}

bool byte_pool_init(void)
{
    bool ok = true;

    BYTE_POOL_LOCK();

    if (s_init_ref_count == UINT16_MAX) {
        ok = false;
    } else {
        if (s_init_ref_count == 0u) {
            pool_reset();
            s_initialized = true;
        }
        ++s_init_ref_count;
    }

    BYTE_POOL_UNLOCK();
    return ok;
}

bool byte_pool_acquire(byte_pool_class_t class_id,
                       byte_span_t *span)
{
    byte_span_t result = {0};
    bool ok = false;
    uint16_t local_slot;

    if (span == NULL) {
        return false;
    }
    *span = (byte_span_t){0};

    BYTE_POOL_LOCK();

    if (!s_initialized ||
        class_id < BYTE_POOL_SMALL ||
        class_id >= BYTE_POOL_CLASS_COUNT) {
        goto out;
    }

    for (local_slot = 0u;
         local_slot < pool_class_count(class_id);
         ++local_slot) {
        uint32_t bit = UINT32_C(1) << local_slot;
        uint16_t slot = pool_slot_id(class_id, local_slot);
        slot_ref_t ref;

        (void)pool_get_slot(slot, &ref);

        if ((s_used_bitmap[class_id] & bit) == 0u) {
            if (*ref.magic != BYTE_POOL_MAGIC_FREE) {
                goto out;
            }

            s_used_bitmap[class_id] |= bit;
            *ref.magic = BYTE_POOL_MAGIC_USED;

            result.data = ref.data;
            result.capacity = ref.capacity;
            result.slot = slot;
            ok = true;
            break;
        }
    }

out:
    BYTE_POOL_UNLOCK();

    if (ok) {
        memset(result.data, 0, result.capacity);
        *span = result;
    }
    return ok;
}

bool byte_pool_check(const byte_span_t *span)
{
    slot_ref_t ref;
    bool ok = false;

    BYTE_POOL_LOCK();

    if (s_initialized && pool_span_matches(span, &ref)) {
        uint32_t bit = UINT32_C(1) << ref.local_slot;

        ok = (s_used_bitmap[ref.class_id] & bit) != 0u &&
             *ref.magic == BYTE_POOL_MAGIC_USED;
    }

    BYTE_POOL_UNLOCK();
    return ok;
}

bool byte_pool_release(byte_span_t *span)
{
    slot_ref_t ref;
    bool ok = false;

    BYTE_POOL_LOCK();

    if (s_initialized && pool_span_matches(span, &ref)) {
        uint32_t bit = UINT32_C(1) << ref.local_slot;

        if ((s_used_bitmap[ref.class_id] & bit) != 0u &&
            *ref.magic == BYTE_POOL_MAGIC_USED) {
            *ref.magic = BYTE_POOL_MAGIC_FREE;
            s_used_bitmap[ref.class_id] &= ~bit;
            ok = true;
        }
    }

    BYTE_POOL_UNLOCK();

    if (ok) {
        *span = (byte_span_t){0};
    }
    return ok;
}

static bool pool_all_free(void)
{
    uint16_t slot;

    for (slot = 0u; slot < BYTE_POOL_CLASS_COUNT; ++slot) {
        if (s_used_bitmap[slot] != 0u) {
            return false;
        }
    }

    for (slot = 0u; slot < BYTE_POOL_SLOT_COUNT; ++slot) {
        slot_ref_t ref;
        (void)pool_get_slot(slot, &ref);
        if (*ref.magic != BYTE_POOL_MAGIC_FREE) {
            return false;
        }
    }

    return true;
}

bool byte_pool_deinit(void)
{
    bool ok = false;

    BYTE_POOL_LOCK();

    if (!s_initialized || s_init_ref_count == 0u) {
        goto out;
    }

    --s_init_ref_count;
    if (s_init_ref_count > 0u) {
        ok = true;
        goto out;
    }

    ok = pool_all_free();
    s_initialized = false;

out:
    BYTE_POOL_UNLOCK();
    return ok;
}

实现中没有 malloc/freeslot 采用全局编号,内部再映射为类别与局部索引。

调用方式

缓冲区应跟随对象生命周期申请和释放,而不是在高频报文路径中反复操作池。

typedef struct {
    byte_span_t work_buffer;
} worker_t;

bool worker_create(worker_t *worker)
{
    if (worker == NULL || !byte_pool_init()) {
        return false;
    }

    if (!byte_pool_acquire(BYTE_POOL_MEDIUM,
                           &worker->work_buffer)) {
        (void)byte_pool_deinit();
        return false;
    }

    return true;
}

bool worker_process(worker_t *worker,
                    const uint8_t *input,
                    uint16_t input_size)
{
    byte_span_t *span;

    if (worker == NULL || input == NULL) {
        return false;
    }

    span = &worker->work_buffer;
    if (input_size > span->capacity) {
        return false;
    }

    memcpy(span->data, input, input_size);
    return byte_pool_check(span);
}

bool worker_destroy(worker_t *worker)
{
    bool released;
    bool deinitialized;

    if (worker == NULL) {
        return false;
    }

    released = byte_pool_release(&worker->work_buffer);
    deinitialized = byte_pool_deinit();
    return released && deinitialized;
}

所有失败分支都应进入统一清理路径,避免申请成功后提前返回导致槽位泄漏。

检查规则

检查和释放不能直接信任调用方传入的 span。内部根据全局 slot 找到真实槽位,再核对:

项目 条件
slot 在全局槽位范围内,并能映射到唯一类别
capacity 等于该类别的固定容量
data 等于真实槽位的 data 地址
位图 对应类别和局部槽位为占用
Magic 真实数据尾部为 USED

若位图显示空闲但 Magic 不是 FREE,说明状态不同步或内存损坏。释放校验失败时,不应把槽位重新投入使用,也不应清空调用方视图,以便保留诊断信息。

检测边界

        头部下溢             合法数据区             尾部顺序越界
   ◀──────────────  ┌──────────────────────┐  ──────────────▶
      无法检测       │         data         │    可破坏 Magic
                    └──────────────────────┴──────────────┐
                                                       magic

尾部 Magic 主要检测顺序写越过末尾。它不能单独检测头部下溢或绕过 Magic 的跳跃写;若越界结果恰好等于预期 Magic,也可能漏检。

因此它只是低成本诊断线索,不能替代长度、索引和整数溢出检查。

最小验证项

  • 初始化、申请、检查、释放和反初始化的正常状态转换。
  • 申请成功后数据区全零,且清零发生在临界区外。
  • 池满、重复释放、伪造视图和错误容量被拒绝。
  • 主动破坏尾部 Magic 后,检查与释放失败。
  • 有残留占用或 Magic 损坏时,最终反初始化失败。
  • 多个使用方共享初始化引用计数。
  • 并发申请不会返回同一槽位。
  • 通过 map 文件核对各类槽位的实际 BSS 占用。
  • 完整目标构建通过,且没有由字节池引入的新编译错误。

设计要点可以归结为三句话:

  1. 按容量和并发量规划固定类别,不让调用者申请任意大小。
  2. 全局临界区只保护分配状态,成功申请后的 slot 由调用者独占。
  3. Magic 紧邻真实数据尾部;申请后锁外清零,释放时不做竞态清零。
posted @ 2026-08-26 17:11  蓝天上的云℡  阅读(2)  评论(0)    收藏  举报