【C/C++】手写内存池(四):内存池的重置、销毁与完整生命周期
前面已经介绍了小块内存的顺序分配、内存对齐、节点扩容以及大块内存的单独管理。一个完整的内存池还需要解决两个问题一批数据使用结束后如何快速重新使用内存池 程序结束时如何完整释放内存池中的所有资源对应的操作分别是mp_reset_pool()重置内存池保留已经申请的内存节点 mp_destroy_pool()销毁内存池彻底释放所有内存本文将结合mp_calloc()、mp_reset_pool()和mp_destroy_pool()完成整个内存池生命周期的分析。一、重置内存池和销毁内存池有什么区别内存池中的小块内存采用顺序分配方式内存起始位置 ↓ ---------------------------------------------- | 已分配A | 已分配B | 已分配C | 剩余空间 | ---------------------------------------------- ↑ ↑ last end每次申请内存时last都会向后移动node-last memory size;小块内存通常不支持逐个释放。如果一批任务全部完成可以直接将所有节点的last恢复到初始位置。这就是重置内存池重置前 ------------------------------------ | 已使用内存 | 剩余空间 | ------------------------------------ ↑ ↑ last end 重置后 -------------------------------------- | 全部可用空间 | -------------------------------------- ↑ ↑ last end重置操作不会释放小块内存节点而是将它们恢复为可重新分配的状态。销毁内存池则完全不同重置内存池 保留内存节点只清除使用状态可以继续分配。 销毁内存池 释放所有大块内存、扩展节点和内存池本身 销毁后不能继续使用。因此二者适用于不同场景同一个内存池需要重复使用 调用 mp_reset_pool() 内存池以后不再使用 调用 mp_destroy_pool()例如服务器在处理每一个请求时都需要申请许多临时对象可以为每个请求复用同一个内存池开始处理请求1 ↓ 从内存池申请临时数据 ↓ 请求1处理完成 ↓ 重置内存池 ↓ 开始处理请求2与逐个调用free()相比重置内存池只需要恢复几个指针效率更高。二、mp_calloc 如何申请并清零内存普通的mp_alloc()只负责申请内存不保证返回空间中的数据为零。void *data mp_alloc(pool, 128);这块内存可能以前被使用过因此其中可能仍然保留旧数据。如果需要得到一块已经清零的内存可以实现mp_calloc()/** * 从内存池申请一块内存并将其初始化为0 * * pool内存池对象 * size需要申请的字节数 * * 返回值 * 成功时返回内存地址失败时返回NULL */ void *mp_calloc(struct mp_pool_s *pool, size_t size) { // 先通过普通分配函数申请内存 void *ptr mp_alloc(pool, size); if (ptr ! NULL) { // 将申请到的size字节全部设置为0 memset(ptr, 0, size); } return ptr; }它的执行过程为调用 mp_alloc() ↓ 获得一块可用内存 ↓ 调用 memset() 清零 ↓ 返回初始化后的内存使用方式如下// 申请128字节并将所有内容初始化为0 unsigned char *buffer mp_calloc(pool, 128); if (buffer NULL) { printf(内存申请失败\n); return; }也可以用于创建并初始化结构体typedef struct user_s { int id; int age; char name[32]; } user_t; // 申请一个user_t对象并将所有成员清零 user_t *user mp_calloc(pool, sizeof(user_t)); if (user ! NULL) { user-id 1001; user-age 24; }此时name数组中的内容默认全部为0不需要再单独初始化。mp_alloc()和mp_calloc()的区别可以总结为mp_alloc 只申请内存原有内容不确定速度相对更快。 mp_calloc 申请内存后调用memset清零适合需要初始化的数据。需要注意mp_calloc()中的calloc只是表示“申请并清零”内存最终仍然由内存池统一管理不能直接对小块内存调用free()。三、mp_reset_pool 如何重置整个内存池重置内存池需要完成以下工作释放所有大块内存 ↓ 清空大块内存记录入口 ↓ 恢复每个小块节点的last指针 ↓ 清空节点分配失败次数 ↓ 让current重新指向第一个节点实现代码如下/** * 重置内存池 * * 重置后 * 1. 所有小块内存重新变为可分配状态 * 2. 所有大块内存都会被释放 * 3. 已经创建的小块内存节点会被保留 * 4. 内存池可以继续使用。 */ void mp_reset_pool(struct mp_pool_s *pool) { if (pool NULL) { return; } struct mp_large_s *large; struct mp_node_s *node; /* * 第一步释放所有仍然存在的大块内存。 * * large管理节点本身位于小块内存池中 * 因此这里只释放large-alloc指向的实际内存。 */ for (large pool-large; large ! NULL; large large-next) { if (large-alloc ! NULL) { free(large-alloc); large-alloc NULL; } } /* * 重置小块区域后原来的large管理记录也会失效 * 因此将大块内存链表入口清空。 */ pool-large NULL; /* * 第二步恢复所有小块内存节点的分配位置。 * * 每个节点开头保存mp_node_s管理结构 * 真正的可分配空间位于管理结构之后。 */ for (node pool-head; node ! NULL; node node-next) { node-last (unsigned char *)node sizeof(struct mp_node_s); // 清空当前节点的分配失败次数 node-failed 0; } /* * 第三步重新从第一个节点开始分配。 */ pool-current pool-head; }假设内存池中有三个节点pool-head | v node1 → node2 → node3 → NULL重置之前每个节点可能已经使用了一部分空间node1last接近end node2last位于中间 node3last刚刚开始移动执行mp_reset_pool(pool);以后三个节点都会恢复node1last恢复到管理结构之后 node2last恢复到管理结构之后 node3last恢复到管理结构之后节点本身并没有被释放因此下一次分配可以直接重复使用这些内存不需要再次调用posix_memalign()创建节点。这也是重置操作效率较高的原因。重置后原来的指针还能使用吗不能。例如char *name mp_alloc(pool, 64); strcpy(name, memory pool); mp_reset_pool(pool); // 错误name指向的空间已经重新归内存池管理 printf(%s\n, name);虽然name保存的地址没有立刻改变但该地址已经可以被下一次内存申请重新使用。例如char *name mp_alloc(pool, 64); mp_reset_pool(pool); char *message mp_alloc(pool, 64);此时message很可能与name指向同一位置name 0x1000 message 0x1000因此调用mp_reset_pool()后之前从该内存池中获得的所有指针都应该视为失效。四、mp_destroy_pool 如何彻底释放资源销毁内存池时需要释放三类资源1. 通过malloc或posix_memalign单独申请的大块内存 2. 内存池扩容时创建的额外小块节点 3. 创建内存池时申请的首块内存。内存池结构可以表示为pool ├── 首个节点head │ ├── node2 │ ├── node3 │ └── large链表 ├── large1 → 大块内存1 └── large2 → 大块内存2销毁函数如下/** * 销毁内存池并释放全部资源 * * 调用后pool以及从pool中申请的所有地址都会失效。 */ void mp_destroy_pool(struct mp_pool_s *pool) { if (pool NULL) { return; } struct mp_large_s *large; struct mp_node_s *node; struct mp_node_s *next; /* * 第一步释放所有尚未手动释放的大块内存。 */ for (large pool-large; large ! NULL; large large-next) { if (large-alloc ! NULL) { free(large-alloc); large-alloc NULL; } } /* * 第二步释放扩容时创建的额外节点。 * * 第一个节点head与pool位于同一次内存申请中 * 因此不能在这里单独释放head。 */ node pool-head-next; while (node ! NULL) { next node-next; free(node); node next; } /* * 第三步释放内存池本身。 * * pool和第一个节点head属于同一块内存 * free(pool)会同时释放二者。 */ free(pool); }为什么从下面的位置开始释放节点node pool-head-next;而不是node pool-head;原因是第一个节点与内存池对象是在同一次内存申请中创建的posix_memalign( (void **)pool, MP_ALIGNMENT, size sizeof(struct mp_pool_s) sizeof(struct mp_node_s) );内存布局为---------------------------------------------------- | mp_pool_s | 第一个mp_node_s | 可分配空间 | ---------------------------------------------------- ↑ pool因此第一个节点不能单独调用free(pool-head);否则会释放一个并非独立申请的地址造成未定义行为。后续扩展节点则是单独申请的posix_memalign( (void **)memory, MP_ALIGNMENT, block_size );它们可以逐个调用free()。正确销毁顺序是释放所有大块内存 ↓ 释放node2、node3等额外节点 ↓ 释放pool及其内部的第一个节点在部分代码中销毁函数可能被命名为mp_destory_pool()其中destory是拼写错误更规范的命名应该是mp_destroy_pool()实际使用时函数声明、函数定义和调用处保持一致即可。五、完整使用流程与注意事项下面通过一个完整示例演示内存池的创建、分配、重置、复用和销毁过程。#include stdio.h #include stdlib.h #include string.h int main(void) { /* * 创建一个4096字节的内存池。 */ struct mp_pool_s *pool mp_create_pool(4096); if (pool NULL) { printf(内存池创建失败\n); return -1; } /* * 第一阶段申请小块内存。 */ char *name mp_alloc(pool, 64); if (name ! NULL) { strcpy(name, memory pool); printf(name %s\n, name); } /* * 申请并清零一个整数数组。 */ int *numbers mp_calloc( pool, sizeof(int) * 10 ); if (numbers ! NULL) { for (int i 0; i 10; i) { printf(%d , numbers[i]); } printf(\n); } /* * 申请一块大内存。 * 如果大小超过pool-max * 内部会调用malloc单独申请。 */ void *large_buffer mp_alloc(pool, 8192); if (large_buffer ! NULL) { printf( large buffer %p\n, large_buffer ); } /* * 大块内存支持单独释放。 */ mp_free(pool, large_buffer); /* * 第一批数据使用完毕重置内存池。 * * 重置后name和numbers都不能继续使用。 */ mp_reset_pool(pool); /* * 第二阶段复用已经创建的内存节点。 */ char *message mp_calloc(pool, 128); if (message ! NULL) { strcpy(message, pool reused successfully); printf(message %s\n, message); } /* * 内存池不再使用彻底释放所有资源。 */ mp_destroy_pool(pool); /* * 销毁后不能继续访问pool、message等地址。 */ pool NULL; message NULL; return 0; }完整生命周期可以概括为创建内存池 mp_create_pool() ↓ 申请小块或大块内存 mp_alloc() / mp_calloc() ↓ 大块内存可以单独释放 mp_free() ↓ 一批数据使用完成 mp_reset_pool() ↓ 重复使用已有内存节点 ↓ 程序不再需要内存池 mp_destroy_pool()使用过程中需要注意以下问题1. mp_reset_pool不会释放小块节点而是恢复分配位置 2. 重置后之前申请的所有小块内存地址都不能继续使用 3. 重置时尚未手动释放的大块内存也会被释放 4. mp_free只用于释放large链表管理的大块内存 5. 第一个节点与pool属于同一次内存申请不能单独free 6. 额外扩展节点需要在销毁时逐个释放 7. 内存池销毁后所有从池中获得的地址都会失效 8. 当前实现没有同步保护不支持多线程同时分配。在创建内存池时还应确保第一个节点的成员被完整初始化pool-head-next NULL; pool-head-failed 0;重置内存池时也建议恢复pool-current pool-head; node-failed 0;避免分配器继续使用重置前的查找状态。至此一个基础内存池的主要内容已经完整包括固定大小内存块与空闲链表 小块内存的顺序分配与地址对齐 大块内存的记录与单独释放 内存池的清零、重置和彻底销毁。0voice · GitHub