内存池
内存池
📚 本节导读
学习时长: 约 25 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: 4.4 内存管理、C 语言链表
内存池(Memory Pool)是 OneOS 提供的另一种内存管理机制。与堆内存不同,内存池预先分配固定大小的内存块,分配和释放操作具有确定性时间,且无碎片问题。它特别适合高频、固定大小的对象分配场景,如网络数据包缓冲区、传感器数据缓存等。
🎯 学习目标
- 理解内存池与堆内存的区别和适用场景
- 掌握内存池的创建、分配、释放 API
- 了解内存池大小计算方法
- 掌握内存池溢出检测机制
- 能够使用 Shell 命令查看内存池状态
一、内存池概念
1.1 什么是内存池
内存池是一块预先分配好的内存区域,被划分为若干个大小相同的块(block)。分配和释放操作都在这些固定大小的块上进行。
┌──────────────────────────────────────────────────────────────┐
│ 内存池结构 │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 内存池控制块 (os_mempool_t) │ │
│ │ ┌──────────┬──────────┬──────────┬──────────────┐ │ │
│ │ │ blk_size │ total_num│ free_num │ free_list ──┐│ │ │
│ │ └──────────┴──────────┴──────────┴─────────────┘│ │ │
│ └──────────────────────────────────────────────────┼───┘ │
│ │ │
│ ┌──────────────────────────────────────────────────┘ │
│ ▼ │
│ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │ 块 0 │──→│ 块 1 │──→│ 块 2 │──→ ... →│ 块 N-1│──→NULL│
│ └───────┘ └───────┘ └───────┘ └───────┘ │
│ 空闲链表头 (全部空闲块串联) │
│ │
│ 分配后: │
│ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │ 块 2 │──→ ... →│ 块 N-1│──→NULL (剩余空闲块) │
│ └───────┘ └───────┘ │
│ 空闲链表头 │
│ ┌───────┐ │
│ │ 块 0 │ (已分配,在用户手中) │
│ └───────┘ │
│ ┌───────┐ │
│ │ 块 1 │ (已分配,在用户手中) │
│ └───────┘ │
└──────────────────────────────────────────────────────────────┘1.2 内存池 vs 堆内存
| 特性 | 内存池 | 堆内存 |
|---|---|---|
| 分配大小 | 固定 | 可变 |
| 分配时间 | 确定性 O(1) | 非确定性 |
| 碎片 | 无 | 有 |
| 超时等待 | 支持 | 不支持 |
| 并发保护 | 自旋锁 | 信号量 |
| 适用场景 | 高频固定大小分配 | 通用动态分配 |
1.3 使用场景与选型边界
一句话核心:当你需要反复分配/释放相同大小的对象、且对分配时间确定性要求高时,用内存池;当大小不确定、需要灵活伸缩时,用堆。
典型场景:
| 场景 | 为什么用内存池 |
|---|---|
| 网络数据包缓冲区 | 包大小固定,高频分配/释放,O(1) 确定性时间 |
| 传感器数据缓存 | 每次读到的数据大小固定,且可能来自中断(需 OS_NO_WAIT) |
| 通信协议控制块 | 每个连接一个固定大小的控制块,频繁创建/销毁 |
| 任务栈/信号量等内核对象 | 数量固定、大小固定,避免堆碎片 |
选型边界表(内存池 vs 堆):
| 判断条件 | 选内存池 | 选堆 |
|---|---|---|
| 块大小固定? | 是 | 否(大小不定) |
| 分配时间要求确定? | 是 | 否(可能耗时) |
| 能忍受内存浪费? | 可(按块大小向上取整) | 否(按需分配) |
| 中断中分配? | 可(OS_NO_WAIT) | 不可 |
| 需要等待空闲块? | 可(超时/永久等待) | 不可 |
选型关键注意点:
- 内存池内部碎片:若请求 40 字节、块是 64 字节,浪费 24 字节。块越小越省。
- 内存池只能分配固定大小,大小必须提前规划好(
OS_MEMPOOL_SIZE算好)。 - 中断中只能
OS_NO_WAIT,不能阻塞等待空闲块。
二、配置
#define OS_USING_MEM_POOL /* 必须:启用内存池 */
#define OS_USING_MEMPOOL_CHECK_TAG /* 可选:启用溢出检测 */三、Shell 命令
show_mempool # 显示所有内存池信息
show_mempool <mempool_address> # 显示指定内存池信息输出示例:
MemPool TotalCount FreeCount PerSize Block Task
---------- ---------- ---------- ---------- ----------
SENSOR_POOL 20 18 128 0
PKT_POOL 100 95 256 5: task1四、内部实现要点
4.1 核心数据结构与宏
os_mempool_t 控制块完整源码(kernel/source/os_prototypes.h,与项目逐字一致):
来源:kernel/source/os_prototypes.h
struct os_mempool
{
void *start_addr; /* Memory pool start. */
void *free_list; /* Avaliable memory blocks list. */
os_size_t size; /* Size of memory pool. */
os_size_t blk_size; /* Size of memory blocks, maybe not the size for users. */
os_size_t blk_total_num; /* Numbers of memory block. */
os_size_t blk_free_num; /* Numbers of free memory block. */
os_list_node_t task_list_head; /* Task suspend on this memory pool. */
os_list_node_t resource_node; /* Node in resource list */
uint8_t object_alloc_type; /* Indicates whether object is allocated */
uint8_t object_inited; /* Whether inited. */
char name[OS_NAME_MAX + 1]; /* mempool name. */
os_spinlock_t lock; /* Spin lock used under SMP */
};
typedef struct os_mempool os_mempool_t;字段含义:
| 字段 | 含义 |
|---|---|
start_addr | 内存池数据区起始地址(对齐后) |
free_list | 空闲块单向链表头(指向第一个空闲块) |
size | 内存池总大小(字节) |
blk_size | 单个块完整大小(含头部,不是用户可用大小) |
blk_total_num | 块总数量(size / blk_size) |
blk_free_num | 当前空闲块数量 |
task_list_head | 阻塞等待该内存池的任务链表 |
resource_node | 全局资源链表节点 |
object_alloc_type | 对象分配类型(静态/动态) |
object_inited | 是否已初始化 |
name[] | 内存池名称 |
MEMPOOL 相关宏完整源码(kernel/source/os_mem_pool.c,与项目逐字一致):
来源:kernel/source/os_mem_pool.c
#ifdef OS_USING_MEMPOOL_CHECK_TAG
struct mempool_check_tag
{
uint32_t magic_number : 31;
uint32_t used_flag : 1;
};
#define MEMPOOL_MAGIC_NUMBER 0x504F4F4C
#define MEMPOOL_TAG_SIZE sizeof(struct mempool_check_tag)
#define MEMPOOL_SIZE_WITH_TAG(size) ((size) + MEMPOOL_TAG_SIZE)
#define MEMPOOL_PTR_WITHOUT_TAG(mem) ((void *)((char *)(mem) + MEMPOOL_TAG_SIZE))
#define MEMPOOL_PTR_WITH_TAG(mem) ((void *)((char *)(mem) - MEMPOOL_TAG_SIZE))
#define MEMPOOL_GET_ALIGN_BLK_SIZE(blk_size) OS_ALIGN_UP(MEMPOOL_SIZE_WITH_TAG(blk_size), OS_ALIGN_SIZE)
#else
#define MEMPOOL_TAG_SIZE 0UL
#define MEMPOOL_GET_ALIGN_BLK_SIZE(blk_size) OS_ALIGN_UP((blk_size), OS_ALIGN_SIZE)
#endif关键宏含义:
| 宏 | 含义 |
|---|---|
MEMPOOL_TAG_SIZE | 块头部大小(开启检测=4,否则=0) |
MEMPOOL_PTR_WITHOUT_TAG(mem) | 数据指针 → 跳过头部,返回用户指针 |
MEMPOOL_PTR_WITH_TAG(mem) | 用户指针 → 回退头部,得到块头 |
MEMPOOL_GET_ALIGN_BLK_SIZE(blk_size) | 块大小 + 头部后向上对齐到 OS_ALIGN_SIZE |
struct mempool_check_tag | 溢出检测标记(31 位魔数 + 1 位已用标记) |
溢出检测标签详解(合并自内存块章节):
magic_number(31 位):魔数0x504F4F4C(ASCII "POOL"),识别"这是内存池的块头"。used_flag(1 位):已用标记OS_TRUE(1)。
检测原理:
正常情况:
┌──────────┬──────────────────────┐
│ 0x504F4F4C│ 用户数据 │
│ (1)│ │
└──────────┴──────────────────────┘
↑ 标签正常(释放时 MEMPOOL_BLK_TAG_OK 通过)
缓冲区溢出(向前写入):
┌──────────┬──────────────────────┐
│ 0x12345678│ 用户数据 │ ← magic_number 被破坏!
│ (0)│ │
└──────────┴──────────────────────┘
↑ 标签被破坏,释放时 MEMPOOL_BLK_TAG_OK 断言失败- 写标签(分配时):
MEMPOOL_SET_BLK_TAG(mem)写入magic_number = 0x504F4F4C、used_flag = OS_TRUE。 - 验标签(释放时):
MEMPOOL_BLK_TAG_OK(mem)校验magic_number == 0x504F4F4C且used_flag == OS_TRUE。 - 检测失败:
OS_ASSERT(MEMPOOL_BLK_TAG_OK(mem))触发断言崩溃,定位"谁越界写坏了块头"。
if-else 条件走向总结(释放时的标签校验):
os_mempool_free(id, mem) 的标签校验
│
├─ mem = MEMPOOL_PTR_WITH_TAG(mem) /* 回退到块头 */
│
└─ if (MEMPOOL_BLK_TAG_OK(mem))? /* 魔数+已用标记是否完好? */
├─ 是(标签完好)→ 继续释放流程
└─ 否(被越界写坏)→ OS_ASSERT 断言崩溃数值演算(块地址 0x1040,启用检测):
| 项 | 计算 | 结果 |
|---|---|---|
| 块头 | 0x1040(struct mempool_check_tag) | 4 字节 |
| 分配写标签 | magic_number=0x504F4F4C,used_flag=1 | 块头 OK |
| 返回用户指针 | 0x1040 + 4 | 0x1044 |
| 用户越界前写 | 把 0x504F4F4C 覆盖为 0x12345678 | 标签损坏 |
| 释放验标签 | MEMPOOL_BLK_TAG_OK(0x1040) | magic_number 不匹配 → 断言 |
4.2 内存对齐与块大小计算
内存对齐完整源码(os_mempool_init 片段,kernel/source/os_mem_pool.c):
来源:kernel/source/os_mem_pool.c
start_addr = (void *)OS_ALIGN_UP((os_ubase_t)start, OS_ALIGN_SIZE);
size = OS_ALIGN_DOWN((os_ubase_t)start + size - (os_ubase_t)start_addr, OS_ALIGN_SIZE);
blk_size = MEMPOOL_GET_ALIGN_BLK_SIZE(blk_size);start_addr= 起始地址向上对齐到OS_ALIGN_SIZE。size= 向下对齐(去掉起始对齐的偏移)。blk_size= 用户块大小 + 头部(若启用检测)后向上对齐。
数值演算(start = 0x1008、size = 0x520(1312)、blk_size = 64,OS_ALIGN_SIZE = 8):
| 项 | 计算 | 结果 |
|---|---|---|
start_addr | OS_ALIGN_UP(0x1008, 8) | 0x1008(已对齐) |
size | OS_ALIGN_DOWN(0x1008 + 0x520 - 0x1008, 8) | 0x520(1312) |
无检测 blk_size | OS_ALIGN_UP(64, 8) | 64 |
有检测 blk_size | OS_ALIGN_UP(64 + 4, 8) = OS_ALIGN_UP(68, 8) | 72 |
| 无检测块数 | 1312 / 64 | 20 块 |
| 有检测块数 | 1312 / 72 | 18 块 |
注意:启用溢出检测后,每个块额外占 4 字节头部,同样大小的内存池可容纳的块数减少(20 → 18)。
4.3 空闲链表
所有空闲块通过单向链表连接,链表节点 blk_head 直接复用空闲块自身的开头字节(不额外分配节点)。完整源码(kernel/source/os_mem_pool.c,与项目逐字一致):
来源:kernel/source/os_mem_pool.c
struct blk_head
{
struct blk_head *next;
};- 分配:从链表头部取一个块,
free_list = free_list->next(跳过头节点)。 - 释放:将块插入链表头部,
block->next = free_list; free_list = block。 - 关键:
blk_head复用空闲块数据区开头的几个字节存next指针。只有空闲块才需要存next,被分配的块(数据在用户手中)无需该指针,因此"链表节点"不额外占用内存。
为什么 blk_head 复用块数据区开头、不额外占内存?
核心疑问:内存池把内存分成 N 个固定大小块,没有单独的"链表节点存储区"。空闲链表怎么串联这些块?——直接借用每个空闲块自己的数据区开头几个字节存 next。
- 空闲块:躺在链表里、当前没有用户数据,开头几个字节是闲置的,正好用来存
next指针。 - 被分配的块:已给用户、全是用户数据,没有多余空间(也不需要)存
next。
因为"空闲(存指针)"和"被分配(存数据)"在时间上互斥,所以复用安全、零开销。
举例(start_addr = 0x1000、blk_size = 64,4 个块):
初始化时,所有块空闲,串成链表:
0x1000 ──► ┌──────────────┬──────────────────────────────┐
│ blk_head │ │
块0 │ next=0x1040 │ (开头 4/8 字节借来存 next) │
└──────────────┴──────────────────────────────┘
0x1040 ──► ┌──────────────┬──────────────────────────────┐
块1 │ next=0x1080 │ │
└──────────────┴──────────────────────────────┘
0x1080 ──► ┌──────────────┬──────────────────────────────┐
块2 │ next=0x10C0 │ │
└──────────────┴──────────────────────────────┘
0x10C0 ──► ┌──────────────┬──────────────────────────────┐
块3 │ next=NULL │ (链表尾) │
└──────────────┴──────────────────────────────┘每个空闲块开头 4/8 字节存下一个空闲块的地址,其余空间(如 64 - 4 = 60 字节)当前空闲未用。
分配块 0 后,整块 64 字节全给用户,next 指针"消失"(空间归用户数据):
0x1000 ──► ┌──────────────────────────────┐
块0(已分配) │ 用户数据(整块 64 字节) │ ← 开头 next 的空间也归用户
└──────────────────────────────┘
0x1040 ──► ┌──────────────┬──────────────┐
块1(空闲) │ next=0x1080 │ │ ← 仍是空闲块,开头存 next
└──────────────┴──────────────┘
free_list 现在指向块1(0x1040)为什么这样设计:若为每块单独分配一个 blk_head 节点,会额外浪费内存(每块多一个指针)。复用空闲块自身空间,链表节点"寄生"在空闲块里,零额外开销。
与 BUDDY 的对比:BUDDY 每个块有固定独立的块头(buddy_block),始终占 8 字节;内存池则无固定块头,块头(next)只在空闲时借用数据区。所以内存池分配返回的指针就是块起始地址(不跳过头部),而 BUDDY 返回的是跳过块头的数据指针。
块的实际布局(合并自内存块章节):
┌──────────────────────────────────────────────────────────────┐
│ 内存块布局(启用溢出检测) │
│ │
│ ┌──────────────────────┬──────────────────────────────────┐ │
│ │ mempool_check_tag │ 用户数据区 │ │
│ │ ┌────────┬────────┐ │ │ │
│ │ │magic │ used │ │ (blk_size 字节) │ │
│ │ │ 31位 │ 1位 │ │ │ │
│ │ └────────┴────────┘ │ │ │
│ │ 4 字节 │ │ │
│ └──────────────────────┴──────────────────────────────────┘ │
│ │
│ ◄─ MEMPOOL_TAG_SIZE ─►◄────── blk_size ──────────►│
│ ◄──── OS_ALIGN_UP(blk_size + TAG_SIZE, ALIGN) ────►│
│ │
│ 未启用溢出检测时: │
│ ┌──────────────────────────────────────────────────────────┐│
│ │ 用户数据区 (blk_size 字节) ││
│ └──────────────────────────────────────────────────────────┘│
│ ◄────── OS_ALIGN_UP(blk_size, OS_ALIGN_SIZE) ─────────────►│
└──────────────────────────────────────────────────────────────┘- 启用检测:块 = 4 字节标签(
mempool_check_tag)+ 用户数据区,总大小OS_ALIGN_UP(blk_size + TAG_SIZE, ALIGN)。 - 未启用:块 = 纯用户数据区,总大小
OS_ALIGN_UP(blk_size, ALIGN)。
4.4 初始化流程
os_mempool_init 完整源码(kernel/source/os_mem_pool.c,与项目逐字一致):
来源:kernel/source/os_mem_pool.c
os_mempool_id
os_mempool_init(os_mempool_dummy_t *mempool_cb, const char *name, void *start, os_size_t size, os_size_t blk_size)
{
void *start_addr;
os_mempool_t *mempool;
os_err_t ret;
OS_ASSERT(OS_FALSE == os_is_irq_active());
OS_ASSERT(mempool_cb);
OS_ASSERT(start);
OS_ASSERT(size >= OS_ALIGN_SIZE);
OS_ASSERT(blk_size > 0);
mempool = OS_TYPE_CONVERT(os_mempool_t *, mempool_cb);
ret = OS_SUCCESS;
start_addr = (void *)OS_ALIGN_UP((os_ubase_t)start, OS_ALIGN_SIZE);
size = OS_ALIGN_DOWN((os_ubase_t)start + size - (os_ubase_t)start_addr, OS_ALIGN_SIZE);
blk_size = MEMPOOL_GET_ALIGN_BLK_SIZE(blk_size);
if (0 != (size / blk_size))
{
ret = _k_mempool_add_resourcelist(mempool, OS_ALLOC_TYPE_STATIC);
if (OS_SUCCESS == ret)
{
_k_mempool_init(mempool, name, start_addr, size, blk_size);
}
else
{
mempool = OS_NULL;
}
}
else
{
mempool = OS_NULL;
}
return OS_TYPE_CONVERT(os_mempool_id, mempool);
}分步详解:
第一步:参数校验(无条件执行)
OS_ASSERT(OS_FALSE == os_is_irq_active());
OS_ASSERT(mempool_cb);
OS_ASSERT(start);
OS_ASSERT(size >= OS_ALIGN_SIZE);
OS_ASSERT(blk_size > 0);- 校验:不能在中断中调用、控制块/起始地址非空、堆大小 ≥ 对齐大小、块大小 > 0。
第二步:对齐与块大小换算(无条件执行)
start_addr = (void *)OS_ALIGN_UP((os_ubase_t)start, OS_ALIGN_SIZE);
size = OS_ALIGN_DOWN((os_ubase_t)start + size - (os_ubase_t)start_addr, OS_ALIGN_SIZE);
blk_size = MEMPOOL_GET_ALIGN_BLK_SIZE(blk_size);见 4.2 的数值演算。
第三步:判断能否容纳至少一个块(条件:0 != (size / blk_size))
if (0 != (size / blk_size))
{
ret = _k_mempool_add_resourcelist(mempool, OS_ALLOC_TYPE_STATIC);
if (OS_SUCCESS == ret)
{
_k_mempool_init(mempool, name, start_addr, size, blk_size);
}
else
{
mempool = OS_NULL;
}
}
else
{
mempool = OS_NULL;
}- 条件成立(能装下至少 1 个块):
- 先加入全局资源链表(
_k_mempool_add_resourcelist),若该内存池已存在则返回OS_INVAL(重复创建)。 - 子分支:加入成功 → 调用
_k_mempool_init真正初始化(见 4.8 辅助函数)。 - 子分支:加入失败(已存在)→
mempool = OS_NULL。
- 先加入全局资源链表(
- 条件不成立(装不下任何块)→
mempool = OS_NULL。
if-else 条件走向总结(树状图):
os_mempool_init(cb, name, start, size, blk_size)
│
├─ 参数校验(断言)
├─ 对齐 start_addr / size / blk_size
│
└─ if (size / blk_size != 0)? /* 能装下至少一个块? */
├─ 否 → mempool = OS_NULL(初始化失败)
│
└─ 是
├─ ret = 加入资源链表
│
└─ if (加入成功)?
├─ 否(内存池已存在)→ mempool = OS_NULL
└─ 是 → _k_mempool_init() 真正初始化
返回 mempool数值演算(延续 4.2:size=1312、blk_size=64、无检测):
| 项 | 计算 | 结果 |
|---|---|---|
size / blk_size | 1312 / 64 | 20 ≠ 0 → 条件成立 |
| 加入资源链表 | 内存池不在全局列表 | 成功 |
_k_mempool_init | 初始化控制块 | blk_total_num = 20 |
4.5 分配流程
os_mempool_alloc 完整源码(kernel/source/os_mem_pool.c,与项目逐字一致):
来源:kernel/source/os_mem_pool.c
void *os_mempool_alloc(os_mempool_id mempool_id, os_tick_t timeout)
{
void *mem;
os_tick_t tick_before;
os_tick_t tick_elapse;
os_err_t ret;
os_mempool_t *mempool;
OS_KERNEL_INIT();
mempool = OS_TYPE_CONVERT(os_mempool_t *, mempool_id);
OS_ASSERT(mempool);
OS_ASSERT(OS_KOBJ_INITED == mempool->object_inited);
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_irq_active()));
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_irq_disabled()));
OS_ASSERT((OS_NO_WAIT == timeout) || (OS_NULL == k_task_self()) || (OS_FALSE == os_is_schedule_locked()));
OS_ASSERT((timeout < (OS_TICK_MAX / 2)) || (OS_WAIT_FOREVER == timeout));
mem = OS_NULL;
ret = OS_SUCCESS;
#ifdef OS_USING_SMP
os_spin_lock_irq(&mempool->lock, &irq_save);
#else
OS_KERNEL_ENTER();
#endif
while ((mempool->free_list) == OS_NULL)
{
if (OS_NO_WAIT == timeout)
{
ret = OS_NOMEM;
break;
}
OS_ASSERT((OS_NULL != _k_task_self()));
tick_before = os_tick_get_value();
ret = k_block_task(&mempool->lock, irq_save, &mempool->task_list_head, timeout, OS_TRUE);
if (OS_SUCCESS != ret)
{
ret = OS_FAILURE;
return mem;
}
#ifdef OS_USING_SMP
os_spin_lock_irq(&mempool->lock, &irq_save);
#else
OS_KERNEL_ENTER();
#endif
if (OS_WAIT_FOREVER != timeout)
{
tick_elapse = os_tick_get_value() - tick_before;
timeout = (tick_elapse >= timeout) ? OS_NO_WAIT : (timeout - tick_elapse);
}
}
if (OS_SUCCESS == ret)
{
mem = mempool->free_list;
mempool->free_list = ((struct blk_head *)mem)->next;
mempool->blk_free_num--;
#ifdef OS_USING_SMP
os_spin_unlock_irq(&mempool->lock, irq_save);
#else
OS_KERNEL_EXIT();
#endif
#ifdef OS_USING_MEMPOOL_CHECK_TAG
MEMPOOL_SET_BLK_TAG(mem);
mem = MEMPOOL_PTR_WITHOUT_TAG(mem);
#endif
}
else
{
#ifdef OS_USING_SMP
os_spin_unlock_irq(&mempool->lock, irq_save);
#else
OS_KERNEL_EXIT();
#endif
}
return mem;
}分步详解:
第一步:进入临界区(无条件执行)
mem = OS_NULL;
ret = OS_SUCCESS;
OS_KERNEL_ENTER(); /* 或 SMP 下 os_spin_lock_irq */初始化 mem = OS_NULL、ret = OS_SUCCESS,进入临界区(关闭调度或加自旋锁),保证分配操作互斥。
第二步:循环等待空闲块(条件:free_list == OS_NULL)
while ((mempool->free_list) == OS_NULL)
{
if (OS_NO_WAIT == timeout)
{
ret = OS_NOMEM;
break;
}
OS_ASSERT((OS_NULL != _k_task_self()));
tick_before = os_tick_get_value();
ret = k_block_task(&mempool->lock, irq_save, &mempool->task_list_head, timeout, OS_TRUE);
if (OS_SUCCESS != ret)
{
ret = OS_FAILURE;
return mem;
}
if (OS_WAIT_FOREVER != timeout)
{
tick_elapse = os_tick_get_value() - tick_before;
timeout = (tick_elapse >= timeout) ? OS_NO_WAIT : (timeout - tick_elapse);
}
}- 条件
free_list == OS_NULL(无空闲块):进入循环。- 子分支
timeout == OS_NO_WAIT:不等待,ret = OS_NOMEM,break跳出。 - 子分支有超时/永久等待:记录
tick_before,调用k_block_task阻塞当前任务。若阻塞失败返回,ret = OS_FAILURE直接返回。唤醒后更新剩余timeout。
- 子分支
- 条件
free_list != OS_NULL(有空闲块):跳过循环,直接取块。
第三步:取块并返回(条件:ret == OS_SUCCESS)
if (OS_SUCCESS == ret)
{
mem = mempool->free_list;
mempool->free_list = ((struct blk_head *)mem)->next;
mempool->blk_free_num--;
#ifdef OS_USING_SMP
os_spin_unlock_irq(&mempool->lock, irq_save);
#else
OS_KERNEL_EXIT();
#endif
#ifdef OS_USING_MEMPOOL_CHECK_TAG
MEMPOOL_SET_BLK_TAG(mem);
mem = MEMPOOL_PTR_WITHOUT_TAG(mem);
#endif
}
return mem;- 条件
ret == OS_SUCCESS(有块可取):mem = free_list取链表头;free_list跳到下一块;blk_free_num--。- 启用检测时:给块头打魔数标记
MEMPOOL_SET_BLK_TAG,mem = MEMPOOL_PTR_WITHOUT_TAG(mem)跳过头部返回用户指针。
- 条件
ret != OS_SUCCESS:mem保持OS_NULL。 - 离开临界区,返回
mem。
if-else 条件走向总结(树状图):
os_mempool_alloc(id, timeout)
│
├─ 进入临界区
│
└─ while (free_list == NULL)? /* 无空闲块? */
├─ 否(有空闲块)→ 跳过循环
│
└─ 是(无空闲块)
├─ if (timeout == OS_NO_WAIT)?
│ ├─ 是 → ret = OS_NOMEM,跳出循环
│ └─ 否 → 记录时间,k_block_task 阻塞
│ ├─ 若阻塞失败 → 返回 OS_NULL
│ └─ 唤醒后更新剩余 timeout,继续循环判断
│
└─ (循环结束)
│
└─ if (ret == OS_SUCCESS)? /* 有块可取? */
├─ 否 → 返回 OS_NULL
│
└─ 是 → 取链表头,blk_free_num--
├─ 若开启检测 → 打标记,返回用户指针
└─ 否则 → 直接返回块指针数值演算(free_list 链:块0→块1→块2→NULL,blk_free_num = 3):
| 项 | 计算 | 结果 |
|---|---|---|
| 分配前 | free_list = 块0,blk_free_num = 3 | — |
| 取块 | mem = 块0;free_list = 块1 | 块 0 出链 |
blk_free_num-- | 3 - 1 | 2 |
| 无检测返回 | mem = 块0(用户直接用) | 块 0 数据指针 |
| 有检测返回 | mem = 块0 + 4(跳过头部) | 用户指针 |
4.6 释放流程
os_mempool_free 完整源码(kernel/source/os_mem_pool.c,与项目逐字一致):
来源:kernel/source/os_mem_pool.c
void os_mempool_free(os_mempool_id mempool_id, void *mem)
{
os_task_t *task;
os_bool_t need_sched;
os_mempool_t *mempool;
OS_KERNEL_INIT();
mempool = OS_TYPE_CONVERT(os_mempool_t *, mempool_id);
OS_ASSERT(mempool);
OS_ASSERT(OS_KOBJ_INITED == mempool->object_inited);
OS_ASSERT(mem);
OS_ASSERT((mem >= mempool->start_addr) && ((char *)mem < ((char *)mempool->start_addr + mempool->size)));
#ifdef OS_USING_MEMPOOL_CHECK_TAG
mem = MEMPOOL_PTR_WITH_TAG(mem);
OS_ASSERT(MEMPOOL_BLK_TAG_OK(mem));
#endif
OS_ASSERT((((os_size_t)((char *)mem - (char *)mempool->start_addr) % mempool->blk_size) == 0));
#ifdef OS_USING_SMP
os_spin_lock_irq(&mempool->lock, &irq_save);
#else
OS_KERNEL_ENTER();
#endif
((struct blk_head *)mem)->next = mempool->free_list;
mempool->free_list = mem;
mempool->blk_free_num++;
#ifdef OS_USING_SMP
k_kernel_spin_lock();
#endif
if (!os_list_empty(&mempool->task_list_head))
{
task = os_list_first_entry(&mempool->task_list_head, os_task_t, task_node);
need_sched = k_unblock_task(task);
}
else
{
need_sched = OS_FALSE;
}
#ifdef OS_USING_SMP
k_kernel_spin_unlock();
os_spin_unlock_irq(&mempool->lock, irq_save);
if (OS_TRUE == need_sched)
{
OS_KERNEL_ENTER();
OS_KERNEL_EXIT_SCHED();
}
#else
if (OS_TRUE == need_sched)
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}
#endif
}分步详解:
第一步:参数与地址校验(无条件执行)
OS_ASSERT(mem);
OS_ASSERT((mem >= mempool->start_addr) && ((char *)mem < ((char *)mempool->start_addr + mempool->size)));
#ifdef OS_USING_MEMPOOL_CHECK_TAG
mem = MEMPOOL_PTR_WITH_TAG(mem);
OS_ASSERT(MEMPOOL_BLK_TAG_OK(mem));
#endif
OS_ASSERT((((os_size_t)((char *)mem - (char *)mempool->start_addr) % mempool->blk_size) == 0));- 校验
mem非空、且在内存池范围内。 - 启用检测时:
mem回退头部MEMPOOL_PTR_WITH_TAG,校验块头魔数是否有效(防止释放非本池/非法指针)。 - 校验
mem相对start_addr的偏移是blk_size的整数倍(确保是合法的块边界)。
第二步:释放块回链表(无条件执行)
((struct blk_head *)mem)->next = mempool->free_list;
mempool->free_list = mem;
mempool->blk_free_num++;- 把
mem块头next指向当前链表头,free_list = mem,块插入链表头部;blk_free_num++。
第三步:唤醒等待任务(条件:有任务在等待)
if (!os_list_empty(&mempool->task_list_head))
{
task = os_list_first_entry(&mempool->task_list_head, os_task_t, task_node);
need_sched = k_unblock_task(task);
}
else
{
need_sched = OS_FALSE;
}- 条件成立(有任务阻塞等待):唤醒链表头的第一个等待任务(
k_unblock_task)。 - 条件不成立(无等待任务):
need_sched = OS_FALSE,无需调度。
if-else 条件走向总结(树状图):
os_mempool_free(id, mem)
│
├─ 校验 mem 非空 / 在范围内 / 块边界合法(否则断言)
├─ 若开启检测 → 校验块头魔数(否则断言)
├─ 进入临界区
├─ 块插入空闲链表头,blk_free_num++
│
├─ if (有任务阻塞等待)?
│ ├─ 是 → 唤醒第一个等待任务,need_sched = TRUE
│ └─ 否 → need_sched = FALSE
│
└─ 离开临界区,按 need_sched 决定是否触发调度数值演算(分配例子的延续:释放块 0,blk_free_num 从 2 变回 3):
| 项 | 计算 | 结果 |
|---|---|---|
| 释放前 | free_list = 块1,blk_free_num = 2 | — |
| 头插 | 块0->next = 块1;free_list = 块0 | 块 0 回到链表头 |
blk_free_num++ | 2 + 1 | 3 |
| 唤醒 | 若有任务等待 | 唤醒一个分配任务 |
4.7 获取信息
os_mempool_info 完整源码(kernel/source/os_mem_pool.c,与项目逐字一致):
来源:kernel/source/os_mem_pool.c
void os_mempool_info(os_mempool_id mempool_id, os_mpinfo_t *info)
{
os_mempool_t *mempool;
OS_KERNEL_INIT();
mempool = OS_TYPE_CONVERT(os_mempool_t *, mempool_id);
OS_ASSERT(mempool);
OS_ASSERT(OS_KOBJ_INITED == mempool->object_inited);
OS_ASSERT(info);
OS_KERNEL_ENTER();
info->blk_size = mempool->blk_size - MEMPOOL_TAG_SIZE;
info->blk_total_num = mempool->blk_total_num;
info->blk_free_num = mempool->blk_free_num;
OS_KERNEL_EXIT();
}分步详解:
- 校验
mempool/info有效。 - 进入临界区,读取三个字段:
info->blk_size = mempool->blk_size - MEMPOOL_TAG_SIZE:用户可用大小 = 完整块大小 - 头部(对应 3.2os_mpinfo_t的blk_size语义)。info->blk_total_num= 块总数。info->blk_free_num= 空闲块数。
- 离开临界区。
数值演算(blk_size = 72(有检测)、blk_total_num = 18、blk_free_num = 10):
| 字段 | 计算 | 结果 |
|---|---|---|
info->blk_size | 72 - 4 | 68(用户可用) |
info->blk_total_num | 18 | 18 |
info->blk_free_num | 10 | 10 |
4.8 辅助函数 _k_mempool_init / _k_mempool_init_free_list
初始化主流程调用了两个辅助函数,这里单独成节详解。
_k_mempool_init 完整源码(kernel/source/os_mem_pool.c,与项目逐字一致):
来源:kernel/source/os_mem_pool.c
static void _k_mempool_init(os_mempool_t *mempool, const char *name, void *start, os_size_t size, os_size_t blk_size)
{
if (OS_NULL != name)
{
(void)strncpy(&mempool->name[0], name, OS_NAME_MAX);
mempool->name[OS_NAME_MAX] = '\0';
}
else
{
mempool->name[0] = '\0';
}
mempool->start_addr = start;
mempool->size = size;
mempool->blk_size = blk_size;
mempool->blk_total_num = (size / blk_size);
mempool->blk_free_num = mempool->blk_total_num;
_k_mempool_init_free_list(mempool);
os_list_init(&mempool->task_list_head);
#ifdef OS_USING_SMP
os_spin_lock_init(&mempool->lock);
#endif
mempool->object_inited = OS_KOBJ_INITED;
}分步详解:
- 复制名称(
if (name != NULL)→ 拷贝,否则置空)。 - 设置
start_addr、size、blk_size;blk_total_num = size / blk_size;blk_free_num = blk_total_num(初始全空闲)。 _k_mempool_init_free_list(mempool):串联所有空闲块成单向链表(见下)。- 初始化任务阻塞链表
task_list_head、SMP 自旋锁、标记object_inited。
_k_mempool_init_free_list 完整源码(kernel/source/os_mem_pool.c,与项目逐字一致):
来源:kernel/source/os_mem_pool.c
static void _k_mempool_init_free_list(os_mempool_t *mempool)
{
struct blk_head *blk;
uint32_t i;
mempool->free_list = mempool->start_addr;
blk = (struct blk_head *)mempool->start_addr;
for (i = 0; i < (mempool->blk_total_num - 1UL); i++)
{
blk->next = (struct blk_head *)((char *)blk + mempool->blk_size);
blk = blk->next;
}
blk->next = OS_NULL;
}分步详解:
free_list = start_addr:链表头指向第一个块。for循环:从第一个块开始,每块的next指向下一块(偏移blk_size),遍历blk_total_num - 1次,把blk_total_num个块全部串联。- 最后一个块的
next = OS_NULL(链表结尾)。
数值演算(start_addr = 0x1000、blk_size = 64、blk_total_num = 4):
块 0: 0x1000 ──► 块 1: 0x1040 ──► 块 2: 0x1080 ──► 块 3: 0x10C0 ──► NULL
free_list 头指向块 0,每块 next = 前一块地址 + 644.9 任务阻塞与唤醒
当无空闲块时,分配任务会通过 k_block_task 阻塞在内存池的 task_list_head 上(见 4.5 第二步)。当有块被释放时,os_mempool_free 会通过 k_unblock_task 唤醒链表头的一个等待任务(见 4.6 第三步)。
关键点:
- 阻塞:
k_block_task(&lock, irq_save, &task_list_head, timeout, OS_TRUE)把当前任务挂到task_list_head,同时释放临界区(lock),让其他任务能操作内存池。 - 唤醒:释放块后,从
task_list_head取出第一个任务k_unblock_task唤醒。被唤醒的任务回到os_mempool_alloc的while循环,重新判断free_list是否非空。 - 超时:若等待超时(
k_block_task返回非成功),分配返回OS_NULL。
📝 本节小结
核心要点回顾
- 内存池:预先分配一块内存,划分为固定大小的块,分配/释放时间为 O(1)、无外部碎片。
- 适用场景:高频固定大小对象分配(网络包、传感器缓存、通信控制块等),且需要确定性分配时间。
- 核心数据结构:
os_mempool_t(控制块)+blk_head单向空闲链表(复用块数据区开头)。 - 初始化(4.4):对齐地址/大小 →
_k_mempool_init_free_list把所有空闲块串成链表。 - 分配(4.5):无空闲块时按
timeout阻塞等待或返回OS_NULL;有块则取链表头。 - 释放(4.6):块插回链表头,
blk_free_num++,唤醒一个等待任务。 - 溢出检测:
OS_USING_MEMPOOL_CHECK_TAG在块头加 4 字节魔数标记,越界写入会被检测。
关键易错点速查表
| 易错点 | 说明 | 对应章节 |
|---|---|---|
| 块大小固定 | 只能分配固定大小,不能像堆那样按需伸缩 | 1.1 |
| 内部碎片 | 请求 40 字节、块 64 字节会浪费 24 字节 | 1.3 |
| 中断中分配 | 只能 OS_NO_WAIT,不能阻塞 | 3.5 |
| 溢出检测影响块数 | 开启后每块多占 4 字节头部,同大小池块数减少 | 4.2 |
| 释放非法指针 | mem 必须来自同一内存池,否则断言崩溃 | 4.6 |
| 重复释放 | 已释放块再释放 → 魔数/状态校验失败 | 4.6 |
blk_size 语义 | os_mempool_t.blk_size 是含头部,os_mpinfo_t.blk_size 是用户可用 | 4.7 |
| 永久等待风险 | OS_WAIT_FOREVER 若无块释放会一直阻塞 | 3.5 |
下一步
继续学习 4.4.5 内存追踪,了解如何追踪内存分配/释放来定位内存泄漏与使用异常。内存块的块布局、溢出检测标签、地址验证等细节已并入本章(4.3 块布局、4.1 溢出检测)。