代码版本:
zephyr/VERSION=4.4.99
重点源码:kernel/mempool.c、kernel/kheap.c、kernel/mem_slab.c、lib/heap/heap.c、lib/heap/heap.h、include/zephyr/sys/sys_heap.h、include/zephyr/kernel.h
核心修正:旧文档中的k_mem_pool已在 Zephyr 架构重构中废弃;当前主线由sys_heap(底座算法)、k_heap/k_malloc(内核动态堆)和k_mem_slab(固定块池)三分天下。
文章目录
- @[toc]
- 一、数据结构全景总览
-
- 二、整体分层架构
-
- 三、底层核心分配器:`sys_heap` —— 初始化与内存组织形式
-
- 四、`sys_heap` 分配全过程与状态变迁
-
- 五、`sys_heap_free()` 与立即合并策略
-
- 六、动态堆上层封装 —— `k_heap` / `k_malloc` 与 libc `malloc`
-
- 七、`k_mem_slab` —— 固定块内存池的初始化与 Direct Handoff
-
- 八、内存硬化与安全检测机制
-
- 九、运行时统计 API
- 十、工程最佳实践与 API 误用防范
- 十一、关键全局变量与初始化时序总结
- 十二、源码阅读指南
文章目录
- @[toc]
- 一、数据结构全景总览
- 二、整体分层架构
- 三、底层核心分配器:`sys_heap` —— 初始化与内存组织形式
- 四、`sys_heap` 分配全过程与状态变迁
- 五、`sys_heap_free()` 与立即合并策略
- 六、动态堆上层封装 —— `k_heap` / `k_malloc` 与 libc `malloc`
- 七、`k_mem_slab` —— 固定块内存池的初始化与 Direct Handoff
- 八、内存硬化与安全检测机制
- 九、运行时统计 API
- 十、工程最佳实践与 API 误用防范
- 十一、关键全局变量与初始化时序总结
- 十二、源码阅读指南
一、数据结构全景总览
在深入代码之前,先把三大核心数据结构放在一起对比,建立起整体印象。
1.1 struct z_heap —— 底层分配器元数据(lib/heap/heap.h)
struct z_heap_bucket {
chunkid_t next; // 该 bucket 下空闲链表的首 chunk ID(环形链表入口)
};
struct z_heap {
chunkid_t chunk0_hdr[2]; // 第 0 号 chunk 的 header(被 struct z_heap 本身占据)
chunkid_t end_chunk; // 末端哨兵 chunk 的 ID(最高合法 chunk ID)
uint32_t avail_buckets; // 位图:bit[i]=1 表示 bucket[i] 有空闲块
#ifdef CONFIG_SYS_HEAP_RUNTIME_STATS
size_t free_bytes; // 当前空闲总字节数
size_t allocated_bytes; // 当前已分配总字节数
size_t max_allocated_bytes; // 历史峰值
#endif
#ifdef CONFIG_SYS_HEAP_CANARIES_RANDOM
uint32_t canary_base; // 随机 canary 基值
#endif
struct z_heap_bucket buckets[]; // 变长数组:按 2 的幂次分桶,个数由堆大小决定
};
关键设计意图:struct z_heap 被直接 嵌入在堆内存的最起始位置(参见下文初始化),chunk_buf(h) 直接返回 (chunk_unit_t *)h,即堆缓冲区首地址 = 元数据首地址。这意味着堆的前几个 chunk unit 被元数据自身占据(chunk 0),永远标记为 USED。
1.2 struct sys_heap —— 外部句柄(include/zephyr/sys/sys_heap.h)
struct sys_heap {
struct z_heap *heap; // 指向堆内存内部的 struct z_heap
void *init_mem; // 用户传入的原始内存起始地址(用于静态初始化器)
size_t init_bytes; // 用户传入的原始内存大小
};
这是对 struct z_heap 的轻量封装,供外部 API 使用。静态定义时 init_mem/init_bytes 暂存参数,运行时 sys_heap_init() 将它们转化为嵌入堆内存的 struct z_heap。
1.3 struct k_heap —— 内核堆(include/zephyr/kernel.h)
struct k_heap {
struct sys_heap heap; // 底层无锁分配器
_wait_q_t wait_q; // 等待队列(阻塞式分配用)
struct k_spinlock lock; // 自旋锁(保护 sys_heap 的并发访问)
};
1.4 struct k_mem_slab —— 固定块内存池(include/zephyr/kernel.h)
struct k_mem_slab {
_wait_q_t wait_q; // 等待队列
struct k_spinlock lock; // 自旋锁
char *buffer; // 块数组的起始地址
char *free_list; // 空闲块单向链表头指针
struct k_mem_slab_info info; // {num_blocks, block_size, num_used}
// ... tracing / obj_core 字段省略
};
二、整体分层架构
Zephyr 4.4.99 的内存管理采用了严格的分层解耦设计,底层为无锁的纯内存分配算法,上层封装线程同步、等待队列和上下文安全保护:
+-----------------------------------------------------------------------+
| 应用层 / 子系统 |
+-----------------------------------------------------------------------+
| | |
v v v
[ 静态分配对象 ] [ 系统堆/动态堆 API ] [ 固定块内存池 API ]
- Stack Defines - k_malloc / k_free - k_mem_slab_alloc
- K_MEM_SLAB_DEFINE - k_heap_alloc - k_mem_slab_free
- K_HEAP_DEFINE - k_heap_aligned_alloc
| | |
| v |
| +---------------+ |
| | k_heap 包装层 | |
| | (Spinlock + | |
| | Wait Queue) | |
| +---------------+ |
| | |
+--------------+ v |
| sys_heap Allocator |
| (Segregated Fit Algorithm) |
+--------------+---------------+
|
v
物理 RAM (SRAM / CCM)
2.1 内存机制选型速查表
| 场景 | 推荐机制 | 时间复杂度 | 外部碎片 | 特性与适用条件 |
|---|---|---|---|---|
| 线程栈、驱动 Data/Config | 静态分配 | O(1) | 无 | 编译期确定,最安全,无运行时开销 |
| 固定大小对象高频分配(如 Socket/包头) | k_mem_slab | O(1) | 无 | 极低开销,无碎片,支持阻塞等待 |
| 临时变长数据/字符串拼接 | k_malloc() | 确定性有界 | 有 | 全局系统堆,开箱即用,不支持阻塞 |
| 需等待/指定对齐/多 Heap 实例 | k_heap | 确定性有界 | 有 | 支持 timeout、自旋锁保护、自定义对齐 |
| 无 OS 上下文/裸硬件/自定义分配器 | sys_heap | 确定性有界 | 有 | 纯算法实现,不依赖内核对象,无锁 |
三、底层核心分配器:sys_heap —— 初始化与内存组织形式
3.1 sys_heap_init() 的两条核心调用链路
在深入 sys_heap_init() 的逐行实现之前,先搞清楚谁在调用它。在系统启动过程中,sys_heap_init() 通过两条 SYS_INIT 路径被调用,它们分别服务于"内核静态堆"和"libc malloc 堆":
两条路径的对比:
| 维度 | 路径一:内核静态堆 | 路径二:libc malloc 堆 |
|---|---|---|
| 初始化函数 | statics_init() | malloc_prepare() |
| 源码位置 | kernel/kheap.c | lib/libc/common/source/stdlib/malloc.c |
SYS_INIT 阶段 | PRE_KERNEL_1 | POST_KERNEL |
| 初始化优先级 | CONFIG_KERNEL_INIT_PRIORITY_OBJECTS | CONFIG_KERNEL_INIT_PRIORITY_LIBC |
| 服务的堆 | 所有 K_HEAP_DEFINE 堆(含 _system_heap) | z_malloc_heap(C 库 malloc 后端) |
| 堆内存来源 | K_HEAP_DEFINE 宏静态分配的 BSS | CONFIG_COMMON_LIBC_MALLOC_ARENA_SIZE 指定的静态 arena,或 _end 之后的剩余 SRAM |
| 上层 API | k_heap_alloc() / k_malloc() / k_free() | malloc() / free() / realloc() |
| 同步保护 | k_heap.lock 自旋锁 | z_malloc_heap_mutex 互斥锁 |
| 是否支持阻塞等待 | 是(wait_q + timeout) | 否(OOM 直接返回 NULL) |
时序要点:路径一在
PRE_KERNEL_1阶段执行,早于大部分驱动(POST_KERNEL);路径二在POST_KERNEL阶段执行。两者都晚于 KASAN 影子内存注册(PRE_KERNEL_1, 0),这样 KASAN 可以在sys_heap_init()之前准备好。
为什么分两条路径? 内核静态堆(
k_heap)需要等待队列和自旋锁,供k_heap_alloc等内核 API 使用,必须在PRE_KERNEL_1就绪以支持早期内核对象的动态分配;而 libcmalloc堆只服务于 C 标准库malloc/free,且其堆区域可能依赖 MMU/MPU 布局完成(ALLOCATE_HEAP_AT_STARTUP需要k_mem_map),因此推迟到POST_KERNEL。
下面分别详述两条路径,随后再逐行剖析 sys_heap_init() 本身做了什么。
3.2 k_heap_init() 与静态堆的自动初始化(路径一)
void k_heap_init(struct k_heap *heap, void *mem, size_t bytes)
{
z_waitq_init(&heap->wait_q);
heap->lock = (struct k_spinlock) {};
sys_heap_init(&heap->heap, mem, bytes);
}
对于通过 K_HEAP_DEFINE(name, size) 静态定义的堆,Zephyr 在系统启动阶段通过 SYS_INIT 自动完成初始化:
/* kernel/kheap.c */
static int statics_init(void)
{
STRUCT_SECTION_FOREACH(k_heap, heap) {
k_heap_init(heap, heap->heap.init_mem, heap->heap.init_bytes);
}
return 0;
}
SYS_INIT_NAMED(statics_init_pre, statics_init, PRE_KERNEL_1,
CONFIG_KERNEL_INIT_PRIORITY_OBJECTS);
时序要点:静态堆初始化在
PRE_KERNEL_1阶段,早于大部分驱动(POST_KERNEL),晚于 KASAN 影子内存注册(PRE_KERNEL_1, 0)。这样 KASAN 可以在sys_heap_init()之前准备好。
3.3 libc malloc 堆的初始化(路径二)
与内核静态堆不同,libc 的 malloc/free/realloc 并不使用 k_heap,而是直接操作一个裸 sys_heap 实例 z_malloc_heap,并用互斥锁(而非自旋锁)保护并发访问。
堆句柄与互斥锁定义(lib/libc/common/source/stdlib/malloc.c):
Z_LIBC_DATA static struct sys_heap z_malloc_heap;
#ifdef CONFIG_MULTITHREADING
Z_LIBC_DATA SYS_MUTEX_DEFINE(z_malloc_heap_mutex);
static inline void malloc_lock(void) {
sys_mutex_lock(&z_malloc_heap_mutex, K_FOREVER);
}
static inline void malloc_unlock(void) {
sys_mutex_unlock(&z_malloc_heap_mutex);
}
#else
#define malloc_lock()
#define malloc_unlock()
#endif
malloc/free 的实现只是对这个 z_malloc_heap 加锁后调用 sys_heap API,没有等待队列、不支持阻塞:
void *malloc(size_t size)
{
malloc_lock();
void *ret = sys_heap_aligned_alloc(&z_malloc_heap,
__alignof__(z_max_align_t), size);
if (ret == NULL && size != 0) {
errno = ENOMEM;
}
malloc_unlock();
return ret;
}
void free(void *ptr)
{
malloc_lock();
sys_heap_free(&z_malloc_heap, ptr);
malloc_unlock();
}
初始化函数 malloc_prepare() 与堆区域来源:
static int malloc_prepare(void)
{
void *heap_base = NULL;
size_t heap_size;
#ifdef ALLOCATE_HEAP_AT_STARTUP
/* CONFIG_MMU 且 arena_size<0:运行时从空闲页中分配 */
heap_size = k_mem_free_get();
if (heap_size != 0) {
heap_base = k_mem_map(heap_size, K_MEM_PERM_RW);
}
#elif defined(Z_MALLOC_PARTITION_EXISTS) && \
defined(CONFIG_MPU) && \
defined(CONFIG_MPU_REQUIRES_POWER_OF_TWO_ALIGNMENT)
/* MPU 需要功率对齐:逐步缩小到能放下的最大 2 的幂 */
heap_size = 1;
while (heap_size * 2 <= HEAP_SIZE) { heap_size *= 2; }
while (heap_size >= HEAP_ALIGN) {
heap_base = UINT_TO_POINTER(ROUND_UP(HEAP_BASE, heap_size));
if (POINTER_TO_UINT(heap_base) + heap_size <= HEAP_BASE + HEAP_SIZE) {
break;
}
heap_size >>= 1;
}
#else
/* 默认:使用链接脚本中 _end 之后的剩余 SRAM */
heap_base = UINT_TO_POINTER(HEAP_BASE);
heap_size = HEAP_SIZE;
#endif
#if Z_MALLOC_PARTITION_EXISTS && !defined(HEAP_STATIC)
z_malloc_partition.start = POINTER_TO_UINT(heap_base);
z_malloc_partition.size = heap_size;
z_malloc_partition.attr = K_MEM_PARTITION_P_RW_U_RW;
#endif
sys_heap_init(&z_malloc_heap, heap_base, heap_size); /* ← 路径二的入口 */
return 0;
}
SYS_INIT(malloc_prepare, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_LIBC);
堆区域(heap_base / heap_size)有三种来源,由 CONFIG_COMMON_LIBC_MALLOC_ARENA_SIZE 与架构特性共同决定:
| 配置 / 条件 | heap_base 来源 | heap_size 来源 | 说明 |
|---|---|---|---|
ALLOCATE_HEAP_AT_STARTUP(MMU 且 arena_size < 0) | k_mem_map() 动态映射 | k_mem_free_get() 空闲页 | 运行时从内核页框分配 |
CONFIG_COMMON_LIBC_MALLOC_ARENA_SIZE > 0 | 静态 BSS 数组 malloc_arena[] | ROUND_UP(arena_size, HEAP_ALIGN) | 编译期确定,适合无 MMU 的 MCU |
默认(arena_size <= 0,无 MMU) | _end 之后对齐地址 | SRAM 末尾减 _end | 链接脚本预留的剩余 RAM |
为什么用互斥锁而非自旋锁?
malloc/free是标准库接口,可能被应用层在普通线程上下文长期持有(如大块memcpy);用sys_mutex可以在争用时让出 CPU,避免持有期间阻塞中断/调度。而k_heap用自旋锁是因为其临界区极短(仅sys_heap纯算法操作),且k_heap_alloc的阻塞等待由wait_q承担。
时序要点:
malloc_prepare在POST_KERNEL阶段、CONFIG_KERNEL_INIT_PRIORITY_LIBC优先级执行,晚于内核对象初始化(路径一)和大部分驱动初始化。这意味着在PRE_KERNEL_1/POST_KERNEL早期阶段调用标准库malloc()是未定义行为——此时z_malloc_heap尚未初始化。
3.4 sys_heap_init() 逐行详解
了解了两条调用路径之后,现在逐行剖析 sys_heap_init() 本身做了什么。这是整个动态内存体系的起点。当你调用 sys_heap_init(&my_heap, buf, 4096) 时,下面的事情发生(源码 lib/heap/heap.c:772):
void sys_heap_init(struct sys_heap *heap, void *mem, size_t bytes)
{
// 1. 预留末端哨兵 chunk 的 header 空间
bytes -= heap_footer_bytes(bytes); // big_heap: 8字节, small_heap: 4字节
// 2. 对齐:起始向上取整到 CHUNK_UNIT(8字节),末尾向下取整
uintptr_t addr = ROUND_UP(mem, CHUNK_UNIT);
uintptr_t end = ROUND_DOWN((uint8_t *)mem + bytes, CHUNK_UNIT);
chunksz_t heap_sz = (end - addr) / CHUNK_UNIT; // 总 chunk 数量
// 3. struct z_heap 直接嵌入在堆内存的最开头!
struct z_heap *h = (struct z_heap *)addr;
heap->heap = h;
h->end_chunk = heap_sz;
h->avail_buckets = 0; // 初始无空闲
// 4. 计算需要多少个 bucket(基于堆大小)
int nb_buckets = bucket_idx(h, heap_sz) + 1;
chunksz_t chunk0_size = // chunk0类似于超级块的作用,管理整个heap区
chunksz(sizeof(struct z_heap) + nb_buckets * sizeof(struct z_heap_bucket))
+ CHUNK_TRAILER_SIZE;
// 5. 初始化所有 bucket 为空
for (int i = 0; i < nb_buckets; i++) {
h->buckets[i].next = 0;
}
// 6. ===== 创建三个初始 chunk =====
// chunk 0:元数据自身占据的区域,标记为 USED(永不释放)
set_chunk_size(h, 0, chunk0_size);
set_left_chunk_size(h, 0, 0);
set_chunk_used(h, 0, true);
// chunk chunk0_size:剩余所有空间,标记为 FREE(初始唯一的大空闲块)
set_chunk_size(h, chunk0_size, heap_sz - chunk0_size);
set_left_chunk_size(h, chunk0_size, chunk0_size);
// chunk heap_sz:末端哨兵,size=0,标记为 USED(防止合并越过边界)
set_chunk_size(h, heap_sz, 0);
set_left_chunk_size(h, heap_sz, heap_sz - chunk0_size); //注意上那么的第1点,bytes已经减去了末端哨兵的空间,已经为其预留了,所以不会溢出
set_chunk_used(h, heap_sz, true);
// 7. 将唯一的空闲大块加入 free list
free_list_add(h, chunk0_size);
}
3.5 初始化后的完整内存布局图
假设我们调用 sys_heap_init(&h, buf, 1024),且为 32 位系统(small heap,header 4 字节,无 canary)。注意 heap_sz 不包含末端哨兵:在算 heap_sz 之前,bytes -= heap_footer_bytes(bytes) 已经先扣掉 4 字节(= 哨兵头部大小),所以 bytes = 1020;再 ROUND_DOWN(1020, 8) = 1016,最终 heap_sz = 1016 / 8 = 127 个 chunk unit(哨兵坐在索引 127 处,占用 [1016, 1020) 这 4 字节,末尾 [1020, 1024) 因向下对齐被浪费):
Byte Offset: 0 32 64 96 128 ... 1016 1020 1024
| | | | | | | | |
+--------+---------+--------+--------+---------+----------+----+----+
| struct z_heap |///| FREE SPACE (chunk "chunk0_size") |END|///|
| + buckets[] |///| (heap_sz - chunk0_size 个 chunk unit) |哨兵|浪费|
| 元数据占据 chunk 0 |///| chunk "c1" |头部| |
+--------+---------+--------+--------+---------+----------+----+----+
Chunk ID: 0 c1 = chunk0_size heap_sz=127
State: USED (永久) FREE (初始) USED (永久)
Size: chunk0_size 127 - chunk0_size 0
堆物理起始地址 addr ──────────────────────────────────────────────────────────
示例中的 chunk0_size 取值说明:为简化图示,上图沿用
chunk0_size = 4(32 字节)作为假设值。实际值由nb_buckets决定:本例min_chunk_size=1,bucket_idx(h,127)=6,nb_buckets=7,sizeof(struct z_heap)+7*4 ≈ 44字节,chunksz(44)=6,即真实chunk0_size=6。下图与表格中的c1可理解为该假设值,重点在于heap_sz=127与哨兵位置的对应关系。
三个 chunk 的 header 字段详细表:
| Chunk ID | LEFT_SIZE | SIZE_AND_USED | 含义 |
|---|---|---|---|
0 | 0 | (chunk0_size << 1) | 1 | 左邻居不存在(置0),自身占 chunk0_size,USED |
c1 (=chunk0_size) | chunk0_size | ((127-c1) << 1) | 0 | 左邻居 chunk0 占 c1,自身占剩余空间,FREE |
127 (哨兵) | 127 - c1 | (0 << 1) | 1 | 左邻居 c1 占 127-c1,自身大小 0,USED |
为什么需要哨兵 chunk? 在
free_chunk()合并时,算法会检查right_chunk(h, c)是否空闲。如果没有哨兵,最后一个 chunk 的right_chunk()会越界。哨兵的size=0且USED=true,阻止合并越过边界。
3.6 Bucket 分桶机制与索引计算
sys_heap 按 2 的幂次将空闲块分桶。Bucket 索引由 bucket_idx() 计算(lib/heap/heap.h:308):
static inline int bucket_idx(struct z_heap *h, chunksz_t sz)
{
unsigned int usable_sz = sz - min_chunk_size(h) + 1;
return 31 - __builtin_clz(usable_sz); // 等价于 floor(log2(usable_sz))
}
min_chunk_size(h) = header 字节数 + 1 + trailer 字节数(换算成 chunk unit)。在小堆(header=4 字节)中约为 2 个 chunk unit,大堆(header=8 字节)中约为 3 个 chunk unit。
示例:假设 min_chunk_size = 2,一个 size=8 的 chunk 归入 bucket:
usable_sz = 8 - 2 + 1 = 7,31 - clz(7) = 31 - 29 = 2,即 bucket 2(对应 2^2=4 到 2^3-1=7 的范围)
Bucket 与尺寸的对应关系:
| Bucket 索引 | 可容纳的 chunk size 范围 | 典型用户请求(约) |
|---|---|---|
| 0 | 2 ~ 3 | 0 ~ 8 字节 |
| 1 | 4 ~ 7 | 9 ~ 32 字节 |
| 2 | 8 ~ 15 | 33 ~ 96 字节 |
| 3 | 16 ~ 31 | 97 ~ 224 字节 |
| … | … | … |
3.7 Chunk Header 的四种字段(lib/heap/heap.h)
每个 chunk 头部包含 4 个字段(定义见 enum chunk_fields),在 big_heap(堆 > 256KB 或 64 位系统)下每个字段为 uint32_t(共 16 字节),在 small_heap 下每个字段为 uint16_t(共 8 字节)。
+---- chunk c 的 header ----+
Chunk 内存布局: | LEFT_SIZE | SIZE_AND_USED | FREE_PREV | FREE_NEXT | Payload...
|<- 头部 ->|<-- 用户可用 -->|
当 USED 时:用户数据从 SIZE_AND_USED 之后开始(覆盖 FREE_PREV/FREE_NEXT)
当 FREE 时:FREE_PREV/FREE_NEXT 存储空闲链表指针
四种字段的作用:
| 字段 | 全称 | 含义 | 访问函数 |
|---|---|---|---|
LEFT_SIZE | Left Chunk Size | 物理左邻居 chunk 的大小(chunk unit 数),用于 left_chunk(h, c) = c - LEFT_SIZE 向左遍历 | chunk_field(h, c, LEFT_SIZE) |
SIZE_AND_USED | Size + Used Flag | bit[0]:USED(1)/FREE(0);bit[31:1]:当前 chunk 的总大小(含 header) | chunk_size(h,c) / chunk_used(h,c) |
FREE_PREV | Free List Previous | 空闲链表前驱节点 chunk ID(仅 FREE 时有效,构成环形双向链表) | prev_free_chunk(h,c) |
FREE_NEXT | Free List Next | 空闲链表后继节点 chunk ID | next_free_chunk(h,c) |
关键设计:
LEFT_SIZE和SIZE_AND_USED始终有效(无论 USED/FREE),它们构成了物理地址上的双向隐式链表。而FREE_PREV/FREE_NEXT仅在 FREE 状态下有效,它们复用 Payload 区域的前 8 字节构成显式空闲链表。
四、sys_heap 分配全过程与状态变迁
4.1 sys_heap_alloc() 完整流程图
sys_heap_alloc(heap, bytes)
│
├── 0. bytes == 0 → return NULL
│
├── 1. bytes_to_chunksz(): 将字节数转换为所需 chunk 数量
│ 计算公式:(bytes/8) + (extra/8) + 向上取整的 header+oddments+trailer
│ 最终 clamp 到 h->end_chunk(不可能超过整个堆大小)
│
├── 2. alloc_chunk(h, chunk_sz): 核心分配(见下节)
│ └─ 返回 0 表示 OOM → return NULL
│
├── 3. 若剩余空间 > 所需:split_chunks() 拆出多余部分
│ └─ 将多余部分作为新 FREE chunk 通过 free_list_add() 归还
│
├── 4. set_chunk_used(h, c, true): 标记 SIZE_AND_USED 的 bit[0] = 1
│ └─ FULL 硬化等级下:set_chunk_canary() 写入尾部 canary
│
├── 5. chunk_mem(h, c): 计算用户指针 = chunk_buf(h)[c] + header_bytes
│
└── 6. 更新 runtime stats → 返回用户指针
4.2 alloc_chunk() —— 有界时间搜索算法的逐行解析
static chunkid_t alloc_chunk(struct z_heap *h, chunksz_t sz)
{
// 步骤 1:计算理想 bucket 索引
int bi = bucket_idx(h, sz);
struct z_heap_bucket *b = &h->buckets[bi];
// ====== 阶段 A:有限步采样(Bounded Search)======
// 在最小可能满足的 bucket 中,最多检查 CONFIG_SYS_HEAP_ALLOC_LOOPS 个节点
// 默认 CONFIG_SYS_HEAP_ALLOC_LOOPS = 3
if (b->next != 0U) {
chunkid_t first = b->next; // 记录起始位置(环形链表检测)
int i = CONFIG_SYS_HEAP_ALLOC_LOOPS;
do {
chunkid_t c = b->next; // 当前候选 chunk
if (chunk_size(h, c) >= sz) {
// 找到了!从空闲链表摘除并返回
free_chunk_check(h, c, false); // 完整性校验
free_list_remove_bidx(h, c, bi);
return c;
}
// 太小,跳过到下一个
b->next = next_free_chunk(h, c);
} while (--i && b->next != first); // 步数耗尽或环形一圈
}
// ====== 阶段 B:降级搜索(Guaranteed Fit)======
// avail_buckets 是 uint32_t 位图,bit[i] = 1 表示 bucket[i] 非空
// ~BIT_MASK(bi+1) 屏蔽掉 ≤bi 的所有低位 bucket(它们的 chunk 太小)
uint32_t bmask = h->avail_buckets & ~BIT_MASK(bi + 1);
if (bmask != 0U) {
// __builtin_ctz 找最低非零位 → 最小但保证够大的 bucket
int minbucket = __builtin_ctz(bmask);
chunkid_t c = h->buckets[minbucket].next; // 直接取首节点(必定 fit)
free_chunk_check(h, c, false);
free_list_remove_bidx(h, c, minbucket);
return c;
}
return 0; // 整个堆没有足够大的空闲块 → OOM
}
为什么有限步采样有效? 假设 LOOPS=3,空闲块大小均匀分布,命中概率约 1 - (1-p)^3。即使最坏情况,阶段 B 保证一定能找到(因为 avail_buckets 位图直接定位到有足够大块的 bucket),时间复杂度始终有界。
4.3 分配前后的堆状态变化示例
假设初始堆有 1024 字节,chunk0_size = 4(元数据占 4 个 chunk unit = 32 字节),用户调用 sys_heap_alloc(&h, 100):
分配前:
Chunk 0 (USED, sz=4) | Chunk 4 (FREE, sz=123) | Chunk 127 (哨兵)
^ ^
空闲链表头 整个 FREE 区域 = 123*8 - header = ~976 字节可用
bytes_to_chunksz() 计算:100 字节 → 需要约 (100/8) + ceil((4+100%8)/8) + trailer ≈ 14 个 chunk unit。
alloc_chunk() 返回 chunk 4(123 远大于 14)。
split_chunks(h, 4, 4+14=18):将 chunk 4 拆为:
- 左半:chunk 4,size=14(给用户)
- 右半:chunk 18,size=123-14=109(放回 free list)
分配后:
Chunk 0 (USED, sz=4) | Chunk 4 (USED, sz=14) | Chunk 18 (FREE, sz=109) | Chunk 127 (哨兵)
^ ^
用户拿到 ptr = 空闲链表新的头
chunk_buf[4] + header_size
各 chunk header 状态更新:
| Chunk | LEFT_SIZE 旧→新 | SIZE_AND_USED 旧→新 | 说明 |
|---|---|---|---|
| 0 | 0→0 | (4<<1)|1 不变 | 无变化 |
| 4 | 4→4 | (123<<1)|0 → (14<<1)|1 | 大小从 123 变为 14,标记 USED |
| 18(新) | 无→14 | 无→(109<<1)|0 | 拆分产生,加入 free list |
| 127 | 123→109 | (0<<1)|1 不变 | LEFT_SIZE 从 123 变为 109(因为 chunk 18 的左邻居是 chunk 4,右邻居是哨兵,RIGHT(chunk 18) = 18+109 = 127 = 哨兵) |
哨兵的 LEFT_SIZE 为什么变了? 因为
split_chunks()不仅更新被拆的 chunk,还更新右邻居的left_chunk_size:set_left_chunk_size(h, right_chunk(h, rc), rsz); // right_chunk(h, 18) = 18 + 109 = 127 (哨兵) // 所以哨兵的 LEFT_SIZE 被更新为 109
4.4 sys_heap_aligned_alloc() 完整解析
sys_heap_aligned_alloc() 在普通分配基础上增加了指定对齐和内部 rewind(回退)编码。它的总体策略不是在已分配块内部永久保留一大段 padding,而是:
- 先申请一个保证能容纳对齐调整的较大 chunk;
- 在其中计算满足条件的返回地址;
- 将能够独立成块的前缀和后缀重新放回 free list;
- 只把中间有效区域标记为 USED。
与其他 sys_heap API 一样,该函数本身不加锁,并发保护由上层负责。
4.4.1 第一阶段:拆分 align 和 rew
入口首先解析 align:
size_t gap, rew;
rew = align & -align;
if (align != rew) {
align -= rew;
gap = min(rew, chunk_header_bytes(h));
} else {
if (align <= chunk_header_bytes(h)) {
return sys_heap_alloc(heap, bytes);
}
rew = 0;
gap = chunk_header_bytes(h);
}
__ASSERT((align & (align - 1)) == 0,
"align must be a power of 2");
表达式:
rew = align & -align;
提取 align 的最低置位位(lowbit)。这段代码支持三种情况:
| 传入值 | 解析结果 | 后续行为 |
|---|---|---|
align == 0 | 无显式对齐要求 | 直接走 sys_heap_alloc() |
align 是 2 的幂 | 普通对齐,rew = 0 | 返回地址本身按 align 对齐 |
align = 实际对齐 | rewind | 内部编码 | 返回地址加 rewind 后按实际对齐对齐 |
普通对齐示例:
传入 align = 32 = 0b0010_0000
lowbit = 32
align == lowbit,说明没有额外的 rewind 位
最终:align = 32,rew = 0
如果普通对齐不大于 chunk_header_bytes(h),chunk 的自然布局已经能满足要求,函数直接调用 sys_heap_alloc(),不再走过量分配流程。
4.4.2 Rewind 编码是什么
源码注释给出的典型编码为:
0x28 = 32 | 8
解析过程为:
传入值 = 0x28 = 0b0010_1000
rew = lowbit(0x28) = 8
实际 align = 0x28 - 8 = 32
它表达的不是“按 40 字节对齐”,而是:
返回一个地址
mem,使mem + 8按 32 字节对齐。
即保证:
(mem + rew) % align == 0
这个机制主要为 kernel/mempool.c:z_alloc_helper() 服务。k_aligned_alloc() 需要在用户内存前隐藏一个 struct k_heap *:
底层返回 mem 用户看到的地址
│ │
v v
+----------------------+-------------------+------------------+
| 隐藏的 struct k_heap * | 用户数据 |
+----------------------+--------------------------------------+
|<------- rew -------->|
如果用户要求 32 字节对齐,隐藏指针占 8 字节,底层就需要返回“32 字节边界之前 8 字节”的地址。包装层写入 heap * 后执行 ++heap_ref,用户地址才恰好落到 32 字节边界。
对应编码代码为:
__align = align | sizeof(heap_ref);
mem = sys_heap_allocator(&heap->heap, __align, size);
heap_ref = mem;
*heap_ref = heap;
mem = ++heap_ref;
Rewind 是 sys_heap 与内核包装层之间的内部约定。普通调用者应传入 0 或正常的 2 的幂对齐值,不应自行拼接该编码。
4.4.3 为什么 gap 能减少过量分配
普通 chunk 的用户地址并不是从 chunk 起点开始,而是已经跨过了 header:
chunk 起点 chunk_mem()
│ │
v v
+--------------+--------------+------------------+
| chunk header | 可用于吸收对齐差值的区域/用户区 |
+--------------+---------------------------------+
|<-- header -->|
因此不需要额外预留完整的 align 字节。源码计算:
/* 普通对齐 */
gap = chunk_header_bytes(h);
/* rewind 编码 */
gap = min(rew, chunk_header_bytes(h));
后续只额外预留:
align - gap
字节,再由 bytes_to_chunksz() 把用户数据、header、trailer 和对齐余量统一换算为 chunk 数量。
4.4.4 过量分配与失败路径
完成参数解析后先处理零长度请求,再计算较大的候选 chunk:
if (bytes == 0) {
return NULL;
}
chunksz_t padded_sz = bytes_to_chunksz(h, bytes, align - gap);
chunkid_t c0 = alloc_chunk(h, padded_sz);
if (c0 == 0) {
return NULL;
}
其中:
bytes 用户请求的有效数据字节数
align-gap 为地址调整预留的额外字节数
padded_sz 包含 header/trailer/对齐余量的 chunk unit 数
c0 alloc_chunk() 返回的原始大 chunk 起点
alloc_chunk() 找不到足够大的空闲块就立即返回 0;sys_heap_aligned_alloc() 随即返回 NULL,本层没有阻塞等待逻辑。
4.4.5 核心对齐公式
拿到原始 chunk 后:
uint8_t *mem = chunk_mem(h, c0);
mem = (uint8_t *)ROUND_UP(mem + rew, align) - rew;
chunk_unit_t *end =
(chunk_unit_t *)ROUND_UP(mem + bytes, CHUNK_UNIT);
对齐公式分三步:
1. mem + rew
2. ROUND_UP(..., align)
3. 再减去 rew
普通模式中 rew == 0,公式等价于:
mem = ROUND_UP(mem, align);
Rewind 模式中,以“32 字节对齐、回退 8 字节”为例:
mem = ROUND_UP(original_mem + 8, 32) - 8;
所以 mem 自己可能不是 32 字节对齐,但 mem + 8 必然是。
end 则把 mem + bytes 向上舍入到 CHUNK_UNIT 边界,因为 sys_heap 只能按 chunk unit 切分内存。
4.4.6 将地址转换回 chunk 边界
函数随后把有效区域转换成 chunk ID:
chunkid_t c = mem_to_chunkid(h, mem);
chunkid_t c_end = end - chunk_buf(h) + CHUNK_TRAILER_SIZE;
CHECK(c >= c0 && c < c_end && c_end <= c0 + padded_sz);
各变量含义为:
c0 原始过量分配 chunk 的起点
c 对齐调整后的有效 chunk 起点
c_end 用户数据结束并计入 trailer 后的 chunk 边界
CHECK 保证裁剪后的 [c, c_end) 完全位于最初申请的 [c0, c0 + padded_sz) 内。
4.4.7 回收未使用的前缀
对齐可能使有效起点后移。如果 c > c0:
if (c > c0) {
split_chunks(h, c0, c);
free_list_add(h, c0);
}
布局从:
[ 原始过量分配 chunk ]
^ c0
变成:
[ 未使用前缀(FREE) ][ 有效区域及后缀 ]
^ c0 ^ c
前缀通过 free_list_add() 回到对应 bucket。如果前缀小于 min_chunk_size(),free_list_add() 会因 undersized_chunk() 而不把它挂入 free list;它仍保留为物理 FREE chunk,之后可在相邻块释放时参与合并。
4.4.8 回收未使用的后缀
若有效区域后面还有空间:
if (right_chunk(h, c) > c_end) {
split_chunks(h, c, c_end);
free_list_add(h, c_end);
}
最终布局为:
[ 前缀 FREE ][ 中间有效区域 USED ][ 后缀 FREE ]
^ c0 ^ c ^ c_end
因此“过量分配”只是寻找对齐地址的临时手段。能够形成独立空闲 chunk 的多余部分会被立即归还,永久额外开销主要来自:
- chunk header 和可选 trailer;
- 按
CHUNK_UNIT向上取整产生的内部碎片; - 小于
min_chunk_size()、暂时无法进入 free list 的碎片。
4.4.9 标记、统计并返回
裁剪结束后,中间区域才被正式标记为已使用:
set_chunk_used(h, c, true);
if (SYS_HEAP_HARDENING_FULL) {
set_chunk_canary(h, c);
}
随后依配置更新运行时统计、heap listener、MSAN 和 sanitizer hook,最后:
return mem;
这里返回的是前面计算出的对齐地址,而不是最初的 chunk_mem(h, c0)。
4.4.10 完整流程图
4.4.11 与 sys_heap_alloc() 的区别
| 项目 | sys_heap_alloc() | sys_heap_aligned_alloc() |
|---|---|---|
| 显式对齐参数 | 无 | 有 |
| 返回地址 | chunk 的自然用户地址 | 调整后的指定对齐地址 |
| Rewind 支持 | 无 | 有,供内部包装层隐藏元数据 |
| 候选大小 | 用户大小 + 元数据 | 再增加对齐余量 |
| 前缀切分 | 不需要 | 可能发生 |
| 后缀切分 | 拆出普通剩余块 | 按对齐后有效区域裁剪 |
| 自身是否加锁 | 否 | 否 |
| 内存不足时等待 | 否 | 否 |
| 典型调用者 | k_heap_alloc() 的底层路径 | libc malloc()、aligned_alloc()、k_aligned_alloc() |
一句话总结:
sys_heap_alloc()从合适的空闲 chunk 头部直接分配;sys_heap_aligned_alloc()先申请得稍大一些,再从中裁出满足“mem + rew按align对齐”的有效 chunk,并回收两端。
五、sys_heap_free() 与立即合并策略
5.1 sys_heap_free() 逐行解析
void sys_heap_free(struct sys_heap *heap, void *mem)
{
if (mem == NULL) return; // ISO C free(NULL) 语义
struct z_heap *h = heap->heap;
chunkid_t c = mem_to_chunkid(h, mem); // 从用户指针反推 chunk ID
// ===== 多层硬化检查 =====
// BASIC (Level 1): 检查 double free → chunk_used 必须为 true
// BASIC (Level 1): 检查 buffer overflow → left(right(c)) == c
// MODERATE (Level 2): 检查 left neighbor 一致性 → right(left(c)) == c
// 核心:标记为 FREE
set_chunk_used(h, c, false);
// 调用 free_chunk() 执行合并逻辑
free_chunk(h, c);
}
5.2 free_chunk() —— 双向立即合并
static void free_chunk(struct z_heap *h, chunkid_t c)
{
// ===== 步骤 1:尝试与右邻居合并 =====
chunkid_t rc = right_chunk(h, c); // 物理右邻居
if (!chunk_used(h, rc)) { // 右邻居是 FREE?
free_chunk_check(h, rc, true); // 验证右邻居完整性
free_list_remove(h, rc); // 从空闲链表摘除右邻居
merge_chunks(h, c, rc); // 合并:c.size += rc.size
}
// merge_chunks 做的事:
// newsz = chunk_size(lc) + chunk_size(rc);
// set_chunk_size(h, lc, newsz);
// set_left_chunk_size(h, right_chunk(h, rc), newsz);
// ===== 步骤 2:尝试与左邻居合并 =====
chunkid_t lc = left_chunk(h, c); // 物理左邻居
if (!chunk_used(h, lc)) { // 左邻居是 FREE?
free_chunk_check(h, lc, false); // 验证左邻居完整性
free_list_remove(h, lc); // 从空闲链表摘除左邻居
merge_chunks(h, lc, c); // 合并:lc.size += c.size
c = lc; // 合并后的 chunk 以 lc 为 ID
}
// ===== 步骤 3:将合并后的大块插回空闲链表 =====
free_list_add(h, c);
}
5.3 释放合并示例
假设释放上文分配的 chunk 4(USED, sz=14,右邻居 chunk 18 是 FREE, sz=109):
释放前: ... | Chunk 4 (USED, sz=14) | Chunk 18 (FREE, sz=109) | ...
^ 要释放的 ^ 右邻居空闲
步骤1: right_chunk(4) = 18, chunk_used(18) = false
→ free_list_remove(18)
→ merge_chunks(4, 18): chunk 4 的 size 变为 14+109 = 123
步骤2: left_chunk(4) = 0, chunk_used(0) = true (元数据)
→ 不合并
步骤3: free_list_add(4): chunk 4 (sz=123, FREE) 加入 bucket[6](因为 123 对应的 2^6=64 ~ 2^7-1=127)
释放后: ... | Chunk 4 (FREE, sz=123) | Chunk 127 (哨兵)
^
回到最初的完整空闲块!
六、动态堆上层封装 —— k_heap / k_malloc 与 libc malloc
sys_heap_init()的两条初始化链路已在 3.1–3.3 节详述,底层分配算法已在第四、五章详述。本章回到上层,比较建立在sys_heap之上的三组运行期接口:可阻塞的k_heap、系统堆接口k_malloc/k_free,以及 libcmalloc/free。
三者不是简单的逐层调用关系,而是共享 sys_heap 底座的不同封装分支:
6.1 k_heap —— 调用者指定、支持超时等待的堆
k_heap 在一个 sys_heap 外增加 k_spinlock 和 wait_q。调用者在分配和释放时都显式传入同一个 struct k_heap *,因此返回指针前面不需要隐藏 heap 所属信息。
6.1.1 k_heap_alloc() —— 带超时的阻塞式分配
k_heap_alloc() 与 k_malloc() 的核心区别在于支持 timeout 等待:
void *k_heap_alloc(struct k_heap *heap, size_t bytes, k_timeout_t timeout)
{
return z_heap_alloc_helper(heap, 0, bytes, timeout, sys_heap_noalign_alloc);
}
static void *z_heap_alloc_helper(struct k_heap *heap, size_t align, size_t bytes,
k_timeout_t timeout,
sys_heap_allocator_t *sys_heap_allocator)
{
k_timepoint_t end = sys_timepoint_calc(timeout); // 计算绝对到期时间
void *ret = NULL;
k_spinlock_key_t key = k_spin_lock(&heap->lock);
// ISR 中调用且 timeout != K_NO_WAIT 会触发断言失败
__ASSERT(!arch_is_in_isr() || K_TIMEOUT_EQ(timeout, K_NO_WAIT), "");
while (ret == NULL) {
ret = sys_heap_allocator(&heap->heap, align, bytes);
// 分配成功 或 不允许等待 或 没有多线程 → 直接返回
if (!IS_ENABLED(CONFIG_MULTITHREADING) ||
(ret != NULL) || K_TIMEOUT_EQ(timeout, K_NO_WAIT)) {
break;
}
// ===== 阻塞等待:挂起当前线程 =====
timeout = sys_timepoint_timeout(end); // 重新计算剩余时间
(void) z_pend_curr(&heap->lock, key, &heap->wait_q, timeout);
// ↑ 释放锁、将本线程加入 wait_q、触发调度
key = k_spin_lock(&heap->lock); // ↑ 唤醒后重新获取锁
}
k_spin_unlock(&heap->lock, key);
return ret;
}
阻塞分配的时间线:
线程A: k_heap_alloc(h, 200, K_MSEC(100))
│
├─ sys_heap_alloc → NULL (OOM)
├─ z_pend_curr → 线程A 挂起到 h->wait_q,释放锁,调度出去
│
│ ... 100ms 超时或线程B 释放内存 ...
│
├─ 被唤醒,重新获取锁
├─ 再次 sys_heap_alloc → 拿到内存!
└─ 释放锁,返回指针
6.1.2 k_heap_free() —— 释放内存并唤醒等待者
void k_heap_free(struct k_heap *heap, void *mem)
{
k_spinlock_key_t key = k_spin_lock(&heap->lock);
sys_heap_free(&heap->heap, mem);
if (IS_ENABLED(CONFIG_MULTITHREADING) &&
(z_unpend_all(&heap->wait_q) != 0)) {
z_reschedule(&heap->lock, key);
} else {
k_spin_unlock(&heap->lock, key);
}
}
释放后的处理分为两种情况:
没有等待线程
→ 直接 k_spin_unlock()
存在等待线程
→ z_unpend_all() 将等待者全部变为 ready
→ z_reschedule() 在释放 heap->lock 的同时执行必要的调度
→ 每个被唤醒线程重新获取 heap->lock 并再次尝试分配
这里唤醒全部等待者,是因为等待请求的大小可能不同。释放出来的块也许无法满足队首线程,却能满足其他等待线程;真正能否成功仍由各线程回到 6.1.1 的分配循环后重新判断。
6.2 k_malloc() / k_free() —— 全局系统堆快速接口
k_malloc() 使用内核全局 _system_heap,但不提供 timeout 参数。与 k_heap_alloc() 不同,它通过隐藏的 k_heap * 记录内存来自哪个资源池,使调用者执行 k_free(ptr) 时不必再次传入 heap。
6.2.1 全局系统堆 _system_heap
/* kernel/mempool.c */
#if (K_HEAP_MEM_POOL_SIZE > 0)
K_HEAP_DEFINE(_system_heap, Z_HEAP_MIN_SIZE_FOR(K_HEAP_MEM_POOL_SIZE));
#define _SYSTEM_HEAP (&_system_heap)
K_HEAP_MEM_POOL_SIZE 是 Kconfig 配置的全局系统堆大小(默认通常为 0,需显式配置)。Z_HEAP_MIN_SIZE_FOR() 确保为请求的有效容量预留足够的 sys_heap 元数据空间。
6.2.2 z_alloc_helper() —— 隐藏 k_heap * 的 Rewind Magic
k_malloc()、k_aligned_alloc() 以及线程资源池分配接口通过 z_alloc_helper() 在分配时隐藏一个回指 k_heap * 的指针。k_heap_alloc() 不经过该函数,也没有这个隐藏前缀,因为它在释放时由调用者显式传入 heap:
static void *z_alloc_helper(struct k_heap *heap, size_t align, size_t size,
sys_heap_allocator_t sys_heap_allocator)
{
struct k_heap **heap_ref;
// 扩展 size:多申请 sizeof(struct k_heap*) 的空间存回指指针
if (size_add_overflow(size, sizeof(heap_ref), &size)) {
return NULL;
}
// 将 sizeof(heap_ref) 编码为 rewind bit → 传给 sys_heap_aligned_alloc
size_t __align = align | sizeof(heap_ref);
// 加锁调用底层分配器
k_spinlock_key_t key = k_spin_lock(&heap->lock);
void *mem = sys_heap_allocator(&heap->heap, __align, size);
k_spin_unlock(&heap->lock, key);
if (mem == NULL) return NULL;
// 在分配块头部写入 k_heap 指针
heap_ref = mem;
*heap_ref = heap;
// 用户指针 = 原始地址 + sizeof(k_heap*)
return ++heap_ref;
}
完整内存布局:
底层 sys_heap 分配器返回的原始地址
│
v
+---------------------------+----------------------------------+
| struct k_heap *heap_ref | 用户可用 Payload |
| (4 或 8 字节) | |
+---------------------------+----------------------------------+
^
k_malloc()/k_aligned_alloc() 返回给用户的 ptr
6.2.3 k_free() —— 反向推导 heap
void k_free(void *ptr)
{
if (ptr == NULL) return;
struct k_heap **heap_ref = ptr;
--heap_ref; // 回退 sizeof(k_heap*) → 定位到隐藏的 header
struct k_heap *heap = *heap_ref; // 读出 k_heap 指针
k_spinlock_key_t key = k_spin_lock(&heap->lock);
sys_heap_free(&heap->heap, heap_ref); // 用原始地址释放
k_spin_unlock(&heap->lock, key);
}
致命陷阱:
k_malloc()分配的内存必须用k_free()释放。sys_heap_alloc()分配的内存必须用sys_heap_free()释放。- 混用会导致指针偏移错误(多回退/少回退 sizeof(k_heap*)),直接破坏堆结构!
6.3 libc malloc() —— 独立的标准库堆封装
libc malloc() 与 k_heap、k_malloc() 一样,属于 sys_heap 之上的策略封装。它使用 3.3 节初始化的专属 z_malloc_heap,并不经过 k_heap_alloc() 或 _system_heap。
6.3.1 运行期调用链
核心源码为:
void *malloc(size_t size)
{
malloc_lock();
void *ret = sys_heap_aligned_alloc(&z_malloc_heap,
__alignof__(z_max_align_t),
size);
if (ret == NULL && size != 0) {
errno = ENOMEM;
}
malloc_unlock();
return ret;
}
完整调用关系为:
malloc(size)
├─ sys_mutex_lock(&z_malloc_heap_mutex, K_FOREVER)
├─ sys_heap_aligned_alloc(&z_malloc_heap,
│ __alignof__(z_max_align_t), size)
│ ├─ 对齐要求较小时 → sys_heap_alloc()
│ └─ 否则 → 过量分配、对齐、回收前后缀
├─ 失败且 size != 0 → errno = ENOMEM
├─ sys_mutex_unlock(&z_malloc_heap_mutex)
└─ 返回用户指针或 NULL
6.3.2 为什么使用 z_max_align_t
z_max_align_t 是 Zephyr 提供的、作用类似 C11 max_align_t 的联合体:
typedef union {
long long thelonglong;
long double thelongdouble;
uintmax_t theuintmax_t;
size_t thesize_t;
uintptr_t theuintptr_t;
void *thepvoid;
void (*thepfunc)(void);
} z_max_align_t;
联合体采用所有成员中最严格的对齐要求,因此:
__alignof__(z_max_align_t)
代表平台常规基础类型所需的最大对齐。malloc() 使用该值,是为了让返回内存可以安全承载任意这类对象;需要更严格对齐的显式过对齐类型应调用 aligned_alloc()。
如果这个对齐值不大于 chunk_header_bytes(h),sys_heap_aligned_alloc() 会直接退化到 sys_heap_alloc();否则执行 4.4 节解析的过量分配和前后缀裁剪过程。
6.3.3 锁等待与内存等待是两回事
多线程配置下,malloc_lock() 调用:
sys_mutex_lock(&z_malloc_heap_mutex, K_FOREVER);
因此 malloc() 可能阻塞等待另一个线程完成对 z_malloc_heap 的操作,但取得锁后若内存不足,它不会等待其他线程调用 free():
malloc 堆正被其他线程操作
→ 等待互斥锁
已取得互斥锁,但没有足够大的空闲 chunk
→ sys_heap_aligned_alloc() 立即返回 NULL
→ errno = ENOMEM
对比 k_heap_alloc():
k_heap_alloc() 分配失败且 timeout != K_NO_WAIT
→ 当前线程进入 heap->wait_q
→ 等待释放内存或超时后重新尝试
malloc() 分配失败
→ 不进入内存等待队列
→ 立即返回 NULL
6.3.4 配套 API 与指针格式
同一 libc 堆上的入口为:
free(ptr)
├─ malloc_lock()
├─ sys_heap_free(&z_malloc_heap, ptr)
└─ malloc_unlock()
realloc(ptr, size)
├─ malloc_lock()
├─ sys_heap_aligned_realloc(&z_malloc_heap, ptr,
│ alignof(z_max_align_t), size)
└─ malloc_unlock()
aligned_alloc(alignment, size)
├─ malloc_lock()
├─ sys_heap_aligned_alloc(&z_malloc_heap, alignment, size)
└─ malloc_unlock()
malloc() 返回的就是 sys_heap_aligned_alloc() 产生的地址,没有 k_malloc() 使用的隐藏 k_heap * 前缀。三组接口的 heap 和指针格式不同:
| 接口族 | 后端 heap | 并发保护 | OOM 后等待内存 | 隐藏 k_heap * | 正确释放方式 |
|---|---|---|---|---|---|
k_heap_alloc() | 调用者指定的 k_heap | k_spinlock | 支持 timeout | 无 | k_heap_free(heap, ptr) |
k_malloc() | _system_heap | k_spinlock | 不支持 | 有 | k_free(ptr) |
libc malloc() | z_malloc_heap | sys_mutex | 不支持 | 无 | free(ptr) |
结论:三组接口最终都可能进入
sys_heap,但它们不是同一个堆,也不共享相同的指针包装格式,不能交叉释放。
七、k_mem_slab —— 固定块内存池的初始化与 Direct Handoff
7.1 k_mem_slab 数据结构解析
struct k_mem_slab {
_wait_q_t wait_q; // 等待队列
struct k_spinlock lock; // 自旋锁
char *buffer; // 块数组基址
char *free_list; // 空闲块单向链表头指针
struct k_mem_slab_info info; // {num_blocks, block_size, num_used}
};
7.2 create_free_list() —— 空闲链表初始化的精妙实现
static int create_free_list(struct k_mem_slab *slab)
{
char *p;
size_t total_size;
// 校验 block_size 非零
CHECKIF(slab->info.block_size == 0U) { return -EINVAL; }
// 校验 buffer 和 block_size 都至少 4 字节对齐(存放指针需要)
CHECKIF(((slab->info.block_size | (uintptr_t)slab->buffer) &
(sizeof(void *) - 1)) != 0U) { return -EINVAL; }
// 计算 buffer 末尾
total_size = slab->info.block_size * slab->info.num_blocks;
slab->free_list = NULL;
p = (char *)(total_size - slab->info.block_size); // 从最后一个块开始
// 从后往前构建单向链表(头插法)
for (uint32_t i = 0; i < slab->info.num_blocks; i++) {
*(char **)p = slab->free_list; // 把当前块的前 4/8 字节写为 next 指针
slab->free_list = p; // 链表头指向当前块
p -= slab->info.block_size; // 指针向前移动一个 block
}
return 0;
}
int k_mem_slab_init(struct k_mem_slab *slab, void *buffer,
size_t block_size, uint32_t num_blocks)
{
int rc;
slab->info.num_blocks = num_blocks;
slab->info.block_size = block_size;
slab->buffer = buffer;
slab->info.num_used = 0U;
slab->lock = (struct k_spinlock) {};
#ifdef CONFIG_MEM_SLAB_TRACE_MAX_UTILIZATION
slab->info.max_used = 0U;
#endif /* CONFIG_MEM_SLAB_TRACE_MAX_UTILIZATION */
rc = create_free_list(slab);
if (rc < 0) {
goto out;
}
// ...
z_waitq_init(&slab->wait_q);
k_object_init(slab);
out:
SYS_PORT_TRACING_OBJ_INIT(k_mem_slab, slab, rc);
return rc;
}
初始化后的内存布局(假设 4 个 block,每个 64 字节,buffer 起始地址 0x20000000):
+0 +64 +128 +192 +256 (total_size)
buffer ────────> +---------+---------+---------+---------+
物理地址: | Block 0 | Block 1 | Block 2 | Block 3 |
+---------+---------+---------+---------+
链表构建过程(倒序遍历):
i=0: p = 0x200000C0 (Block 3 起始), *(char**)p = NULL, free_list = 0x200000C0
i=1: p = 0x20000080 (Block 2 起始), *(char**)p = 0x200000C0, free_list = 0x20000080
i=2: p = 0x20000040 (Block 1 起始), *(char**)p = 0x20000080, free_list = 0x20000040
i=3: p = 0x20000000 (Block 0 起始), *(char**)p = 0x20000040, free_list = 0x20000000
最终链表:
free_list → Block0 → Block1 → Block2 → Block3 → NULL
关键洞察:空闲链表复用了块自身的前 sizeof(void*) 字节存储 next 指针,不需要任何额外的内存开销。这也是为什么 block_size 必须至少 sizeof(void*) 字节且对齐。
7.3 k_mem_slab_alloc() —— O(1) 分配
int k_mem_slab_alloc(struct k_mem_slab *slab, void **mem, k_timeout_t timeout)
{
k_spinlock_key_t key = k_spin_lock(&slab->lock);
if (slab->free_list != NULL) {
// ===== 快速路径:有空闲块 =====
*mem = slab->free_list; // 取出链表头
slab->free_list = *(char **)(slab->free_list); // 链表头前进到下一个
slab->info.num_used++;
result = 0;
} else if (K_TIMEOUT_EQ(timeout, K_NO_WAIT) ||
!IS_ENABLED(CONFIG_MULTITHREADING)) {
// ===== 不等待或无双线程 → 返回失败 =====
*mem = NULL;
result = -ENOMEM;
} else {
// ===== 阻塞等待路径 =====
result = z_pend_curr(&slab->lock, key, &slab->wait_q, timeout);
if (result == 0) {
*mem = _current->base.swap_data; // 从 swap_data 拿到 Direct Handoff 的块
}
return result; // 此时锁已被 z_pend_curr 释放
}
k_spin_unlock(&slab->lock, key);
return result;
}
7.4 k_mem_slab_free() —— Direct Handoff 实现
void k_mem_slab_free(struct k_mem_slab *slab, void *mem)
{
k_spinlock_key_t key = k_spin_lock(&slab->lock);
// ===== 关键:先检查是否有等待者 =====
if (unlikely(slab->free_list == NULL) && IS_ENABLED(CONFIG_MULTITHREADING)) {
// free_list 为空意味着所有块都在使用中,极可能有线程在等待
if (z_sched_wake(&slab->wait_q, 0, mem)) {
// ↑ 将 mem 直接传给等待线程的 swap_data,跳过 free_list!
z_reschedule(&slab->lock, key);
return; // 不需要放回 free_list,直接唤醒等待者
}
}
// ===== 正常路径:没有等待者,放回 free_list =====
*(char **) mem = slab->free_list; // 前插到链表头
slab->free_list = (char *) mem;
slab->info.num_used--;
k_spin_unlock(&slab->lock, key);
}
7.5 Direct Handoff 完整时序图
线程A (优先级高) 线程B (优先级低)
│ │
│ k_mem_slab_alloc(slab, &mem, K_FOREVER) │
├─ free_list == NULL │
├─ z_pend_curr → 挂起到 wait_q │
│ (线程A 被调度出去) │
│ │ ... 运行中 ...
│ │ k_mem_slab_free(slab, blockX)
│ ├─ free_list == NULL? YES
│ ├─ z_sched_wake(wait_q, 0, blockX)
│ │ → 将 blockX 写入 线程A.swap_data
│ │ → 将线程A 放入就绪队列
│ ├─ z_reschedule → 触发调度
│ │
│ (线程A 被调度回来) │ (线程B 被调度出去)
├─ 从 swap_data 拿到 blockX ←─────────────┘
├─ *mem = blockX
└─ 返回 0 (成功)
为什么这是"零拷贝"? 传统方案需要:
free将 block 放回free_list- 唤醒等待线程
alloc从free_list取出
Direct Handoff 省去了步骤 1 和 3,直接将 block 指针从释放者传递给等待者,避免了对 free_list 的两次操作。
八、内存硬化与安全检测机制
8.1 硬化等级与检测点映射
| 等级 | Kconfig 值 | 检测内容 | 性能开销 |
|---|---|---|---|
| 0: DISABLED | 0 | 无检测 | 零 |
| 1: BASIC | 1 | free 时检查 double free(chunk_used==false);检查 buffer overflow(left(right(c)) == c) | 极低(两次字段读取+比较) |
| 2: MODERATE | 2 | 上述 + 检查 left neighbor 一致性(right(left(c)) == c);free list 双向链接完整性 | 较低(额外两次字段读取) |
| 3: FULL | 3 | 上述 + Canary(分配时在 chunk trailer 写魔数,释放时校验);left neighbor canary 验证 | 中等(每次 alloc/free 计算+校验 canary) |
| 4: EXTREME | 4 | 上述 + 每次 alloc/free 前完整遍历整个堆校验(z_heap_full_check) | 极高(O(n) 遍历),仅调试用 |
8.2 Canary 机制详解
当 CONFIG_SYS_HEAP_CANARIES 开启后,每个 used chunk 的尾部会追加一个 struct z_heap_chunk_trailer:
struct z_heap_chunk_trailer {
uint32_t canary;
} __aligned(CHUNK_UNIT); // 8 字节对齐
// Canary 值计算:混淆 chunk ID 和 size,防止简单伪造
static inline uint32_t compute_canary(struct z_heap *h, chunkid_t c)
{
return ((c >> 16) | (c << 16)) ^ chunk_size(h, c) ^ HEAP_CANARY_MAGIC(h);
}
带 Canary 的 Used Chunk 布局:
+-----------+---------+------------------+------------------+
| LEFT_SIZE | SIZE&USED| Payload | canary (4 bytes) |
+-----------+---------+------------------+------------------+
用户可用区域 ^
分配时写入,释放时校验
8.3 Buffer Overflow 检测原理
sys_heap_free() 中最关键的溢出检测:
// 检查 left(right_chunk(c)) == c
// 原理:
// right_chunk(c) 读取 c 的 SIZE_AND_USED → 得到右邻居 rc
// left_chunk(rc) 读取 rc 的 LEFT_SIZE → 应该回到 c
// 如果 c 的 payload 发生溢出,会破坏 rc 的 LEFT_SIZE 字段(它在 c 的 payload 之后)
// 导致 left_chunk(rc) 不等于 c
//
// 内存视角:
// [c header] [c payload ] [rc LEFT_SIZE] [rc SIZE...]
// 溢出区域 ──────────→ 破坏这个字段!
if (SYS_HEAP_HARDENING_BASIC && left_chunk(h, right_chunk(h, c)) != c) {
LOG_ERR("heap corruption (buffer overflow?) at %p", mem);
k_panic();
}
九、运行时统计 API
#include <zephyr/sys/sys_heap.h>
// sys_heap 层统计
struct sys_memory_stats stats;
sys_heap_runtime_stats_get(&my_heap.heap, &stats);
printk("free=%zu allocated=%zu max=%zu\n",
stats.free_bytes, stats.allocated_bytes, stats.max_allocated_bytes);
// k_mem_slab 层统计
k_mem_slab_runtime_stats_get(&my_slab, &stats);
// stats.allocated_bytes = num_used * block_size
// stats.free_bytes = (num_blocks - num_used) * block_size
十、工程最佳实践与 API 误用防范
最佳实践法则:
- 严禁混用分配与释放 API:
k_malloc()只能用k_free()释放。k_heap_alloc()只能用k_heap_free()释放。k_mem_slab_alloc()只能用k_mem_slab_free()释放。- 优先使用静态分配:线程栈(
K_THREAD_STACK_DEFINE)、信号量、队列等内核对象,切忌动态分配。- 避免在高频 Loop 中使用
k_malloc():尽管sys_heap时间有界,但碎片化依然不可避免。频繁固定大小申请请改用k_mem_slab。- 超时设置技巧:需要等待内存可用时,绝不能用
k_malloc(),必须显式定义k_heap并传入K_MSEC(50)等超时参数。- ISR 中必须
K_NO_WAIT:中断上下文中调用k_heap_alloc()或k_mem_slab_alloc()时,timeout 必须为K_NO_WAIT,否则触发 kernel panic。
十一、关键全局变量与初始化时序总结
| 变量/函数 | 定义位置 | 初始化时机 | 作用 |
|---|---|---|---|
_system_heap (K_HEAP_DEFINE) | kernel/mempool.c | PRE_KERNEL_1 通过 statics_init() | 全局系统堆,k_malloc/k_free 的后端 |
自定义 K_HEAP_DEFINE 堆 | 用户代码 | PRE_KERNEL_1 通过 statics_init() 遍历 k_heap section | 每个静态堆自动初始化 |
K_MEM_SLAB_DEFINE slab | 用户代码 | K_KERNEL_INIT_PRE 通过 init_mem_slab_module() | 构建 free_list 链表 |
sys_heap 裸分配器 | 用户代码 | 用户显式调用 sys_heap_init() | 无自动初始化,需手动调用 |
十二、源码阅读指南
源码入口点 关键逻辑 / 学习目标
├── lib/heap/heap.h --> Chunk Header 结构、bucket_idx 计算、字段访问器
├── lib/heap/heap.c:sys_heap_init() --> 初始化三 chunk 布局、bucket 数组创建
├── kernel/kheap.c:statics_init() --> 路径一:PRE_KERNEL_1 遍历静态 k_heap 初始化
├── lib/libc/common/source/stdlib/malloc.c --> 路径二:malloc_prepare() 初始化 z_malloc_heap
├── lib/heap/heap.c:alloc_chunk() --> 有限步采样 + 降级搜索算法
├── lib/heap/heap.c:sys_heap_alloc() --> 分配入口:size 转换 → alloc → split → 标记
├── lib/heap/heap.c:free_chunk() --> 双向立即合并逻辑
├── lib/heap/heap.c:split_chunks() --> chunk 拆分与邻居 LEFT_SIZE 更新
├── lib/heap/heap.c:sys_heap_aligned_alloc() --> 对齐分配、Rewind 机制
├── kernel/mempool.c:z_alloc_helper() --> k_heap* 隐藏指针、Rewind 编码
├── kernel/mempool.c:k_malloc()/k_free() --> 全局系统堆的快速路径
├── kernel/kheap.c:z_heap_alloc_helper() --> 阻塞式分配的 wait_q 循环
├── kernel/kheap.c:k_heap_init()/statics_init --> 静态堆自动初始化
└── kernel/mem_slab.c --> free_list 构建、Direct Handoff 唤醒

399

被折叠的 条评论
为什么被折叠?



