nginx内存池源码学习及代码移植实现内存池类

nginx内存池源码学习及代码移植

通过学习nginx内存池源码了解其所创建的内存分配机制,并利用C++面向对象的思想将其封装为一个内存池类进行代码移植

为什么需要内存池

C/C++中通过malloc或new 的内存分配的主要缺点有:
一是可能需要花费很多时间,每次malloc或new 都要进入到内核,是一个效率相对较低的操作。
二是每次开辟的内存空间大小具有随机性,系统new操作是找到最近的一个足够大的连续空间分配出去,如果某个较小的内存被delete回收到系统,而之后发生的new操作需要更大尺寸的内存,于是较小的内存被弃用成为所谓的内存碎片。这样较小的内存被弃用在内存上就会将形成碎片,所以一个应用程序的运行会越来越慢。
内存池的出现就是为了提高向系统申请内存时的分配效率并减少内存碎片的出现(或对申请的内存进行充分的利用),内存池的主要思想是程序在申请堆上的内存时,我们可以给它一大块内存(远超出程序要申请的大小,即内存池),这样减少系统调用的次数,然后依据其所设计的逻辑对这块内存进行管理进行充分利用。

nginx内存池的数据结构及主要函数:

数据结构

//开辟的内存池的主体结构
struct ngx_pool_s {
    ngx_pool_data_t       d;	//内存池的数据头
    size_t                max;	//内存池所能分配的小块内存的最大值
    ngx_pool_t           *current;//小块内存池的入口指针
    ngx_chain_t          *chain;
    ngx_pool_large_t     *large;//大块内存分配的入口指针
    ngx_pool_cleanup_t   *cleanup;//清理函数的handler的入口指针
    ngx_log_t            *log;
};
//小块内存数据头信息
typedef struct {
    u_char               *last;//可分配内存的开始位置
    u_char               *end;//可分配内存的末尾位置
    ngx_pool_t           *next;//指向下一个内存池的地址
    ngx_uint_t            failed;//记录当前内存吃分配失败的次数
} ngx_pool_data_t;
//大块内存数据头信息
typedef struct ngx_pool_large_s  ngx_pool_large_t;
struct ngx_pool_large_s {
    ngx_pool_large_t     *next;//指向下一个同类型内存块
    void                 *alloc;//大块内存的起始地址
};
//清理回调函数的类型定义
typedef void (*ngx_pool_cleanup_pt)(void *data);
typedef struct ngx_pool_cleanup_s  ngx_pool_cleanup_t;
struct ngx_pool_cleanup_s {
    ngx_pool_cleanup_pt   handler;//保存预先设置的回调函数
    void                 *data;//释放资源时资源的地址
    ngx_pool_cleanup_t   *next;//链表
};

主要函数:

ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log);//开辟指定大小的内存池
void ngx_destroy_pool(ngx_pool_t *pool);//销毁内存池
void ngx_reset_pool(ngx_pool_t *pool);//重置内存池

void *ngx_palloc(ngx_pool_t *pool, size_t size);//内存分配函数,支持内存对齐
void *ngx_pnalloc(ngx_pool_t *pool, size_t size);//内存分配函数,不支持内存对齐
void *ngx_pcalloc(ngx_pool_t *pool, size_t size);//内存分配函数,支持内存初始化为0

ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);//大块内存释放

ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size);//添加handler

底层具体的实现文章较多,也容易理解,具体看源码,自己画一画。

学习总结

1、nginx的小块内存分配效率极为高效,通过简单的指针偏移就可以实现
2、其大块内存的信息数据头和清理回调函数的信息数据头也是通过调用void *ngx_palloc(ngx_pool_t *pool, size_t size)函数实现,将内存分配及回收效率极大的提高
3、但是其通过指针偏移来分配内存的机制决定了其不能够对小块内存进行回收,因为用last和end来记录未被分配的内存,不能将1、2、3中间的2内存回收通过两个指针来记录两块空余的内存地址
4、在开辟新内存池方面,新内存池的大小和原来第一次创建的内存大小一样,但是只包含ngx_pool_data_t小块内存数据头信息,而不再包含max、current、large等信息,只在第一个内存池上记录即可。每开辟一个新的内存池,说明在前面的内存池中都申请失败,把他们的failed+1,当某个内存池failed次数大于4时,认为其上内存几乎分配完毕,令current指向下一块内存池的地址
5、在大块内存申请方面,并没有当判断其为大块内存申请时就为其创建内存头保存信息,而是遍历原有的大块内存头链表,但是只遍历前三个,如果有空闲的(它之前的内存释放了,但头信息保留置空)直接利用即可,遍历前三个的原因是链表遍历效率不高
6、重置内存池时,先把大块内存释放掉,因为大块内存的内存头信息还在小块内存池中保留着,如果先释放小块内存,那么大块内存的地址信息丢失就会造成内存泄露
7、在如下的遍历小块内存函数并进行重置时,将last指向sizeof(ngx_pool_t)的位置,但是只有第一个内存池的头部信息有这么大,之后开辟的只有sizeof(ngx_pool_data_t)大小,如第4条所说,所以应该将其分别处理

