上一篇文章Nginx学习之路(六)NginX中的内存管理之---Nginx中的内存对齐和内存分页说到了Nginx中的内存对齐机制和内存分页机制,今天就来说下Nginx中的内存池,内存池是一个使用非常广泛的技术,在web服务器的高并发情况下可能存在平凡的malloc()和free()过程,通过内存池的方式可以将这一过程的开销极大程度的减少,Nginx的内存池的设计相比经典的sgi stl中的allocator内存池实现方式更加的贴合操作系统,下面就来详细的介绍一下Nginx中的内存池:
首先先看内存池的结构体:
typedef struct {
u_char *last;//内存池的可使用位置
u_char *end;//内存池的结束位置
ngx_pool_t *next;//指向下一块内存池的指针
ngx_uint_t failed;//统计该内存池不能满足分配请求的次数
} ngx_pool_data_t;
struct ngx_pool_s {
ngx_pool_data_t d;//数据块,里面存储了关于这块内存池的可使用位置,结束位置,以及指向下一块内存池的指针等
size_t max;//数据块的大小,即小块内存的最大值
ngx_pool_t *current;//记录当前内存池的地址
ngx_chain_t *chain;//chain结构体指针,类似一个链表,链表节点数据为一个buf
ngx_pool_large_t *large;//分配大块内存用,即超过max的内存请求
ngx_pool_cleanup_t *cleanup;//挂载一些内存池释放的时候,同时释放的资源。
ngx_log_t *log;//日志指针
};
盗用人家的一张图来说明
在调用ngx_create_pool(1024, 0x80d1c4c)后,创建的内存池的结构如下图
这样,内存池的设计结构就很明了了,下面详细的说明一块内存的分配过程:
Nginx的内存池的使用主要是在各个连接到达之后为每个连接单独开辟一个内存池,在连接关闭后释放这个内存池,因此,nginx内存池设计的内存分配比较细化,对于各种size内存的分配都做了一定处理,首先是这两个函数:
//分配内存对齐NGX_ALIGNMENT的块
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)
if (size <= pool->max) {
//分配小块内存
return ngx_palloc_small(pool, size, 1);
}
#endif
//分配大块内存
return ngx_palloc_large(pool, size);
}
//分配内存大小size的块,不做对齐
void *
ngx_pnalloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)
if (size <= pool->max) {
//分配小块内存
return ngx_palloc_small(pool, size, 0);
}
#endif
//分配大块内存
return ngx_palloc_large(pool, size);
}
来看下小块内存是怎么分配的:
static ngx_inline void *
ngx_palloc_small(ngx_pool_t *pool, size_t size, ngx_uint_t align)
{
u_char *m;
ngx_pool_t *p;
p = pool->current;
do {
m = p->d.last;
if (align) {
//内存对齐NGX_ALIGNMENT的块
m = ngx_align_ptr(m, NGX_ALIGNMENT);
}
//然后计算end值减去这个偏移指针位置的大小是否满足索要分配的size大小,
//如果满足,则移动last指针位置,并返回所分配到的内存地址的起始地址;
if ((size_t) (p->d.end - m) >= size) {
p->d.last = m + size;
return m;
}
//如果不满足,则查找下一个链。
p = p->d.next;
} while (p);
//如果遍历完整个内存池链表均未找到合适大小的内存块供分配,则执行ngx_palloc_block()来分配。
//ngx_palloc_block()函数为该内存池再分配一个block,该block的大小为链表中前面每一个block大小的值。
//一个内存池是由多个block链接起来的。分配成功后,将该block链入该poll链的最后,
//同时,为所要分配的size大小的内存进行分配,并返回分配内存的起始地址。
return ngx_palloc_block(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;
//计算新开辟的内存池大小,大小和之前的pool一致
psize = (size_t) (pool->d.end - (u_char *) pool);
//新开辟一块内存池
//执行按NGX_POOL_ALIGNMENT对齐方式的内存分配,假设能够分配成功,则继续执行后续代码片段。
m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
if (m == NULL) {
return NULL;
}
//初始化内存池的一些参数
new = (ngx_pool_t *) m;
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_ALIGNMENT
m = ngx_align_ptr(m, NGX_ALIGNMENT);
new->d.last = m + size;
//失败4次以上移动current指针
for (p = pool->current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
pool->current = p->d.next;
}
}
p->d.next = new;
return m;
}
然后是大块内存的分配:
static void *
ngx_palloc_large(ngx_pool_t *pool, size_t size)
{
//这是一个static的函数,说明外部函数不会随便调用,而是提供给内部分配调用的,
//即nginx在进行内存分配需求时,不会自行去判断是否是大块内存还是小块内存,
//而是交由内存分配函数去判断,对于用户需求来说是完全透明的。
void *p;
ngx_uint_t n;
ngx_pool_large_t *large;
//ngx_alloc是一个简单的封装,直接调用的malloc
p = ngx_alloc(size, pool->log);
if (p == NULL) {
return NULL;
}
n = 0;
//将分配的内存链入pool的large链中,
//这里指原始pool在之前已经分配过large内存的情况。
for (large = pool->large; large; large = large->next) {
if (large->alloc == NULL) {
large->alloc = p;
return p;
}
if (n++ > 3) {
break;
}
}
//当原始pool中没有large块时,比如新建的一块pool
//分配一块ngx_pool_large_t结构体来管理large内存
large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
if (large == NULL) {
ngx_free(p);
return NULL;
}
//将这块large加入pool
large->alloc = p;
large->next = pool->large;
pool->large = large;
return p;
}
关于Nginx的内存释放和回收,这里就直接贴淘宝Tengine团队总结的过程了,总结的很好:
clean up资源:
可以看到所有挂载在内存池上的资源将形成一个循环链表,一路走来,发现链表这种看似简单的数据结构却被频繁使用。
由图可知,每个需要清理的资源都对应有一个头部结构,这个结构中有一个关键的字段handler,handler是一个函数指针,在挂载一个资源到内存池上的时候,同时也会注册一个清理资源的函数到这个handler上。即是说,内存池在清理cleanup的时候,就是调用这个handler来清理对应的资源。
比如:我们可以将一个开打的文件描述符作为资源挂载到内存池上,同时提供一个关闭文件描述的函数注册到handler上,那么内存池在释放的时候,就会调用我们提供的关闭文件函数来处理文件描述符资源了。
内存的释放:
nginx只提供给了用户申请内存的接口,却没有释放内存的接口,那么nginx是如何完成内存释放的呢?总不能一直申请,用不释放啊。针对这个问题,nginx利用了web server应用的特殊场景来完成;
一个web server总是不停的接受connection和request,所以nginx就将内存池分了不同的等级,有进程级的内存池、connection级的内存池、request级的内存池。
也就是说,创建好一个worker进程的时候,同时为这个worker进程创建一个内存池,待有新的连接到来后,就在worker进程的内存池上为该连接创建起一个内存池;连接上到来一个request后,又在连接的内存池上为request创建起一个内存池。这样,在request被处理完后,就会释放request的整个内存池,连接断开后,就会释放连接的内存池。因而,就保证了内存有分配也有释放。
小结:通过内存的分配和释放可以看出,nginx只是将小块内存的申请聚集到一起申请,然后一起释放。避免了频繁申请小内存,降低内存碎片的产生等问题。