蔚淦丞
发布于 2026-09-21 / 0 阅读
0
0

Linux堆内存管理器项目介绍

https://github.com/ygc129550/HeapMemoryManager

HeapMemoryManager

一个基于 页面族(Page Family) 的固定大小对象内存分配器,专为结构体类型已知、大小固定的场景设计。每个页面仅服务一种结构体类型,通过 Worst-fit 策略管理页内空闲块,支持自动合并与整页回收。


1. 设计逻辑结构

1.1 核心概念


┌─────────────────────────────────────────────────────────┐

│                  vm_page_for_families_t                 │ ← 元数据页:存储多个 family 描述符

│  ┌──────────────────┐ ┌──────────────────┐              │

│  │ vm_page_family_t │ │ vm_page_family_t │  ...         │

│  │ struct_name      │ │ struct_name      │              │

│  │ struct_size      │ │ struct_size      │              │

│  │ first_page ────┐ │ │                  │              │

│  │ priority_head  │ │ │                  │              │

│  └────────────────┼─┘ └──────────────────┘              │

└───────────────────┼─────────────────────────────────────┘

                    │

                    │ vm_page_t*

                    ▼

┌─────────────────────────────────────────────────────────┐

│                     vm_page_t                           │ ← 数据页:4096B,仅存一种结构体

│  prev / next / pg_family                                │

│  ┌────────────────────────────────────────────────────┐ │

│  │ block_meta_data_t (页首初始空闲块)                  │ │

│  │  is_free | block_size | offset | priority_node     │ │

│  │  prev_block | next_block                           │ │

│  ├────────────────────────────────────────────────────┤ │

│  │ page_memory[] (用户数据区 + 嵌入式 metadata)         │ │

│  │  [struct][meta][struct][meta][struct][free...]     │ │

│  └────────────────────────────────────────────────────┘ │

└─────────────────────────────────────────────────────────┘

| 概念 | 说明 |

|------|------|

| vm_page_for_families_t | 元数据页,通过柔性数组存储多个 vm_page_family_t 描述符,以链表形式扩展 |

| vm_page_family_t | 页面族描述符,绑定一种结构体类型(名称 + 大小),维护该类型所有数据页的链表和全局空闲块优先链表 |

| vm_page_t | 数据页,从内核 mmap 获取,页头包含双向链表指针、所属 family 指针和首个 block_meta_data_t |

| block_meta_data_t | 块元数据,嵌入在数据页中每个用户数据块的前方,记录空闲状态、大小、页内偏移、物理链表指针和优先链表节点 |

| glue_node_t | 侵入式双向链表节点,用于构建按 block_size 降序排列的空闲块优先链表(Worst-fit) |

1.2 分配流程(xcalloc)


xcalloc("emp_t", 1)

    │

    ▼

look_up_family_by_name("emp_t")

    │

    ▼

mm_allocate_free_data_block(family, struct_size)

    │

    ├── 优先链表头部有 ≥ req_size 的空闲块?

    │       YES → mm_claim_and_split_block()

    │               ├── glue_node_remove()          // 从优先链表摘除

    │               ├── mm_split_free_block_for_allocation()

    │               │       ├── 标记 is_free = false

    │               │       ├── remainder < sizeof(meta)? → 硬碎片,整块返回

    │               │       └── remainder ≥ sizeof(meta)? → 创建尾部空闲块

    │               │               └── mm_bind_blocks_for_allocation()  // 链入物理链表

    │               └── 新空闲块加入优先链表(若产生)

    │

    └── NO → allocate_vm_page(family)

                ├── mmap 新页

                ├── 挂入 family 页面链表

                ├── MARK_VM_PAGE_EMPTY()

                └── mm_claim_and_split_block(页首块, req_size)

1.3 释放流程(xfree)


xfree(ptr)

    │

    ├── ptr == NULL? → return

    │

    ▼

(block_meta_data_t *)ptr - 1   // 反算 metadata 地址

    │

    ▼

assert(!is_free) → is_free = true

    │

    ▼

MM_GET_PAGE_META_BLOCK() → 定位所属 vm_page_t

    │

    ▼

检查前驱/后继是否空闲

    ├── YES → glue_node_remove(邻居.priority_list_node)  // 先从优先链表摘除

    │         更新 free_block_start / free_block_end

    │

    ▼

mm_union_free_blocks(start, end)  // 合并物理块

    │

    ▼

