内存块
2026/7/16大约 5 分钟内核组件内核内存管理内存块
内存块
📚 本节导读
学习时长: 约 15 分钟
难度级别: ⭐⭐☆☆☆
前置知识: 4.4.4 内存池、内存对齐
内存块(Block)是内存池的基本分配单位。理解内存块的内部结构、大小计算和检测机制,对于正确使用内存池和调试内存问题至关重要。本节深入剖析内存块的实现细节。
🎯 学习目标
- 理解内存块的大小对齐计算规则
- 掌握内存块的分配与释放机制
- 了解溢出检测标签(check tag)的原理
- 理解内存块与内存池的关系
一、内存块结构
1.1 块布局
每个内存块在内存池中的实际布局如下:
┌──────────────────────────────────────────────────────────────┐
│ 内存块布局(启用溢出检测) │
│ │
│ ┌──────────────────────┬──────────────────────────────────┐ │
│ │ mempool_check_tag │ 用户数据区 │ │
│ │ ┌────────┬────────┐ │ │ │
│ │ │magic │ used │ │ (blk_size 字节) │ │
│ │ │ 31位 │ 1位 │ │ │ │
│ │ └────────┴────────┘ │ │ │
│ │ 4 字节 │ │ │
│ └──────────────────────┴──────────────────────────────────┘ │
│ │
│ ◄─ OS_MEMPOOL_BLK_HEAD_SIZE ─►◄────── blk_size ──────────►│
│ ◄──────────── OS_ALIGN_UP(blk_size + HEAD_SIZE, ALIGN) ───►│
│ │
│ 未启用溢出检测时: │
│ ┌──────────────────────────────────────────────────────────┐│
│ │ 用户数据区 (blk_size 字节) ││
│ └──────────────────────────────────────────────────────────┘│
│ ◄────── OS_ALIGN_UP(blk_size, OS_ALIGN_SIZE) ─────────────►│
└──────────────────────────────────────────────────────────────┘1.2 空闲块结构
当块处于空闲状态时,其数据区的前几个字节被用作链表指针:
struct blk_head
{
struct blk_head *next; /* 指向下一个空闲块 */
};空闲块链表:
free_list → [块0] → [块1] → [块2] → ... → [块N-1] → NULL二、块大小计算
2.1 计算公式
#ifdef OS_USING_MEMPOOL_CHECK_TAG
#define OS_MEMPOOL_BLK_HEAD_SIZE 4 /* 4 字节溢出检测标签 */
#else
#define OS_MEMPOOL_BLK_HEAD_SIZE 0 /* 无额外开销 */
#endif
#define OS_MEMPOOL_SIZE(block_count, block_size) \
((OS_ALIGN_UP(block_size + OS_MEMPOOL_BLK_HEAD_SIZE, OS_ALIGN_SIZE)) * block_count)实际块大小(内部使用)计算:
/* 启用溢出检测时 */
blk_size = OS_ALIGN_UP(blk_size + sizeof(struct mempool_check_tag), OS_ALIGN_SIZE);
/* 未启用溢出检测时 */
blk_size = OS_ALIGN_UP(blk_size, OS_ALIGN_SIZE);2.2 计算示例
假设 OS_ALIGN_SIZE = 4(32 位平台):
| 用户请求块大小 | 启用溢出检测 | OS_MEMPOOL_BLK_HEAD_SIZE | 实际块大小 | 内部开销 |
|---|---|---|---|---|
| 32 | 否 | 0 | 32 | 0 |
| 32 | 是 | 4 | 36 | 4 |
| 33 | 否 | 0 | 36 | 3 (对齐填充) |
| 33 | 是 | 4 | 40 | 7 (标签+对齐填充) |
| 64 | 否 | 0 | 64 | 0 |
| 64 | 是 | 4 | 68 | 4 |
2.3 总内存池大小计算
/* 10 个块,每个块 64 字节(用户可用),启用溢出检测 */
#define POOL_SIZE OS_MEMPOOL_SIZE(10, 64)
/* 实际计算:(64 + 4 对齐到 4) * 10 = 68 * 10 = 680 字节 */
/* 不启用溢出检测 */
#define POOL_SIZE OS_MEMPOOL_SIZE(10, 64)
/* 实际计算:(64 + 0 对齐到 4) * 10 = 64 * 10 = 640 字节 */三、分配与释放机制
3.1 分配流程
os_mempool_alloc(mempool_id, timeout)
│
├── 检查 free_list 是否为空
│
├── 有空闲块:
│ ├── 从 free_list 头部取一个块
│ ├── free_list = free_list->next
│ ├── blk_free_num--
│ │
│ ├── [启用溢出检测]
│ │ ├── 设置 magic_number = 0x504F4F4C ("POOL")
│ │ ├── 设置 used_flag = OS_TRUE
│ │ └── 返回指针 = 块地址 + MEMPOOL_TAG_SIZE (跳过标签)
│ │
│ └── [未启用溢出检测]
│ └── 直接返回块地址
│
└── 无空闲块:
├── timeout == OS_NO_WAIT → 返回 OS_NULL
└── timeout != OS_NO_WAIT → 阻塞任务,等待释放3.2 释放流程
os_mempool_free(mempool_id, mem)
│
├── 验证 mem 指针在内存池范围内
│
├── [启用溢出检测]
│ ├── 回退指针:mem = mem - MEMPOOL_TAG_SIZE
│ ├── 验证 magic_number == 0x504F4F4C
│ ├── 验证 used_flag == OS_TRUE
│ └── 验证块地址对齐
│
├── 将块插入 free_list 头部
├── blk_free_num++
│
└── 检查是否有任务在等待此内存池
└── 有 → 唤醒一个等待任务四、溢出检测标签
4.1 配置
#define OS_USING_MEMPOOL_CHECK_TAG- 启用后,每个块头部增加 4 字节的检测标签
- 源文件:
kernel/source/os_mem_pool.c
4.2 标签结构
struct mempool_check_tag
{
uint32_t magic_number : 31; /* 魔数:0x504F4F4C */
uint32_t used_flag : 1; /* 使用标志:OS_TRUE (1) */
};4.3 检测原理
正常情况:
┌──────────┬──────────────────────┐
│ 0x504F4F4C│ 用户数据 │
│ (1)│ │
└──────────┴──────────────────────┘
↑ 标签正常
缓冲区溢出(向前写入):
┌──────────┬──────────────────────┐
│ 0x12345678│ 用户数据 │ ← magic_number 被破坏!
│ (0)│ │
└──────────┴──────────────────────┘
↑ 标签被破坏,释放时触发断言失败检测时机:
- 释放时:调用
os_mempool_free时,会检查标签是否完好 - 检查失败:触发
OS_ASSERT断言,帮助定位内存越界问题
4.4 相关宏定义
#define MEMPOOL_MAGIC_NUMBER 0x504F4F4C /* 魔数,ASCII 为 "POOL" */
#define MEMPOOL_TAG_SIZE sizeof(struct mempool_check_tag) /* 4 字节 */
#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)) /* 回退到标签 */五、对齐要求
5.1 块大小对齐
块大小按 OS_ALIGN_SIZE 对齐(通常为 4 或 8 字节):
blk_size = OS_ALIGN_UP(blk_size + OS_MEMPOOL_BLK_HEAD_SIZE, OS_ALIGN_SIZE);5.2 起始地址对齐
内存池的起始地址同样按 OS_ALIGN_SIZE 对齐:
start_addr = (void *)OS_ALIGN_UP((os_ubase_t)start, OS_ALIGN_SIZE);5.3 释放时的地址验证
释放时,会验证传入的指针是否在合法范围内,且地址与块大小对齐:
OS_ASSERT((mem >= mempool->start_addr) &&
((char *)mem < ((char *)mempool->start_addr + mempool->size)));
OS_ASSERT((((os_size_t)((char *)mem - (char *)mempool->start_addr) % mempool->blk_size) == 0));六、块与内存池的关系
os_mempool_t (内存池控制块)
│
├── start_addr ──────→ 指向第一个块的起始地址
├── size ────────────→ 内存池总大小
├── blk_size ────────→ 每个块的实际大小(含标签头,已对齐)
├── blk_total_num ───→ 块总数 = size / blk_size
├── blk_free_num ────→ 空闲块数量
├── free_list ───────→ 空闲块链表头
└── task_list_head ──→ 等待任务链表头
info->blk_size = mempool->blk_size - MEMPOOL_TAG_SIZE (返回用户可用大小)📝 本节小结
- 内存块是内存池的基本分配单位,每个块大小固定
- 块大小计算:
OS_ALIGN_UP(blk_size + OS_MEMPOOL_BLK_HEAD_SIZE, OS_ALIGN_SIZE) - 启用
OS_USING_MEMPOOL_CHECK_TAG时,每块头部增加 4 字节检测标签(魔数 0x504F4F4C) - 溢出检测在释放时进行,通过验证魔数和使用标志来检测内存越界写入
- 空闲块通过单向链表(
blk_head->next)连接,分配和释放均为 O(1) 时间 - 释放时验证地址范围和块对齐,防止非法指针释放