内存池的实现

内存池的实现

1、使用内存池的原因

开源组件:tcmalloc/jemalloc

内存池是对堆上的内存池进行管理。服务器长时间运行,会产生莫名其妙的问题,大多数都是内存问题 。

为什么需要对内存管理?

  • 避免频繁的系统调用带来的开销。
  • 减少了频繁分配和释放小块内存产生的内存碎片。

解决上述问题,最好的方法就是内存池。

2、内存池的工作流程

内存池的具体做法是固定大小、提前申请、重复利用。

  • 固定大小。在调用内存分配函数的时候,小块内存每次都分配固定大小的内存块,这样避免了内存碎片产生的可能性。
  • 提前申请一块大的内存,内存不够用时再二次分配,减少了malloc的次数,提高了效率。

本文参考 nginx 内存池的设计,实现一个简易版的内存池,工作流程如图所示

在这里插入图片描述

3、内存池的实现

3.1、数据结构设计

在这里插入图片描述

3.1.1、内存池

管理整个内存池的头部信息,包括内存池结点(小块内存)链表,大块内存链表。

typedef struct mp_pool_s {
	size_t max;			// 小块内存能分配的最大空间,超过该值使用大块内存分配
	mp_node_t *current;		     // 指向当前内存池
	struct mp_large_s *large;	 // 指向大块内存链表
	mp_node_t head[0];			// 指向小块内存链表
} mp_pool_t;

两个链表为真正的内存分配的地方。在分配内存的时候,根据内存池设定的max值,判断当前要分配的内存块是小块内存还是大块内存。小块内存直接在内存池内 mp_node_s 分配,大块内存向 OS 申请 mp_large_s

3.1.2、小块内存

内存池结点,用于小块内存的分配。

typedef struct mp_node_s {
	unsigned char *last;  // 指向内存池结点已分配的末尾地址,下一次内存分配的起始地址
	unsigned char *end;	  // 指向内存池结点的末尾地址
	struct mp_node_s *next;	 // 指向下一个内存池结点
	size_t failed;			// 当前内存块分配空间失败的次数
} mp_node_t;

对于小块内存的分配,先尝试从内存池的当前结点(p->current)的剩余空间分配。

  • 若空间足够,则返回last的地址作为内存分配的起始地址,并更新 last = last + size
  • 若空间不足,则创建一个新的mp_node_s结点,并更新 p->next,返回 last 的地址作为内存分配的起始地址,并更新 last = last + size

在这里插入图片描述

3.1.3、大块内存
struct mp_large_s {
	struct mp_large_s *next; // 指向下一个大块内存,所有大块内存通过链表串联起来
	void *alloc;		    // 指向实际分配的大块内存
};

3.2、接口设计

3.2.1、创建内存池

申请的内存由两部分组成,一部分用来容纳mp_pool_s结构体,另一部分内存则是用于满足用户申请。last指针和end指针之间的内存是空闲的,当用户申请小块内存时,如果空闲的内存大小满足用户的需求,则可以分配给用户

// 内存对齐,以 alignmen 为单位,如:以4B对齐,17->20, 30->32
#define mp_align(n, alignment) (((n)+(alignment-1)) & ~(alignment-1))
#define mp_align_ptr(p, alignment) (void *)((((size_t)p)+(alignment-1)) & ~(alignment-1))
mp_pool_t *mp_create_pool(size_t size) {
	mp_pool_t *p;

	// posix_memalign申请空间,参数:首地址,对齐方式,大小。
	// 空间大小:申请的空间 + 内存池管理信息(mp_pool_t) + 内存池结点(mp_node_t)
	int ret = posix_memalign((void **)&p, MP_ALIGNMENT, size + sizeof(mp_pool_t) + sizeof(mp_node_t));
	if (ret) {
		return NULL;
	}
	// 设置小块内存能分配的最大空间
	p->max = (size < MP_MAX_ALLOC_FROM_POOL) ? size : MP_MAX_ALLOC_FROM_POOL;
	// 设置当前指向的内存池结点
	p->current = p->head;
	// 设置大块内存链表
	p->large = NULL;
	
	// 设置内存池结点信息 
	p->head->last = (unsigned char *)p + sizeof(mp_pool_t) + sizeof(mp_node_t);
	p->head->end = p->head->last + size;
	p->head->failed = 0;

	return p;
}
3.2.2、内存分配
小块内存分配

