首先,我们来看看它几个主要的数据结构:
//ngx内存池的头部信息
struct ngx_pool_s {
ngx_pool_data_t d; //数据区域
size_t max; //最大每次可分配内存
ngx_pool_t *current;//指向当前的内存池指针地址
ngx_pool_large_t *large; //存大块数据的链表
};
//ngx小块内存的信息
typedef struct {
u_char *last; //内存池中未使用内存的开始结点地址
u_char *end; //内存池的结束地址
ngx_pool_t *next; //下一个内存池
ngx_uint_t failed;//失败次数
} ngx_pool_data_t;
很明显,ngx_pool_data_t和ngx_pool_s构成了内存池的主题。在ngx_pool_data_t中:last表示内存池中未使用内存的开始结点地址,end表示当前内存池的结尾。相当于last到end就是当前内存池还未使用的内存大小。而它链表的形式就决定了nginx中如果当前内存池已满,便不会像平常一样扩大该内存池,而是重新分配一块内存,链到next指针上。但当内存池空间不够却又未满时,申请内存就回会失败,failed主要就是记录失败的次数,进而从新分配一个内存池。
接着就是表示大块内存的 ngx_pool_large_s
//大块内存的信息
struct ngx_pool_large_s {
ngx_pool_large_t *next; //指向下一个存储地址
void *alloc;//数据块指针地址
};
就是一个简单的链表,next指向下一块内存,alloc指向数据。
接下来,通过代码来分析具体的方法,更好的理解内存池的实现。(仅win32)
首先来看ngx_create_pool
///内存池的数据区最大容量
#define MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
//创建ngx的内存池
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
ngx_pool_t *p;
p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
//这里的ngx_memalign()函数相当于(ngx_pool_t*)ngx_alloc(size),执行内存分配。
//#define ngx_memalign(alignment, size, log) ngx_alloc(size, log)
if (p == NULL) {
return NULL;
}
//开始初始化数据区
//由于刚刚的开辟内存,因此last指针指向数据区的开始。
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
//end指针指向数据区的末尾。
p->d.end = (u_char *) p + size;
p->d.next = NULL;
p->d.failed = 0;
//当前内存池的大小减去内存池头部信息的大小,得到真正能够使用的内存大小
size = size - sizeof(ngx_pool_t);
//设置max,因为内存池的大小不超过一页(4k),所以内存池的最大值也就是size和NGX_MAX_ALLOC_FROM_POOL之中较小的。
p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
//current表示当前内存池
p->current = p;
//其他域都置成NULL
p->chain = NULL;
p->large = NULL;
p->cleanup = NULL;
p->log = log;
//返回指针
return p;
}
接下来看看如何从内存池中分配一块内存使用。
//考虑内存对齐的内存池内存申请
void* ngx_palloc(size_t size)
{
//判断当前申请内存的大小是否超过max,没有则说明申请的是小块内存
if (size <= _pool->max)
ngx_alloc_small(size, 1);
//申请大块内存
return ngx_alloc_large(size);
}
//考虑内存不对齐的内存池内存申请
void* ngx_pnalloc(size_t size)
{
if (size <= _pool->max)
ngx_alloc_small(size, 0);
return ngx_alloc_large(size);
}
//小块内存申请
void* ngx_alloc_small(size_t size, int i)
{
u_char *str;
ngx_pool_t *p;
p = _pool->current;//得到当前内存池指针
//遍历内存池
do{
str=p->d.last; //保存last指针
if (i){ //内存对齐操作
str = ngx_align_ptr(p->d.last, NGX_ALTGNMENT);//就是将内存的地址对齐
//#define NGX_ALIGNMENT sizeof(unsigned long)
//#define ngx_align_ptr(p,a) \
(u_char*)(((ngx_uint_t)(p)+((ngx_uint_t)a - 1))& ~((ngx_uint_t)a-1))
}
//判断当前内存池可用大小与请求的内存大小
if ((size_t)(p->d.end - str) >= size){
//更新last指针,并将前面保存的last返回 使用该内存
p->d.last = str + size;
return str;
}
//否则就继续遍历
p = p->d.next;
} while (p);
//表示内存池已经满掉,因此我们需要重新分配一个内存池然后链接到当前的data的next上。
return ngx_palloc_block(size);
}
内存对齐操作,是一个内存地址取整宏,毕竟这里只是对指针进行偏移,last指针的位置需要自己动手将其调整。因为内存不对齐的话,会导致CPU I/O的次数增加,效率降低。
#define ngx_align_ptr(p,a) \
(u_char*)(((ngx_uint_t)(p)+((ngx_uint_t)a - 1))& ~((ngx_uint_t)a-1))
ngx_palloc_block的实现主要就是重新分配一块内存池,然后链接到当前内存池的数据去指针。注意这里的新内存池大小是与其父内存池一样大。
void* ngx_palloc_block(size_t size)
{
u_char *str;
ngx_pool_t *p, *new_p,*current;
size_t psize;
//计算当前内存池的的大小
psize = (size_t)(_pool->d.end - (u_char*)_pool);
//再开辟一块同样大小的内存池
str = (u_char*)ngx_alloc(psize);
if (NULL == str)
return NULL;
new_p = (ngx_pool_t*)str;
//更新指针,初始化新开辟出来的内存池
new_p->d.end = str + psize;
new_p->d.next = NULL;
new_p->d.failed = 0;
//由下面可以看出来,新开辟出来的内存池只存放了ngx_pool_data_t的信息
str += sizeof(ngx_pool_data_t);//头部信息大小
str = ngx_align_ptr(str, NGX_ALTGNMENT);//内存对齐操作
new_p->d.last = str + size;//更新last指针的位置,即申请的size大小
current=_pool->current;//设置current
/*当failed大于4说明我们请求了4次,都不能满足,此时就假设老
的内存池没有空间了,从比较新的内存块开始 提高效率*/
for (p = current; p->d.next; p = p->d.next){
if (p->d.failed++ > 4){
current = p->d.next;
}
}
//连接到最后一个内存池上
p->d.next = new_p;
//如果current为NULL,则current就为new_p
pool->current=current?current:new_p;
return str;
}
这样设置current的原因是因为在ngx_palloc中分配内存是从current开始的,将current设置为比较新分配的内存会提高效率,而循环条件“failed大于4”则在上面说了,同样为提高效率。
至于大块内存的申请,就是简单的malloc一块大块内存链接到主内存池上。
void* ngx_alloc_large(size_t size)
{
void *p;
ngx_uint_t n;
ngx_pool_large_t *large;
p = ngx_alloc(size);
if (NULL == p)
return NULL;
n = 0;
//开始遍历large链表,查找一个alloc(内存区指针)为空,则赋值返回
for (large = _pool->large; large; large = large->next)
{
if (large->alloc == NULL){
large->alloc = p;
return p;
}
if (n++ > 3)//
break;
}
//为提高效率,如果3次都没找到空的,则重新创建一个
large = (ngx_pool_large_t*)ngx_alloc_small(sizeof(ngx_pool_large_t), 1);
if (large == NULL)
{
ngx_pfree(p);
return NULL;
}
//链接数据区指针p到large。这里可以看到直接插入到large链表的头的。
large->alloc = p;
large->next = NULL;
_pool->large = large;
return p;
}
好了,这些便是内存池的内存分配, 下面我们来看看内存的释放。这里我们要知道在nginx中,只有大块内存提供了free接口,可以供我们释放,而小块内存,是没有提供这样的接口的,只有在整个内存desrtoy掉时小块内存才会被释放。
//重置内存池
void ngx_reset_pool()
{
ngx_pool_t *p;
ngx_pool_large_t *large;
//释放大块内存
for (large = _pool->large; large; large = large->next)
{
if (large->alloc){
ngx_free(large->alloc);
}
}
//重置小块内存区域
for (p = _pool; p;p=p->d.next)
{
p->d.last = (u_char*)p + sizeof(ngx_pool_t);
p->d.failed = 0;
}
_pool->current = _pool;
_pool->large = NULL;
}
//把p指向的内存归还给内存池
int ngx_pfree(void *p)
{
ngx_pool_large_t *large;
for (large = _pool->large; large; large = large->next)
{
if (large->alloc){
ngx_free(large->alloc);
large->alloc = NULL;
return NGX_OK;
}
}
return NGX_DECLINED;
}
内存池的销毁
//销毁内存池
void ngx_destroy_pool()
{
ngx_pool_t *p, *n;
ngx_pool_large_t *large;
//先从大块内存开始进行释放
for (large = _pool->large; large; large = large->next)
{
if (large->alloc){
ngx_free(large->alloc);
}
}
//最后释放内存池的内存池
for (p = _pool, n = _pool->d.next;; p = n, n = n->d.next)
{
ngx_free(p);
if (n == NULL)
break;
}
}