
3.2 Nginx内存池
Nginx使用内存池管理进程内内存,当接收到请求时,创建一个内存池。处理请求过程中需要的内存都从这个内存池中申请,请求处理完成后释放内存池。Nginx将内存池中的内存分为两类:小块内存、大块内存。大、小块内存的分界点是由创建内存池时的参数以及系统页大小决定的。对于小块内存,其在用户申请后并不需要释放,而是等到释放内存池时再释放。对于大块内存,用户可以调用相关接口进行释放,也可以等内存池释放时再释放。Nginx内存池支持增加回调函数,当内存池释放时,自动调用回调函数以释放用户申请的其他资源。值得一提的是,回调函数允许增加多个,通过链表进行链接,在内存池释放时被逐一调用。
3.2.1 内存池结构
与Nginx内存池相关的结构主要有3个:ngx_pool_s、ngx_pool_data_t和ngx_pool_large_s。
// ngx_pool_large_s的结构 typedef struct ngx_pool_large_s ngx_pool_large_t; struct ngx_pool_large_s { ngx_pool_large_t *next; // 用于构成链表 void *alloc; // 指向真正的大块内存 }; // ngx_pool_data_t的结构 typedef struct { u_char *last; u_char *end; ngx_pool_t *next; ngx_uint_t failed; } ngx_pool_data_t; // ngx_pool_s的结构 typedef struct ngx_pool_s ngx_pool_t; 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; ngx_log_t *log; };
应用程序首先需要通过ngx_create_pool函数创建一个新的内存池,之后从新的内存池中申请内存或者释放内存池中的内存。下面先看一下如何创建内存池以及内存池的基本结构。
// 返回创建的内存池地址,size为内存池每个内存块大小,log为打印日志 ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log){ ngx_pool_t *p; // 申请内存,如果系统支持内存地址对齐,则默认申请16字节对齐地址 p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log); if (p == NULL) return NULL; // 初始化内存池 p->d.last = (u_char *) p + sizeof(ngx_pool_t); p->d.end = (u_char *) p + size; p->d.next = NULL; p->d.failed = 0; // 计算内存池中每个内存块最大可以分配的内存 size = size - sizeof(ngx_pool_t); p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL; p->current = p; p->chain = NULL; p->large = NULL; p->cleanup = NULL; p->log = log; return p; }
初始化的Nginx内存池结构如图3-1所示。

图3-1 初始化的Nginx内存池结构
3.2.2 申请内存
创建完内存池后,我们可以从内存池中申请内存。Nginx提供了3个API:ngx_palloc、ngx_pcalloc、ngx_pnalloc。其中,ngx_palloc是最基本的内存申请API,获取内存后并不进行初始化操作。ngx_pcalloc对ngx_ palloc进行了简单封装,其通过ngx_palloc申请内存后,对内存进行初始化。相比于ngx_pnalloc,ngx_palloc申请内存的首地址是对齐的(也就是说,申请到的内存首地址是4或者8的整数倍,与系统相关),而ngx_pnalloc没有考虑内存对齐。下面重点介绍ngx_palloc。
void * ngx_palloc(ngx_pool_t *pool, size_t size){ if (size <= pool->max) return ngx_palloc_small(pool, size, 1); return ngx_palloc_large(pool, size); }
可以看出:如果申请的内存小于等于pool->max,则认为是小块内存申请;如果大于pool->max,则认为是大块内存申请。
申请小块内存时,Nginx会先查看内存池中当前的内存块是否还有可以分配的空间,如果没有,则逐一遍历内存池中的内存块,找到则返回。如果没有找到,Nginx则会申请一个新的内存块。值得一提的是:如果某次申请没有从内存池的现有内存块中申请到内存,而是申请了一块新的内存,则会增加内存池中每个内存块的分配失败次数;如果内存块的分配失败次数超过4[1],则不会再尝试从这个内存块中申请内存。申请小块内存的源码可以参考ngx_palloc_small函数。限于篇幅,此处仅给出核心部分的代码。
static 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) m = ngx_align_ptr(m, NGX_ALIGNMENT); if ((size_t) (p->d.end - m) >= size) { p->d.last = m + size; return m; } p = p->d.next; } while (p); // 重新申请一块内存用于小块内存申请 return ngx_palloc_block(pool, size); }
对于大块内存申请,Nginx直接申请相应大小的内存块,通过链表将已经申请的内存块进行链接。值得一提的是,大块内存管理的链表节点ngx_pool_large_t所占用的内存是从内存池中申请的,因为这仅仅是一小块内存,便于释放。
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_alloc(size, pool->log); if (p == NULL) return NULL; // 获取大块内存管理的链表结构,该结构可以复用 n = 0; for (large = pool->large; large; large = large->next) { if (large->alloc == NULL) { large->alloc = p; return p; } if (n++ > 3) break; } // 直接从内存池中申请大块内存管理的链表节点 large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1); if (large == NULL) { ngx_free(p); return NULL; } large->alloc = p; // 将大块内存放到内存池中,也就是放到链表头部,便于维护管理 large->next = pool->large; pool->large = large; return p; }
综上,Nginx内存池基本结构如图3-2所示。

图3-2 Nginx内存池基本结构
3.2.3 释放内存
我们从内存池中申请内存后,可能需要释放内存。对于内存池中的小块内存,其并不需要进行释放,因为在释放整个内存池时会随之释放。我们可以通过ngx_pfree进行大块内存释放。下面先介绍如何释放大块内存。
ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p){ ngx_pool_large_t *l; // 遍历大块内存链表 for (l = pool->large; l; l = l->next) { // 如果找到这块内存 if (p == l->alloc) { ngx_free(l->alloc); l->alloc = NULL; return NGX_OK; } } return NGX_DECLINED; }
讲解完如何释放大块内存后,我们需要知道如何释放内存池。释放内存池的逻辑比较简单,首先查看内存池是否挂载清理函数,如果是,则逐一调用链表中的所有回调函数,之后再释放大块内存,最后释放内存池中的内存块。释放内存池API接口为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) c->handler(c->data); } // 遍历大块内存进行释放 for (l = pool->large; l; l = l->next) { if (l->alloc) ngx_free(l->alloc); } // 释放内存池内存块 for (p = pool, n = pool->d.next;; p = n, n = n->d.next) { ngx_free(p); if (n == NULL) break; } }
[1]可以看出:前面加入的内存块会首先加到5,这些内存块按照申请顺序构成链表。