用户可以调用mp_alloc()向内存池申请内存。分配内存的时候,根据本次申请空间的大小 size 和内存池设定的pool->max,判断当前要分配的内存块是小块内存还是大块内存。

如果用户申请的是小块内存,遍历现有的小块内存链表,寻找是否有满足需求的内存块

  • 有可分配的内存块,返回待分配空间的首地址
  • 没有可分配的内存块,创建一个新的小块内存
void *mp_alloc(mp_pool_t *pool, size_t size) {
	unsigned char *m;
	mp_node_t *p;

	// 1、用户申请的是小块内存
	if (size <= pool->max) {
		// 指向第一个要遍历的内存池结点
		p = pool->current;

		// 遍历小块内存链表,寻找可用的空间分配内存
		do {
			// 指针内存对齐	
			m = mp_align_ptr(p->last, MP_ALIGNMENT);
			// 当前结点的剩余空间足够分配
			if ((size_t)(p->end - m) >= size) {
				// 更新last指针
				p->last = m + size;
				return m;
			}
			// 若当前内存块的剩余内存小于所需内存,则到下一个内存块中寻找
			p = p->next;
		} while (p);
        
		// 没找到合适的小块内存,则创建一个新的小块内存
		return mp_alloc_block(pool, size);
	}

	// 2、用户申请的是大块内存
	return mp_alloc_large(pool, size);
}

mp_alloc_block用来创建新的小块内存,并插入到小块内存链表的末尾。

每次调用mp_alloc_block函数,代表内存池现有小块内存的空间分配失败,此时,所有可用的小块内存的 failed + 1,表示不满足用户的需求增加1次。 由于采取的是尾插法,所以链表中结点的 failed 计数值依次递减。若某个小块内存若连续 5 次不满足用户需求,则不再使用它,遍历时跳过。

static void *mp_alloc_block(mp_pool_t *pool, size_t size) {
	unsigned char *m;
	mp_node_t *p, *new_node, *current;

	mp_node_t *h = pool->head;
	
	// 计算第一个内存块中共计可分配内存的大小
	size_t psize = (size_t)(h->end - (unsigned char *)h);
	
	// 申请新的内存块(和第一个内存块大小相同)
	int ret = posix_memalign((void **)&m, MP_ALIGNMENT, psize);
	if (ret == NULL) return NULL;

	// 初始化内存块
	new_node = (mp_node_t*)m;
	new_node->end = m + psize;
	new_node->next = NULL;
	new_node->failed = 0;

	// 将指针m移动到可分配内存的开始位置
	m += sizeof(mp_node_t);
	// 对指针做内存对齐
	m = mp_align_ptr(m, MP_ALIGNMENT);
	// 设置新内存块的last
	new_node->last = m + size;

	// 当前指向的内存池结点(该结点及后面结点都可分配内存,前面结点无法分配)
	current = pool->current;
	// 寻找最后一个链表结点,结点的查找从 current 开始,不需要每次从头开始查找
	for (p = current; p->next; p = p->next) {
		// 所有小块内存的 failed+1,表示不满足用户的需求 +1 次
		// 若某个小块内存若连续 5 次都不满足用户需求,则跳过这个小块内存,不再遍历它
		if (p->failed++ > 4) { 
			current = p->next;	// 调整 current 指向下一个内存块
		}
	}
	// 将新创建的内存块,尾插到小块内存链表
	p->next = new_node;
	// 更新pool->current指针
	pool->current = current ? current : new_node;

	return m;
}
大块内存分配

内存池mp_pool_t的成员large指向大块内存链表。如果用户申请的是大块内存,那么内存池就会调用mp_alloc_large()分配一个新的大块内存,插入到链表的开头,头插法。

static void *mp_alloc_large(mp_pool_t *pool, size_t size) {
	// 调用 malloc 申请大块内存
	void *p = malloc(size);
	if (p == NULL) return NULL;

	size_t n = 0;
	struct mp_large_s *large;

    // 遍历大块内存链表,找到可以挂载 p 的位置
	for (large = pool->large; large; large = large->next) {
		// 找到可以挂载的地方
		if (large->alloc == NULL) {
			large->alloc = p;
			return p;
		}
		// 若连续 4 次都没找到,就不再寻找了
		if (n ++ > 3) break;
	}

	// 创建一个新的大块内存结点,用来挂载p
	large = mp_alloc(pool, sizeof(struct mp_large_s));
	if (large == NULL) {
		free(p);
		return NULL;
	}

	// 将新创建的结点,头插到大块内存链表中
	large->alloc = p;
	large->next = pool->large;
	pool->large = large;

	return p;
}
3.2.3、内存池释放