mm_is_vm_page_empty(vm_page)?

    ├── YES → mm_vm_page_delete_and_free()  // 整页归还内核

    │           ├── 从 family 页面链表摘除

    │           └── munmap()

    │

    └── NO  → mm_add_free_block_metadata_to_free_block_list()

                // 按 block_size 降序插入优先链表(O(n))

1.4 碎片模型

在本分配器的固定大小 family 模型下,碎片行为具有确定性:

| 碎片类型 | 定义 | 产生位置 | 能否复用 | 处理方式 |

|---------|------|---------|---------|---------|

| 硬碎片 | 分割后剩余空间 < sizeof(block_meta_data_t) | 仅页面顶部最后一次分配边界 | ❌ | 被吞入前一个已分配块的 block_size,随该块释放自动归还 |

| 软碎片 | sizeof(meta) ≤ 剩余 < struct_size | 仅页面顶部最后一次分配边界 | ❌ | 作为独立空闲块存在于优先链表中,但永远无法满足本 family 分配;整页空闲时随页面回收 |

| 外部碎片 | 分散的空闲块总量足够但不连续 | 页面中间 | ✅ | 通过 xfree 时的双向邻居合并及时消除 |

关键性质:由于每个页面只服务一种固定大小的结构体,且所有分配请求均为 struct_size 的整数倍,页面中间的任何空闲块大小必然是 struct_size 的整数倍,不可能产生不可复用的硬/软碎片。碎片仅可能出现在页面顶部,由 (SYSTEM_PAGE_SIZE - offsetof(vm_page_t, block_meta_data)) mod (struct_size + sizeof(block_meta_data_t)) 决定。


2. 代码结构


HeapMemoryManager/

├── mm.h              # 内部数据结构定义、宏、迭代器

├── uapi_mm.h         # 用户 API 声明(xcalloc/xfree/MM_REG_STRUCT 等)

├── mm.c              # 核心实现

├── testapp.c         # 测试用例

├── CMakeLists.txt    # 构建配置(依赖 GLUE_DS 库)

├── build.sh          # 一键构建脚本

└── ../GLUE_DS/       # 侵入式双向链表库(glnode.h)

2.1 文件职责

| 文件 | 职责 |

|------|------|

| uapi_mm.h | 对外接口层。定义 XCALLOC()MM_REG_STRUCT() 等用户宏,屏蔽内部实现细节 |

| mm.h | 内部数据层。定义 vm_page_tblock_meta_data_tvm_page_family_t 等结构体,以及 ITERATE_* 系列遍历宏和 MARK_VM_PAGE_EMPTYmm_bind_blocks_for_allocation 等操作宏 |

| mm.c | 核心逻辑层。分为以下几组函数: |

| | • 初始化mm_init()mm_instantiate_new_page_family() |

| | • 页面管理allocate_vm_page()mm_vm_page_delete_and_free()mm_get_new_vm_page_from_kernel()mm_return_vm_page_to_kernel() |

| | • 分配事务mm_allocate_free_data_block()mm_claim_and_split_block()mm_split_free_block_for_allocation() |

| | • 释放事务xfree()mm_union_free_blocks()mm_assert_free_from_start_to_end() |

| | • 优先链表mm_add_free_block_metadata_to_free_block_list()free_blocks_comparison() |

| | • 查询/调试look_up_family_by_name()mm_is_vm_page_empty()mm_print_*() 系列 |

| glnode.h | 基础设施层。提供侵入式双向链表的 init/add/remove/container_of/isolated 等操作,不依赖任何动态内存分配 |

2.2 关键宏说明

| 宏 | 用途 |

|----|------|

