简洁低消耗字节内存池设计
静态字节内存池
这里记录一个嵌入式固定字节池设计:将函数局部的大型临时数组迁移到静态 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 中额外携带类别。本文改用全局唯一的槽位编号,由内部映射还原类别和局部索引,因此公开视图不需要 class 或 id 字段。
容量规划
类别可以按用途与容量组合划分,但对外仍只暴露通用枚举。每个类别应根据两项数据配置:
- 单次处理的最大报文长度;
- 该类缓冲区的真实并发数量。
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/free。slot 采用全局编号,内部再映射为类别与局部索引。
调用方式
缓冲区应跟随对象生命周期申请和释放,而不是在高频报文路径中反复操作池。
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 占用。
- 完整目标构建通过,且没有由字节池引入的新编译错误。
设计要点可以归结为三句话:
- 按容量和并发量规划固定类别,不让调用者申请任意大小。
- 全局临界区只保护分配状态,成功申请后的 slot 由调用者独占。
- Magic 紧邻真实数据尾部;申请后锁外清零,释放时不做竞态清零。
本文来自博客园,作者:蓝天上的云℡,采用 BY-NC-SA 许可协议,转载请注明:转载自作者蓝天上的云℡ 原文链接 https://www.cnblogs.com/yucloud/p/22703056/char_pool

浙公网安备 33010602011771号