Nginx 内存池内部仅提供大块内存的释放接口

void mp_free(mp_pool_t *pool, void *p) {
	struct mp_large_s *l;
	//遍历大块内存块
	for (l = pool->large; l; l = l->next) {
		if (p == l->alloc) {
			free(l->alloc);
			l->alloc = NULL;
			return ;
		}
	}
}
3.2.4、内存池销毁
void mp_destory_pool(mp_pool_t *pool) {

	mp_node_t *h, *n;
	struct mp_large_s *l;

	// 释放大块内存
	for (l = pool->large; l; l = l->next) {
		if (l->alloc) {
			free(l->alloc);
		}
	}

	// 释放小块内存
	h = pool->head->next;
	while (h) {
		n = h->next;
		free(h);
		h = n;
	}

	// 释放内存池
	free(pool);
}

3.3、代码实现

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <fcntl.h>

#define MP_ALIGNMENT       		32
#define MP_PAGE_SIZE			4096
#define MP_MAX_ALLOC_FROM_POOL	(MP_PAGE_SIZE-1)

// 内存对齐,以 alignmen 为单位,如:17->20, 30->32
#define mp_align(n, alignment) (((n)+(alignment-1)) & ~(alignment-1))
#define mp_align_ptr(p, alignment) (void *)((((size_t)p)+(alignment-1)) & ~(alignment-1))

// 大块内存
typedef struct mp_large_s {
	struct mp_large_s *next; // 指向大块内存链表的下一个大块内存
	void *alloc;			 // 指向实际分配的大块内存
} mp_large_t;

// 内存池结点:小块内存
typedef struct mp_node_s {
	unsigned char *last;	// 指向内存池结点已分配的末尾地址,下一次内存分配的起始地址
	unsigned char *end;		// 指向内存池结点的末尾地址
	
	struct mp_node_s *next;	// 指向下一个内存池结点
	size_t failed;			// 当前内存块分配空间失败的次数
} mp_node_t;

// 内存池
typedef struct mp_pool_s {
	size_t max;					// 小块内存能分配的最大空间,超过该值使用大块内存分配
	mp_node_t *current;	// 指向当前内存池
	struct mp_large_s *large;	// 指向大块内存链表
	mp_node_t head[0];	// 指向小块内存链表
} mp_pool_t;

mp_pool_t *mp_create_pool(size_t size);
void mp_destory_pool(mp_pool_t *pool);
void *mp_alloc(mp_pool_t *pool, size_t size);
void *mp_nalloc(mp_pool_t *pool, size_t size);
void *mp_calloc(mp_pool_t *pool, size_t size);
void mp_free(mp_pool_t *pool, void *p);

// 创建内存池
mp_pool_t *mp_create_pool(size_t size) {
	mp_pool_t *p;

	// posix_memalign申请空间,参数:首地址,对齐方式,大小。malloc无法分配4k空间
	// 空间大小:申请的空间 + 内存池管理信息(mp_pool_s) + 内存池结点(mp_node_s)
	int ret = posix_memalign((void **)&p, MP_ALIGNMENT, size + sizeof(mp_pool_t) + sizeof(mp_node_t));
	if (ret) {
		return NULL;
	}
	// 设置小块内存能分配的最大空间
	p->max = (size < MP_MAX_ALLOC_FROM_POOL) ? size : MP_MAX_ALLOC_FROM_POOL;
	// 设置当前指向的内存池结点
	p->current = p->head;
	// 设置大块内存链表
	p->large = NULL;
	
	// 设置内存池结点信息 
	p->head->last = (unsigned char *)p + sizeof(mp_pool_t) + sizeof(mp_node_t);
	p->head->end = p->head->last + size;
	p->head->failed = 0;

	return p;
}

// 内存池销毁
void mp_destory_pool(mp_pool_t *pool) {

	mp_node_t *h, *n;
	struct mp_large_s *l;

	// 释放大块内存
	for (l = pool->large; l; l = l->next) {
		if (l->alloc) {
			free(l->alloc);
		}
	}

	// 释放小块内存
	h = pool->head->next;
	while (h) {
		n = h->next;
		free(h);
		h = n;
	}

	// 释放内存池
	free(pool);
}