//原代码
for (p = pool; p; p = p->d.next) {
	p->d.last = (u_char *) p + sizeof(ngx_pool_t);
	p->d.failed = 0;
}
//处理第一块内存,包含完整的数据头信息
p = pool_;
p->d.last = (u_char *)p + sizeof(ngx_pool_s);
p->d.failed = 0;
//处理之后的内存,只包含分配小块内存的数据头部信息,少了current,large,cleanup等,只存一份
for (p = p->d.next; p; p = p->d.next) {
	p->d.last = (u_char *)p + sizeof(ngx_pool_data_t);
	p->d.failed = 0;
}

8、在销毁内存池方面先销毁大块内存,若大块内存中存在指向外部资源的变量,那么应该先把外部资源释放掉,这里通过预置的handle回调函数实现

源代码进行移植实现内存池类

#include
#include
#include
using namespace std;

//类型重定义
using u_char = unsigned char;
using ngx_uint_t = unsigned int;

//类型前置声明
struct ngx_pool_s;

//清理函数的类型,回调函数
typedef void(*ngx_pool_cleanup_pt)(void *data);
struct ngx_pool_cleanup_s {
	ngx_pool_cleanup_pt   handler;//保存预先设置的回调函数
	void                  *data;//释放资源时资源的地址
	ngx_pool_cleanup_s    *next;//链表
};

//大块内存的内存头信息
struct ngx_pool_large_s {
	ngx_pool_large_s	*next;//指向下一个同类型内存块
	void				*alloc;//大块内存的起始地址
};

//分配小块内存的内存池的头部数据信息,这里的数据头 每个内存块都有
struct ngx_pool_data_t {
	u_char				*last;//内存池中可用内存的起始地址
	u_char				*end;//内存池可用内存的末尾地址
	ngx_pool_s			*next;//指向下一个开辟的内存池地址,初值为空
	ngx_uint_t			failed;//在当前内存池中分配内存失败的次数
} ;

//nginx内存池的头部信息和管理成员信息
struct ngx_pool_s {
	ngx_pool_data_t		d;//内存池的使用情况的头信息
	//max(creat函数中)是记录当前内存块能用于小内存分配的最大内存大小
	//如果当前内存池中剩余的内存size<4095,那就只能记作size,否则记作4095
	size_t				max;
	ngx_pool_s			*current;//指向当前可分配内存的内存块
	ngx_pool_large_s	*large;//大块内存分配的入口函数
	ngx_pool_cleanup_s	*cleanup;//清理函数handler的入口函数
};


//把数值d调整成邻近的a的倍数
#define ngx_align(d, a)     (((d) + (a - 1)) & ~(a - 1))

//能从池里分配的小块内存的最大内存,一个页面,4K,4096
const int ngx_pagesize = 4096;//默认一个物理页面的大小
const int NGX_MAX_ALLOC_FROM_POOL = ngx_pagesize - 1;
//默认的开辟的ngx内存池的大小
const int NGX_DEFAULT_POOL_SIZE = 16 * 1024;
//内存分配的匹配对齐的字节数
const int NGX_POOL_ALIGNMENT = 16;
//能够定义的最小的池的大小包含一个数据头信息大小和2*2个指针类型大小
//再将其转化为16的倍数
const int NGX_MIN_POOL_SIZE = \
			ngx_align((sizeof(ngx_pool_s) + 2 * sizeof(ngx_pool_large_s)), \
			NGX_POOL_ALIGNMENT);
			
class ngx_mem_pool {
public:
	//创建指定size的内存池
	void* ngx_creat_pool(size_t size);

	//考虑内存字节对齐,从内存池中为小块内存申请size大小的内存
	void *ngx_palloc(size_t size);

	//不考虑内存字节对齐,从内存池中申请size大小的内存
	void *ngx_pnalloc(size_t size);

	//将内存初始化为0的内存申请操作
	void *ngx_pcalloc(size_t size);

	//提供释放大块内存的函数,注(小块内存由于其通过指针偏移来分配内存,所以连续内存不能只释放中间的)
	void ngx_pfree(void *p);

	//重置内存池
	void ngx_reset_pool();
	
	//销毁内存池
	void ngx_destroy_pool();
	
	//添加回调清理操作函数
	ngx_pool_cleanup_s *ngx_pool_cleanup_add(size_t size);

private:
	
	ngx_pool_s			*pool_;//指向nginx内存池的入口指针
	//从池中分配小块内存
	void *ngx_palloc_small(size_t size, ngx_uint_t align);
	//分配大块内存
	void *ngx_palloc_large(size_t size);
	//开辟新的内存池
	void *ngx_palloc_block(size_t size);
};

源代码地址:nginx内存池移植代码

你可能感兴趣的:(c++,内存管理)