| MM_REG_STRUCT(type) | 注册结构体类型,展开为 mm_instantiate_new_page_family(#type, sizeof(type)) |

| XCALLOC(n, type) | 分配 n 个 type 对象,展开为 xcalloc(#type, n) |

| MM_GET_PAGE_META_BLOCK(meta) | 通过 block_meta_data_t 的 offset 字段反算所属 vm_page_t 地址 |

| MARK_VM_PAGE_EMPTY(page) | 将新页面的首个块标记为空闲、链表指针置空 |

| mm_bind_blocks_for_allocation(alloc, free) | 分割后将新空闲块链入物理双向链表 |

| ITERATE_PAGE_FAMILIES_BEGIN/END | 遍历一个 vm_page_for_families_t 中的所有有效 family |

| ITERATE_VM_PAGE_BEGIN/END | 遍历一个 family 的所有数据页 |

| ITERATE_VM_PAGE_ALL_BLOCKS_BEGIN/END | 遍历一个页面内的所有物理块 |

| ITERATE_NODES_START/END | 安全遍历 glue_node 双向链表 |


3. 优缺点分析

3.1 优点

| 优点 | 说明 |

|------|------|

| 零外部碎片(页内) | 固定大小 family 保证页内空闲块始终可被同类型分配复用,合并机制及时消除分散 |

| 碎片可控且可预测 | 硬/软碎片仅出现在页面顶部,浪费上限由 struct_size 和页头大小决定,与分配频率无关 |

| Worst-fit O(1) 分配 | 优先链表降序排列,最大空闲块始终在头部,分配查找为常数时间 |

| 整页自动回收 | 页面完全空闲时自动 munmap 归还内核,防止页面级内存泄漏 |

| 类型隔离 | 不同结构体类型使用独立页面,避免混合分配导致的交叉碎片和缓存污染 |

| 侵入式数据结构 | 优先链表节点嵌入 block_meta_data_t,无需额外 malloc/free 管理链表本身 |

| 调试友好 | 内置 mm_print_memory_usage()mm_print_block_usage() 等可视化函数,可直接观察每页每块的状态 |

3.2 缺点

| 缺点 | 说明 | 缓解措施 |

|------|------|---------|

| 释放 O(n) | mm_add_free_block_metadata_to_free_block_list 线性遍历优先链表找插入点,空闲块多时性能退化 | 改用分级自由链表(Segregated Fit)可将释放降为 O(1) |

| 跨页不合并 | 相邻两页即使各剩一个空闲块也无法合并,只能等整页空闲才回收 | 架构固有约束,适合对象生命周期相近的场景;可通过监控碎片率评估影响 |

| 仅支持固定大小 | 不支持可变大小分配,每种新类型需预先注册 | 适用于控制面/协议栈等结构体类型有限的场景 |

| 页面顶部浪费 | (4096 - 页头) mod struct_size 字节不可用,小 struct_size 时浪费比例较高 | 可选择 struct_size 使其更接近 4096 的因数 |

| 非线程安全 | 无任何内置同步机制,多线程并发调用 xcalloc/xfree 会导致数据竞争 | 外层加互斥锁,或按 family 粒度加细粒度锁 |

| family 查找 O(n) | look_up_family_by_name 线性扫描所有已注册类型 | 类型数量少时可接受;数量多时可改用哈希表 |

| mmap 系统调用开销 | 每次申请新页都触发 syscall,高频小对象分配时开销显著 | 可引入页面池预分配机制 |

3.3 适用场景

✅ 适合:网络设备控制面、协议栈消息处理、嵌入式系统、游戏 ECS 组件管理等结构体类型有限、大小固定、分配释放模式可预测的场景。

❌ 不适合:通用 malloc/free 替代、字符串/缓冲区等可变大小分配、高并发无锁场景、对延迟极度敏感的实时系统。


4. 快速开始

4.1 构建


./build.sh          # 构建

./build.sh clean    # 清理

4.2 使用示例


#include "uapi_mm.h"

typedef struct emp_ {

    char name[32];

    uint32_t emp_id;

} emp_t;

int main() {

    mm_init();                          // 初始化(获取系统页大小)

    MM_REG_STRUCT(emp_t);               // 注册结构体类型

    emp_t *e = XCALLOC(1, emp_t);       // 分配 1 个 emp_t

    xfree(e);                           // 释放

    mm_print_block_usage();             // 查看内存使用统计

    return 0;

}

4.3 API 参考

| API | 说明 |

|-----|------|

| mm_init() | 初始化分配器,必须在所有其他操作之前调用 |

| MM_REG_STRUCT(type) | 注册结构体类型,创建对应的页面族 |

| XCALLOC(units, type) | 分配 units 个 type 对象,返回清零的指针,失败返回 NULL |

| xfree(ptr) | 释放由 XCALLOC 返回的指针,支持 NULL |

| mm_print_registered_page_families() | 打印所有已注册的结构体类型 |

| mm_print_memory_usage(name) | 打印指定类型的详细页面/块布局(name="" 打印全部) |

| mm_print_block_usage() | 打印所有类型的块使用统计摘要 |


评论