内存池主要解决的问题:
- 避免频繁的创建和销毁内存。内存的申请和释放是很低效的。
- 避免内存碎片
内存碎片:
当你malloc了16字节的内存,然后释放掉,再malloc了8字节的内存;那么还有8字节内存未被使用,如果再申请4字节内存,那还剩下4字节内存。可以一直划分,直到所剩的内存空间太小而分配不出内存,这样就会有小部分内存空间无法利用,便叫做内存碎片。
工作原理
- 预申请大块内存:池子创建时,一次性向系统申请一大块连续内存。
- 维护空闲块链表:内存池内部会维护一个数据结构(如链表),记录当前哪些内存块是空闲的、哪些已被占用。
- 自定义分配与释放 :
- 分配:当程序请求内存时,内存池从空闲链表中取出一块返回,而不调用系统函数。
- 释放:程序归还内存时,内存池将其放回空闲链表,而不是还给操作系统(除非整个池子被销毁)。
内存池一般分为固定大小的内存池,和大小不固定的内存池。
一般不同的项目需求不同,所以使用的内存池也大不相同。而线程池则具有很强的适配性,各个项目的线程池都相差不大。
固定大小的内存池
一次申请大块的内存。
把整个大块内存分成固定大小的小块。
每一小块的前8个字节(一个指针8字节)存放下一个空闲小块的地址。
这样可以重复利用已经申请然后释放后的内存。
结构体设计
typedef struct mempool_s {
int block_size; // 每个块的大小
int free_count; // 空闲块数量
char *free_ptr; // 指向第一个空闲块
char *mem; // 指向实际分配的内存起始地址
} mempool_t;
char *ptr = m->free_ptr;
for (i = 0;i < m->free_count;i ++) {
*(char **)ptr = ptr + size;
ptr += size;
}
*(char **)ptr = NULL;
指针占用大小8字节
char *ptr = m->free_ptr; 表示 ptr中就是 m->free_ptr的地址
char ** ptr 表示 m->free_ptr前8个字节的地址
*(char **)ptr 表示 m->free_ptr前8个字节
*(char **)ptr = ptr + size; 表示 m->free_ptr前8个字节存放,下一个小块的地址
这段代码表示将每一个小块用指针连接起来。
void mp_free(mempool_t *m, void *ptr) {
*(char **)ptr = m->free_ptr; // 要释放的小块 指向原来的空闲块
m->free_ptr = (char *)ptr; // 空闲块指向这个要释放的小块
m->free_count ++;
}
那这个要释放的小块内存为什么不清零?如果直接往里面放数据会不会冲突?
不需要,用户申请的时候有责任清零。把清零任务交给用户。
只需要更新一下指针。
如果申请的这页内存用完后MEM_PAGE_SIZE,和下一页之间是不是也没有关联?
没关联。
// 固定大小块内存池
#include <stdio.h>
#include <stdlib.h>
//
#define MEM_PAGE_SIZE 4096
typedef struct mempool_s {
int block_size; // 每个块的大小
int free_count; // 空闲块数量
char *free_ptr; // 指向第一个空闲块
char *mem; // 指向实际分配的内存起始地址
} mempool_t;
int mp_init(mempool_t *m, int size) {
if (!m) return -1;
if (size < 16) size = 16; // 设置最小块16字节
m->block_size = size;
m->mem = (char *)malloc(MEM_PAGE_SIZE);
if (!m->mem) return -1;
m->free_ptr = m->mem;
m->free_count = MEM_PAGE_SIZE / size;
int i = 0;
char *ptr = m->free_ptr;
for (i = 0;i < m->free_count;i ++) {
*(char **)ptr = ptr + size;
ptr += size;
}
*(char **)ptr = NULL;
return 0;
}
void mp_dest(mempool_t *m) {
if (!m || !m->mem) return ;
free(m->mem); // 直接释放整块内存
}
void *mp_alloc(mempool_t *m) {
if (!m || m->free_count == 0) return NULL;
void *ptr = m->free_ptr;
// *(char **)ptr 表示m->free_ptr前8个字节,即下一小块的地址
m->free_ptr = *(char **)ptr; // 指向下一个空闲块
m->free_count --;
return ptr;
}
/*
问:那这个要释放的小块内存需要清零吗?
如果直接往里面放数据会不会冲突?
不需要,也不会。
把清零任务交给用户。只需要更新一下指针。
如果申请的这页内存用完后MEM_PAGE_SIZE,和下一页之间是不是也没有关联?
没有关联。
*/
void mp_free(mempool_t *m, void *ptr) {
*(char **)ptr = m->free_ptr; // 要释放的小块 指向原来的空闲块
m->free_ptr = (char *)ptr; // 空闲块指向这个要释放的小块
m->free_count ++;
}
int mp_get_count(mempool_t *m) {
return m->free_count;
}
int main() {
mempool_t m;
mp_init(&m, 32); // 每一小块内存32B
void *p1 = mp_alloc(&m);
printf("1: mp_alloc: %p\n", p1);
void *p2 = mp_alloc(&m);
printf("2: mp_alloc: %p\n", p2);
void *p3 = mp_alloc(&m);
printf("3: mp_alloc: %p\n", p3);
void *p4 = mp_alloc(&m);
printf("4: mp_alloc: %p\n", p4);
mp_free(&m, p2);
void *p5 = mp_alloc(&m);
printf("5: mp_alloc: %p\n", p5);
return 0;
}
可变大小快内存池
每一个节点具有不同大小的内存,然后再用一个结构体管理所有的节点。
typedef struct mp_node_s {
char *free_ptr; // 当前节点的空闲位置指针
char *end; // 当前节点的结束位置
struct mp_node_s *next; // 指向下一个节点
} mp_node_t;
typedef struct mp_pool_s {
struct mp_node_s *first; // 第一个节点
struct mp_node_s *current; // 当前使用的节点
int max; // 每个节点的大小
} mp_pool_t;
#include <stdio.h>
#include <stdlib.h>
#define MEM_PAGE_SIZE 4096
typedef struct mp_node_s {
char *free_ptr; // 当前节点的空闲位置指针
char *end; // 当前节点的结束位置
struct mp_node_s *next; // 指向下一个节点
} mp_node_t;
typedef struct mp_pool_s {
struct mp_node_s *first; // 第一个节点
struct mp_node_s *current; // 当前使用的节点
int max; // 每个节点的大小
} mp_pool_t;
int mp_init(mp_pool_t *m, int size) {
if (!m) return -1;
void *addr = malloc(size); // 4096
mp_node_t *node = (mp_node_t*)addr;
// 空闲起始位置,跳过节点头结构体
node->free_ptr = (char*)addr + sizeof(mp_node_t);
node->end = (char*)addr + size; // 结束位置
node->next = NULL;
m->first = node;
m->current = node;
m->max = size;
return 0;
}
void mp_dest(mp_pool_t *m) {
if (!m) return ;
while (m->first) {
void *addr = m->first;
mp_node_t *node = (mp_node_t*)addr;
m->first = node->next;
free(addr);
}
return ;
}
void *mp_alloc(mp_pool_t *m, int size) { //size < (4096-sizeof(mp_node_t))
void *addr = m->current;
mp_node_t *node = (mp_node_t*)addr; // 取出当前节点的头部结构体
do {
if (size <= (node->end - node->free_ptr)) { // 当前节点内存足够
char *ptr = node->free_ptr;
node->free_ptr += size;
return ptr;
}
node = node->next; // 当前节点内存不够,换下一个节点
} while (node);
// new node
// 现有的节点内存都不够 ,申请一个新节点
addr = malloc(m->max); // 4096
node = (mp_node_t*)addr;
node->free_ptr = (char*)addr + sizeof(mp_node_t);
node->end = (char*)addr + m->max;
// 头插法
node->next = m->current; // 插入到current之前,而不是current的next
m->current = node;
char *ptr = node->free_ptr;
node->free_ptr += size;
return ptr;
}
void mp_free(mp_pool_t *m, void *ptr) {
}
int main() {
mp_pool_t m;
mp_init(&m, MEM_PAGE_SIZE);
void *p1 = mp_alloc(&m, 16);
printf("1: mp_alloc: %p\n", p1);
void *p2 = mp_alloc(&m, 32);
printf("2: mp_alloc: %p\n", p2);
void *p3 = mp_alloc(&m, 64);
printf("3: mp_alloc: %p\n", p3);
void *p4 = mp_alloc(&m, 128);
printf("4: mp_alloc: %p\n", p4);
void *p5 = mp_alloc(&m, 256);
printf("5: mp_alloc: %p\n", p5);
mp_dest(&m);
}