内存池以及实现

内存池主要解决的问题:

  1. 避免频繁的创建和销毁内存。内存的申请和释放是很低效的。
  2. 避免内存碎片

内存碎片:

当你malloc了16字节的内存,然后释放掉,再malloc了8字节的内存;那么还有8字节内存未被使用,如果再申请4字节内存,那还剩下4字节内存。可以一直划分,直到所剩的内存空间太小而分配不出内存,这样就会有小部分内存空间无法利用,便叫做内存碎片。

工作原理

  1. 预申请大块内存:池子创建时,一次性向系统申请一大块连续内存。
  2. 维护空闲块链表:内存池内部会维护一个数据结构(如链表),记录当前哪些内存块是空闲的、哪些已被占用。
  3. 自定义分配与释放
    • 分配:当程序请求内存时,内存池从空闲链表中取出一块返回,而不调用系统函数。
    • 释放:程序归还内存时,内存池将其放回空闲链表,而不是还给操作系统(除非整个池子被销毁)。

内存池一般分为固定大小的内存池,和大小不固定的内存池。

一般不同的项目需求不同,所以使用的内存池也大不相同。而线程池则具有很强的适配性,各个项目的线程池都相差不大。

固定大小的内存池

一次申请大块的内存。

把整个大块内存分成固定大小的小块。

每一小块的前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);

}
相关推荐
lifejump39 分钟前
双冗余链路实现(2/2期)
网络
F1FJJ1 小时前
Shield CLI PostgreSQL 插件现已上架 VS Code 扩展市场
网络·vscode·网络协议·postgresql·开源软件
123过去2 小时前
responder使用教程
linux·网络·测试工具·安全·哈希算法
不知名。。。。。。。。2 小时前
数据链路层
linux·网络
lpfasd1233 小时前
OpenRouter低延迟使用中国Token算力
网络·token
CQU_JIAKE3 小时前
3.23【A】
linux·服务器·网络
jinanwuhuaguo3 小时前
OpenClaw全网使用人群全景深度分析报告
网络·人工智能·网络协议·rpc·openclaw
忘忧记4 小时前
pytest + YAML + requests`简单实例化
网络·pytest
竹之却4 小时前
如何使用 SakuraFrp 做内网穿透
运维·服务器·网络·frp·内网穿透·sakurafrp
不一样的故事1264 小时前
抓重点、留弹性、重节奏
大数据·网络·人工智能·安全