Zephyr RTOS 内存管理深度解析

代码版本zephyr/VERSION = 4.4.99
重点源码kernel/mempool.ckernel/kheap.ckernel/mem_slab.clib/heap/heap.clib/heap/heap.hinclude/zephyr/sys/sys_heap.hinclude/zephyr/kernel.h
核心修正:旧文档中的 k_mem_pool 已在 Zephyr 架构重构中废弃;当前主线由 sys_heap(底座算法)、k_heap/k_malloc(内核动态堆)和 k_mem_slab(固定块池)三分天下。


文章目录

一、数据结构全景总览

在深入代码之前,先把三大核心数据结构放在一起对比,建立起整体印象。

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_slabO(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 堆":

SYS_INIT: PRE_KERNEL_1
CONFIG_KERNEL_INIT_PRIORITY_OBJECTS

statics_init()
kernel/kheap.c

遍历 k_heap section
所有 K_HEAP_DEFINE 堆

k_heap_init()

sys_heap_init()
初始化每个静态 k_heap 的 sys_heap

含 _system_heap
k_malloc / k_free 后端

SYS_INIT: POST_KERNEL
CONFIG_KERNEL_INIT_PRIORITY_LIBC

malloc_prepare()
lib/libc/.../malloc.c

计算 heap_base / heap_size
静态 arena 或 _end 之后的剩余 SRAM

sys_heap_init(&z_malloc_heap)
初始化 libc malloc 堆

malloc / free / realloc
C 标准库分配后端

两条路径的对比:

维度路径一:内核静态堆路径二:libc malloc
初始化函数statics_init()malloc_prepare()
源码位置kernel/kheap.clib/libc/common/source/stdlib/malloc.c
SYS_INIT 阶段PRE_KERNEL_1POST_KERNEL
初始化优先级CONFIG_KERNEL_INIT_PRIORITY_OBJECTSCONFIG_KERNEL_INIT_PRIORITY_LIBC
服务的堆所有 K_HEAP_DEFINE 堆(含 _system_heapz_malloc_heap(C 库 malloc 后端)
堆内存来源K_HEAP_DEFINE 宏静态分配的 BSSCONFIG_COMMON_LIBC_MALLOC_ARENA_SIZE 指定的静态 arena,或 _end 之后的剩余 SRAM
上层 APIk_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 就绪以支持早期内核对象的动态分配;而 libc malloc 堆只服务于 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 < 0k_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_preparePOST_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=1bucket_idx(h,127)=6nb_buckets=7sizeof(struct z_heap)+7*4 ≈ 44 字节,chunksz(44)=6,即真实 chunk0_size=6。下图与表格中的 c1 可理解为该假设值,重点在于 heap_sz=127 与哨兵位置的对应关系。

三个 chunk 的 header 字段详细表

Chunk IDLEFT_SIZESIZE_AND_USED含义
00(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=0USED=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 = 731 - clz(7) = 31 - 29 = 2,即 bucket 2(对应 2^2=4 到 2^3-1=7 的范围)

Bucket 与尺寸的对应关系:

Bucket 索引可容纳的 chunk size 范围典型用户请求(约)
02 ~ 30 ~ 8 字节
14 ~ 79 ~ 32 字节
28 ~ 1533 ~ 96 字节
316 ~ 3197 ~ 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_SIZELeft Chunk Size物理左邻居 chunk 的大小(chunk unit 数),用于 left_chunk(h, c) = c - LEFT_SIZE 向左遍历chunk_field(h, c, LEFT_SIZE)
SIZE_AND_USEDSize + Used Flagbit[0]:USED(1)/FREE(0);bit[31:1]:当前 chunk 的总大小(含 header)chunk_size(h,c) / chunk_used(h,c)
FREE_PREVFree List Previous空闲链表前驱节点 chunk ID(仅 FREE 时有效,构成环形双向链表)prev_free_chunk(h,c)
FREE_NEXTFree List Next空闲链表后继节点 chunk IDnext_free_chunk(h,c)

关键设计LEFT_SIZESIZE_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 状态更新

ChunkLEFT_SIZE 旧→新SIZE_AND_USED 旧→新说明
00→0(4<<1)|1 不变无变化
44→4(123<<1)|0(14<<1)|1大小从 123 变为 14,标记 USED
18(新)无→14无→(109<<1)|0拆分产生,加入 free list
127123→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,而是:

  1. 先申请一个保证能容纳对齐调整的较大 chunk;
  2. 在其中计算满足条件的返回地址;
  3. 将能够独立成块的前缀和后缀重新放回 free list;
  4. 只把中间有效区域标记为 USED。

与其他 sys_heap API 一样,该函数本身不加锁,并发保护由上层负责。

4.4.1 第一阶段:拆分 alignrew

入口首先解析 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 完整流程图

sys_heap_aligned_alloc(heap, align, bytes)

rew = align & -align

align 是否包含 rewind 位?

拆出实际 align、rew 和 gap

自然对齐已经满足?

return sys_heap_alloc(heap, bytes)

rew = 0,gap = header bytes

bytes == 0?

return NULL

padded_sz = bytes_to_chunksz(
bytes, align - gap)

c0 = alloc_chunk(padded_sz)

c0 == 0?

mem = ROUND_UP(mem + rew, align) - rew

计算 c 与 c_end

存在前缀?

split + free_list_add(c0)

存在后缀?

split + free_list_add(c_end)

set_chunk_used(c, true)

写 canary、更新统计与 hooks

return mem

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 + rewalign 对齐”的有效 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,以及 libc malloc/free

三者不是简单的逐层调用关系,而是共享 sys_heap 底座的不同封装分支:

sys_heap 核心算法

k_heap
spinlock + wait_q + timeout

k_malloc / k_free
_system_heap + 隐藏 heap 指针

libc malloc / free
z_malloc_heap + mutex

6.1 k_heap —— 调用者指定、支持超时等待的堆

k_heap 在一个 sys_heap 外增加 k_spinlockwait_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_heapk_malloc() 一样,属于 sys_heap 之上的策略封装。它使用 3.3 节初始化的专属 z_malloc_heap,并不经过 k_heap_alloc()_system_heap

6.3.1 运行期调用链

应用调用 malloc(size)

malloc_lock()
获取 z_malloc_heap_mutex

sys_heap_aligned_alloc(
&z_malloc_heap,
alignof(z_max_align_t), size)

chunk 自然对齐
已经满足要求?

sys_heap_alloc() 快速路径

执行 4.4 节的对齐分配路径

分配成功?

size != 0 时
errno = ENOMEM

得到用户指针

malloc_unlock()

返回指针或 NULL

核心源码为:

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_heapk_spinlock支持 timeoutk_heap_free(heap, ptr)
k_malloc()_system_heapk_spinlock不支持k_free(ptr)
libc malloc()z_malloc_heapsys_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 (成功)

为什么这是"零拷贝"? 传统方案需要:

  1. free 将 block 放回 free_list
  2. 唤醒等待线程
  3. allocfree_list 取出

Direct Handoff 省去了步骤 1 和 3,直接将 block 指针从释放者传递给等待者,避免了对 free_list 的两次操作。


八、内存硬化与安全检测机制

8.1 硬化等级与检测点映射

等级Kconfig 值检测内容性能开销
0: DISABLED0无检测
1: BASIC1free 时检查 double free(chunk_used==false);检查 buffer overflow(left(right(c)) == c极低(两次字段读取+比较)
2: MODERATE2上述 + 检查 left neighbor 一致性(right(left(c)) == c);free list 双向链接完整性较低(额外两次字段读取)
3: FULL3上述 + Canary(分配时在 chunk trailer 写魔数,释放时校验);left neighbor canary 验证中等(每次 alloc/free 计算+校验 canary)
4: EXTREME4上述 + 每次 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 误用防范

最佳实践法则

  1. 严禁混用分配与释放 API
    • k_malloc() 只能用 k_free() 释放。
    • k_heap_alloc() 只能用 k_heap_free() 释放。
    • k_mem_slab_alloc() 只能用 k_mem_slab_free() 释放。
  2. 优先使用静态分配:线程栈(K_THREAD_STACK_DEFINE)、信号量、队列等内核对象,切忌动态分配。
  3. 避免在高频 Loop 中使用 k_malloc():尽管 sys_heap 时间有界,但碎片化依然不可避免。频繁固定大小申请请改用 k_mem_slab
  4. 超时设置技巧:需要等待内存可用时,绝不能用 k_malloc(),必须显式定义 k_heap 并传入 K_MSEC(50) 等超时参数。
  5. ISR 中必须 K_NO_WAIT:中断上下文中调用 k_heap_alloc()k_mem_slab_alloc() 时,timeout 必须为 K_NO_WAIT,否则触发 kernel panic。

十一、关键全局变量与初始化时序总结

变量/函数定义位置初始化时机作用
_system_heap (K_HEAP_DEFINE)kernel/mempool.cPRE_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 唤醒
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值