// 内存池重置
void mp_reset_pool(mp_pool_t *pool) {
	mp_node_t *h;
	struct mp_large_s *l;

	// 释放大块内存
	for (l = pool->large; l; l = l->next) {
		if (l->alloc) {
			free(l->alloc);
		}
	}
	pool->large = NULL;

	//重置小块内存,并不调用free将内存交还给系统,只是指针的复位操作。
	for (h = pool->head; h; h = h->next) {
		h->last = (unsigned char *)h + sizeof(mp_node_t);
	}
}

// 小块内存分配
static void *mp_alloc_block(mp_pool_t *pool, size_t size) {
	unsigned char *m;
	mp_node_t *p, *new_node, *current;

	mp_node_t *h = pool->head;
	
	// 计算第一个内存块中总共可分配内存的大小
	size_t psize = (size_t)(h->end - (unsigned char *)h);
	
	// 申请新的内存块(和第一个内存块大小相同)
	int ret = posix_memalign((void **)&m, MP_ALIGNMENT, psize);
	if (ret) return NULL;

	// 初始化内存块
	new_node = (mp_node_t*)m;
	new_node->end = m + psize;
	new_node->next = NULL;
	new_node->failed = 0;

	// 将指针m移动到可分配内存的开始位置
	m += sizeof(mp_node_t);
	// 对指针做内存对齐
	m = mp_align_ptr(m, MP_ALIGNMENT);
	// 设置新内存块的last
	new_node->last = m + size;

	// 当前指向的内存池结点(该结点及后面结点都可分配内存,前面结点无法分配)
	current = pool->current;
	// 寻找最后一个链表结点,结点的查找从 current 开始,不需要每次从头开始查找
	for (p = current; p->next; p = p->next) {
		// 每次调用mp_alloc_block函数,代表之前所有小块内存空间分配失败
		// 此时,所有小块内存的 failed+1,表示不满足用户的需求 +1 次
		// 若某个小块内存若连续 5 次都不满足用户需求,则跳过这个小块内存,下次不再遍历它
		// 由于链表结点的插入是尾插法,所以先插入的结点内存分配失败次数多
		if (p->failed++ > 4) { 
			current = p->next;	// 调整 current 指向下一个内存块
		}
	}
	// 将新创建的内存块,尾插到小块内存链表
	p->next = new_node;
	// 更新pool->current指针,判断 current 是否为空?
	// 若非空,指向current指向的结点;若为空,代表之前所有的内存块都分配失败,则指向新的内存块
	pool->current = current ? current : new_node;

	return m;
}

// 分配大块内存
static void *mp_alloc_large(mp_pool_t *pool, size_t size) {
	// 调用 malloc 申请大块内存
	void *p = malloc(size);
	if (p == NULL) return NULL;

	size_t n = 0;
	struct mp_large_s *large;

    // 遍历大块内存链表,找到可以挂载 p 的位置
	for (large = pool->large; large; large = large->next) {
		// 找到可以挂载的地方
		if (large->alloc == NULL) {
			large->alloc = p;
			return p;
		}
		// 若连续 4 次都没找到,就不再寻找了
		if (n ++ > 3) break;
	}

	// 创建一个新的大块内存结点,用来挂载p
	large = mp_alloc(pool, sizeof(struct mp_large_s));
	if (large == NULL) {
		free(p);
		return NULL;
	}

	// 将新创建的结点,头插到大块内存链表中
	large->alloc = p;
	large->next = pool->large;
	pool->large = large;

	return p;
}

void *mp_memalign(mp_pool_t *pool, size_t size, size_t alignment) {

	void *p;
	
	int ret = posix_memalign(&p, alignment, size);
	if (ret) {
		return NULL;
	}

	struct mp_large_s *large = mp_alloc(pool, sizeof(struct mp_large_s));
	if (large == NULL) {
		free(p);
		return NULL;
	}

	large->alloc = p;
	large->next = pool->large;
	pool->large = large;

	return p;
}

// 内存分配
void *mp_alloc(mp_pool_t *pool, size_t size) {
	unsigned char *m;
	mp_node_t *p;

	// 1、用户申请的是小块内存
	if (size <= pool->max) {
		// 指向第一个要遍历的内存池结点
		p = pool->current;

		// 遍历小块内存链表,寻找可用的空间分配内存
		do {
			// 指针内存对齐	
			m = mp_align_ptr(p->last, MP_ALIGNMENT);
			// 当前结点的剩余空间足够分配
			if ((size_t)(p->end - m) >= size) {
				// 更新last指针
				p->last = m + size;
				return m;
			}
			// 若当前内存块的剩余内存小于所需内存,则到下一个内存块中寻找
			p = p->next;
		} while (p);

		// 没找到合适的小块内存,则创建一个新的小块内存
		return mp_alloc_block(pool, size);
	}

	// 2、用户申请的是大块内存
	return mp_alloc_large(pool, size);
}


