先了解一下Nginx:
Nginx是一个高性能的HTTP反向代理服务器,接收浏览器请求。其特点是占有内存少,并发能力强,稳定性高。
nginx有什么功能?
接收http服务,正向反向代理(负载均衡)。
正向代理代理客户端,反向代理代理服务器,反向代理也称作负载均衡器
http协议本身就是一个短链接,无状态的协议
内存池的好处:
减少向系统申请和释放内存的时间开销,解决内存频繁分配产生的碎片,提示程序性能。
nginx有小块内存和大块内存的概念,小块内存nginx在分配的时候会尝试在当前的内存池节点中分配,而大块内存会调用系统函数malloc向操作系统申请
区分小块内存和大块内存的原因有2个:
1、针对大块内存 如果它的生命周期远远短于所属的内存池,那么提供一个单独的释放函数是十分有意义的,但不区分大块内存和小块内存,针对大的内存块便会无法提前释放了
2、大块内存与小块内存的界限是一页内存4K(下文有体到),大于一页的内存在物理上不一定是连续的,所以如果分配的内存大于一页的话,从内存池中使用,和向操作系统重新申请效率差不多是等价的。
主要数据结构:
ngx_pool_data_t(内存池数据块结构)
typedef struct 包含了所需要操作这个内存池数据的一些指针。
{
u_char *last; unsigned char类型指针,保存当前数据区的已经使用的数据的结尾。
u_char *end; 表示当前的内存池的结尾。end-last就是内存池未使用的大小。
ngx_pool_t *next; 指向下一块内存池。
ngx_uint_t failed; 标记请求小块内存由于内存池空间不够,而需要重新分配一个小内存池的次数。
} ngx_pool_data_t;
ngx_pool_t(内存池头部结构)
struct ngx_pool_t
{
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; 清理函数列表
ngx_log_t *log; 日志
};
ngx_pool_large_s(大块内存池类型)
struct ngx_pool_large_s
{
ngx_pool_large_t *next; 指向下一大块内存
void *alloc; 本块内存
};
ngx_pool_cleanup_s(内存池中的数据清理)
struct ngx_pool_cleanup_s
{
ngx_pool_cleanup_pt handler; 清理函数
void *data; 传给清理函数的数据
ngx_pool_cleanup_t *next; 指向下一个清理函数 调用destroy时会遍历清理函数链表,调用handler
}
主要函数:
1、ngx_create_pool() 内存池创建
宏定义的函数,就是对malloc进行简单封装。里面写入了错误日志
#define ngx_memalign(alignment,size,log) ngx_alloc(size,log)
内存池的数据区的最大容量。在x86上ngx_pagesize=4096。
因为寻址页面大小为4096,是一个相对合适的数字,保证size不会太大,导致寻址不到浪费空间
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
ngx_pool_t * 函数返回值类型
ngx_create_pool(size_t size, ngx_log_t *log) 传进来的size并不是真正能使用的大小,应减去sizeof(ngx_poll_t)。
{
ngx_pool_t *p;
p = ngx_memalign(NGX_POOL_ALIGNMENT,size,log) 用宏定义了一个函数,第一个参数宏定义的16,用来内存对齐
if (p == NULL)
{
return NULL;
}
内存已经申请了,对结构体指针初始化。
由于一开始数据区为空,因此last指向数据区的开始。
p->d.last = (u_char *) p + sizeof(ngx_pool_t); 可以证明上述实际使用空间并没有size
end也就是数据区的结束位置
p->d.end = (u_char *) p + size;
p->d.next = NULL;
p->d.failed = 0;
这里才是我们真正能使用的大小。
size = size - sizeof(ngx_pool_t);
然后设置max。内存池的最大值也就是size和最大容量之间的最小值。
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;
}
为什么要考虑内存对齐?
因为内存对齐可以降低CPU读取内存的次数,提高性能。主要为减少内存的I/O操作。
2、内存申请:
ngx_palloc() 分配的内存会对齐
ngx_calloc() 用来分配一块清0的内存
ngx_pnalloc() 分配的内存不会对齐
就对齐的计算,不必深究
#define ngx_align_ptr(p,a) (u_char *)(((uintptr_t)(p) + ((uintptr_t)a - 1)) &~((uintptr_t)a - 1))
#define NGX_ALIGNMENT sizeof(unsigned long)
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
u_char *m;
ngx_pool_t *p;
//判断当前申请的大小是否超过max,如果超过则说明是申请大块内存,此时进入large
if (size <= pool->max) 如果申请小块内存,进入
{
得到当前的内存池指针。
p = pool->current;
开始遍历内存池
do {
首先对齐last指针。调用宏对内存地址进行对齐处理。
如果是ngx_pnalloc(),就省去这一步。
m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);
得到当前内存池中的可用大小。如果剩下的空间不够,则直接返回当前的last,也就是数据的指针。
if ((size_t) (p->d.end - m) >= size) 如果当前够存,进入
{
偏移last,然后返回前面保存的last。
p->d.last = m + size;
return m;
}
否则继续遍历
p = p->d.next;
} while (p);
到达这里说明内存池已经满掉,因此我们需要重新分配一个小块内存然后链接到当前的data的next上。
return ngx_palloc_block(pool, size);
}
申请大块。
return ngx_palloc_large(pool, size);
}
ngx_palloc_block() 申请小块内存
static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
u_char *m;
size_t psize;
ngx_pool_t *p, *new, *current;
计算当前的内存池的大小。
psize = (size_t) (pool->d.end - (u_char *) pool);
再申请一个与pool同样大小的内存
m = ngx_memalign(NGX_POOL_ALIGNMENT,psize,pool->log)
if (m == NULL)
{
return NULL;
}
new = (ngx_pool_t *) m;
接下来和我们create一个内存池做的操作一样。就是更新一些指针
new->d.end = m + psize;
new->d.next = NULL;
new->d.failed = 0;
让m指向该块内存ngx_pool_data_t结构体之后数据区的起始位置
m += sizeof(ngx_pool_data_t);
m = ngx_align_ptr(m, NGX_ALIGNMENT); 对齐指针,前面说了
new->d.last = m + size; 设置last指针位置
设置current。
current = pool->current;
这里遍历所有的子内存池,这里主要是通过failed来标记重新分配子内存池的次数,然后找出最后一个大于4的,标记它
for (p = current; p->d.next; p = p->d.next)
{
if (p->d.failed++ > 4)
{
当failed大于4说明我们至少请求了4次内存分配,都不能满足我们的请求,
此时我们就假设老的内存都已经没有空间了,因此我们就从比较新的内存块开始。
current = p->d.next;
}
}
链接到最后一个内存池后面
p->d.next = new;
如果current为空,则current就为new。
pool->current = current ? current : new;
return m;
}
ngx_palloc_large() 申请大块内存
static void *
ngx_palloc_large(ngx_pool_t *pool, size_t size)
{
void *p;
ngx_uint_t n;
ngx_pool_large_t *large;
分配一大块内存。
p = ngx_memalign(NGX_POOL_ALIGNMENT,size,pool->log) 上面提了
if (p == NULL)
{
return NULL;
}
n = 0;
开始遍历large链表,如果有alloc(也就是内存区指针)为空,则直接指针赋值然后返回。一般第一次请求大块内存都会
for (large = pool->large; large; large = large->next)
{
if (large->alloc == NULL)
{
large->alloc = p;
return p;
}
链表不能超过4个,只对前3个内存块进行检查,否则就直接分配内存。
if (n++ > 3)
{
break;
}
}
malloc一大内存块。
large = ngx_palloc(pool, sizeof(ngx_pool_large_t)); 调用本身
if (large == NULL)
{
ngx_free(p); 从pool的large链表中找到p,然后free掉。
return NULL;
}
然后链接数据区指针p到large。
large->alloc = p;
large->next = pool->large;
pool->large = large;
return p;
}
3、ngx_destroy_pool() 内存池的销毁
void
ngx_destroy_pool(ngx_pool_t *pool)
{
ngx_pool_t *p, *n;
ngx_pool_large_t *l;
ngx_pool_cleanup_t *c;
首先调用所有的数据清理函数
for (c = pool->cleanup; c; c = c->next)
{
if (c->handler)
{
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,"run cleanup: %p", c);
c->handler(c->data); 说结构体的时候就提到了这个函数
}
}
free大块内存 ngx_palloc_large申请的内容
for (l = pool->large; l; l = l->next)
{
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
if (l->alloc)
{
ngx_free(l->alloc);
}
}
遍历小块内存池。 ngx_create_pool申请的内存
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next)
{
直接free掉。
ngx_free(p);
if (n == NULL)
{
break;
}
}
}
补充:
1、在nginx内存池中只有大块内存提供了free接口【ngx_pfree(ngx_pool_t *pool, void *p),这个函数就是从pool的large链表中找到p,然后free掉它】,可以回收释放。
小块数据是不释放的,只有当整个内存池被destroy掉才会被释放,这样就简化了内存池的操作。为什么不释放?原因:http服务器是间歇性处理,是一个短连接,无状态的协议。处理完请求,服务器断开。在处理完http请求完成之后,就可以发起内存池的重置函数。
2、ngx_pool_cleanup_t * ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
这个函数也就是添加一个ngx_pool_cleanup_t到当前的pool上,然后返回,我们此时就能通过返回的结构来给对应的handler赋值。当内存池destroy的时候我们就能添加这些清理函数到pool中,然后当内存池释放的时候就会自动调用。
4、ngx_pfree() 内存池清理
ngx_int _t
ngx_pfree(ngx_pool_t *pool,void *p)
{
ngx_pool_large_s *l;
释放大块内存
for(l=pool->large ;l;l=l->next )
{
if(p==l->alloc)
{
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC,pool->log,0,"free:%p",l->alloc);
free(l->alloc );
l->alloc =NULL;
return NGX_OK;
}
}
return NGX_DECLINED;
}
注:因为大块内存只对前三个进行检查,否则就直接分配,所以大块内存必须及时释放。
5、ngx_reset_pool() 内存池重置
void
ngx_reset_pool(ngx_pool_t *pool)
{
ngx_pool_large_s *l;
ngx_pool_s *p;
释放所有大块内存
for(l=pool->large ;l;l=l->next )
{
if(l->alloc )
{
free(l->alloc );
l->alloc =NULL;
}
}
pool->large = NULL;
重置所有小块内存
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;
}
在处理完一批http请求完成之后就可以发起内存池的重置函数