void *mp_nalloc(mp_pool_t *pool, size_t size) {

	unsigned char *m;
	mp_node_t *p;

	if (size <= pool->max) {
		p = pool->current;

		do {
			m = p->last;
			if ((size_t)(p->end - m) >= size) {
				p->last = m+size;
				return m;
			}
			p = p->next;
		} while (p);

		return mp_alloc_block(pool, size);
	}

	return mp_alloc_large(pool, size);
	
}

void *mp_calloc(mp_pool_t *pool, size_t size) {

	void *p = mp_alloc(pool, size);
	if (p) {
		memset(p, 0, size);
	}

	return p;
	
}

// 内存池释放,只针对大块内存
void mp_free(mp_pool_t *pool, void *p) {
	struct mp_large_s *l;
	//遍历大块内存块
	for (l = pool->large; l; l = l->next) {
		if (p == l->alloc) {
			free(l->alloc);
			l->alloc = NULL;
			return ;
		}
	}
}

int main(int argc, char *argv[]) {

	int size = 1 << 12;

	mp_pool_t *p = mp_create_pool(size);

	int i = 0;
	for (i = 0; i < 10; i++) {
		void *mp = mp_alloc(p, 512);
//		mp_free(mp);
	}

	// printf("mp_create_pool: %ld\n", p->max);
	printf("mp_align(123, 32): %d, mp_align(17, 32): %d\n", mp_align(24, 32), mp_align(17, 32));
	// printf("mp_align_ptr(p->current, 32): %lx, p->current: %lx, mp_align(p->large, 32): %lx, p->large: %lx\n", mp_align_ptr(p->current, 32), p->current, mp_align_ptr(p->large, 32), p->large);

	int j = 0;
	for (i = 0; i < 5; i++) {
		char *pp = mp_calloc(p, 32);
		for (j = 0;j < 32;j ++) {
			if (pp[j]) {
				printf("calloc wrong\n");
			}
			printf("calloc success\n");
		}
	}

	//printf("mp_reset_pool\n");

	for (i = 0;i < 5;i ++) {
		void *l = mp_alloc(p, 8192);
		mp_free(p, l);
	}

	mp_reset_pool(p);

	//printf("mp_destory_pool\n");
	for (i = 0;i < 58;i ++) {
		mp_alloc(p, 256);
	}

	mp_destory_pool(p);

	return 0;
}

4、参考

1 、结构体与类的编写: (A)利用struct关键字定义一个学生结构体(包括学号、姓名、性别):类名:student, num、name、sex,在主函数定义两个对象stud1,stud2,对stud1对象赋值并输出,对第2个对象stud2赋值输出; (B)利用class关键字将1改成类的编写,其它不变 (C)将输出封装成display,输入封装成setdata函数,分别在类里面定义2函数,在主函数中输入输出; (D)将上面两成员函数移至类外定义并输出 (E)将setdata函数利用对象的引用做函数参数,在主函数中输入输出 2、(1)定义一个时间类,属性包括小时、分、秒,定义两成员函数:settime,showtime,分别以两种方式、类内定义成员函数和内外定义成员函数 (2)对1两成员函数分别利用对象的引用做参数、默认参数做参数进行编写与调用并输出。属性 3、编写一个程序,模拟电梯的功能。功能接口包括电梯上行按钮、下行按钮、楼层选择和电梯在行驶过程中的楼层显示。 要求: (1)、由用户选择按上行按钮还是下行按钮,选择操作后再由用户输入要进入的楼层,进而电梯开始运行,显示所到的每一楼层层数。 (2).如果是上行,则选择输入的楼层号不能比当前楼层号小,否则应给出不合法提示。 (3). 如果是下行,则选择输入的楼层号不能比当前楼层号大,否则应给出不合法提示。 (4).电梯一旦开始运作就会始终运行,直到窗口关闭。 (5).电梯在经过不同楼层时,最好每个楼层的显示之间能有延迟,最终停靠的楼层的输出形式能更加醒目。如果可以,在电梯最初开始运行时,能在电梯由内部显示当前日期(提示:实现这些功能时,需要调用系统api,实现时间显示功能可以使用CDate类)。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值