Nginx 内存池管理的源码在src/core/ngx_palloc.h、src/core/ngx_palloc.c 两个文件中。
先将我整理的注释等内容贴上,方便下面分析:
ngx_create_pool:创建pool
ngx_destory_pool:销毁 pool
ngx_reset_pool:重置pool中的部分数据
ngx_palloc/ngx_pnalloc:从pool中分配一块内存
ngx_pool_cleanup_add:为pool添加cleanup数据
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler; // 当前 cleanup 数据的回调函数
void *data; // 内存的真正地址
ngx_pool_cleanup_t *next; // 指向下一块 cleanup 内存的指针
};
struct ngx_pool_large_s {
ngx_pool_large_t *next; // 指向下一块 large 内存的指针
void *alloc; // 内存的真正地址
};
typedef struct {
u_char *last; // 当前 pool 中用完的数据的结尾指针,即可用数据的开始指针
u_char *end; // 当前 pool 数据库的结尾指针
ngx_pool_t *next; // 指向下一个 pool 的指针
ngx_uint_t failed; // 当前 pool 内存不足以分配的次数
} ngx_pool_data_t;
struct ngx_pool_s {
ngx_pool_data_t d; // 包含 pool 的数据区指针的结构体
size_t max; // 当前 pool 最大可分配的内存大小(Bytes)
ngx_pool_t *current; // pool 当前正在使用的pool的指针
ngx_chain_t *chain; // pool 当前可用的 ngx_chain_t 数据,注意:由ngx_free_chain 赋值
ngx_pool_large_t *large; // pool 中指向大数据快的指针(大数据快是指 size > max 的数据块)
ngx_pool_cleanup_t *cleanup; // pool 中指向 ngx_pool_cleanup_t 数据块的指针
ngx_log_t *log; // pool 中指向 ngx_log_t 的指针,用于写日志的
};
使用 ngx_create_pool、ngx_destory_pool、ngx_reset_pool三个函数来创建、销毁、重置 pool。使用ngx_palloc、ngx_pnalloc、ngx_pool_cleanup_add来使用pool。使用结构体 ngx_pool_t 管理整个 pool。下面将详细分析其工作方式。
我们以 nginx 接受并处理 http 请求的方式,来分析pool的工作流程。
在 ngx_http_request.c 中,ngx_http_init_request 函数便是 http 请求处理的开始,在其中调用了 ngx_create_pool 来创建对应于 http 请求的 pool。同一个c文件中,ngx_http_free_request 函数便是 http请求处理的结束,在其中调用了 ngx_destory_pool。
我们一步步来看具体工作流程。首先,调用ngx_create_pool来创建一个pool,源码如下:
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
ngx_pool_t *p;
// 分配一块 size 大小的内存
p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
if (p == NULL) {
return NULL;
}
// 对pool中的数据项赋初始值
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; // pool 中最大可用大小
// 继续赋初始值
p->current = p;
p->chain = NULL;
p->large = NULL;
p->cleanup = NULL;
p->log = log;
return p;
}
创建完pool后,pool示例如为:
最左边的便是创建的pool内存池,其中首sizeof(ngx_pool_t)便是pool的header信息,header信息中的各个字段用于管理整个pool。由于此时刚创建,pool中除了header之外,没有任何数据。
注意:current 永远指向此pool的开始地址。current的意思是当前的pool地址,而非pool中的地址。
从代码的角度来说,pool->d.last ~ pool->d.end 中的内存区便是可用数据区。
接下来,我们使用ngx_palloc从内存池中获取一块内存,源码如下:
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
u_char *m;
ngx_pool_t *p;
// 判断 size 是否大于 pool 最大可使用内存大小
if (size <= pool->max) {
p = pool->current;
do {
// 将 m 对其到内存对齐地址
m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);
// 判断 pool 中剩余内存是否够用
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);
}
return ngx_palloc_large(pool, size);
}
此处需要分3步进行讨论。当需要的内存大于pool最大可分配内存大小时;否则,当需要的内存大于pool目前可用内存大小时;否则,当需要的内存可以在此pool中分配时。
我们先从最简单的情况开始,即,当需要的内存可以在此pool中分配时。此时从代码流程可以看到,判断内存够用后,直接移动 p->d.last 指针,令其向下偏移到指定的值即可,使用此种方式可以避免新分配内存的系统调用,效率大大提高。此时的 pool 示例图为:
我们继续讨论第二种情况,当需要的内存大于pool目前可用内存大小时。从代码流程可以看到,此时首先寻找pool数据区中的下一个节点,看是否有够用的内存,如不够,则调用ngx_palloc_block 重新分配,我们将问题简单化,由于刚创建pool,pool->d.next指针为NULL,所以肯定会重新分配一块内存。源码如下:
static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
u_char *m;
size_t psize;
ngx_pool_t *p, *new, *current;
// 先前的整个 pool 的大小
psize = (size_t) (pool->d.end - (u_char *) pool);
// 在内存对齐了的前提下,新分配一块内存
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 += sizeof(ngx_pool_data_t);
m = ngx_align_ptr(m, NGX_ALIGNMENT);
new->d.last = m + size;
current = pool->current;
// 判断在当前 pool 分配内存的失败次数,即:不能复用当前 pool 的次数,
// 如果大于 4 次,这放弃在此 pool 上再次尝试分配内存,以提高效率
for (p = current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
current = p->d.next;
}
}
// 让旧指针数据区的 next 指向新分配的 pool
p->d.next = new;
// 更新 current 指针
pool->current = current ? current : new;
return m;
}
通过上面可以看到,nginx 重新分配了一个新pool,新pool大小跟之前的大小一样,然后对 pool 赋初始值,最终将新pool串到老pool的后面。注意,此处新pool的current指针目前没有起用,为NULL。另外,在此处会判断一个pool尝试分配内存失败的次数,如果失败次数大于4(不等于4),则更新current指针,放弃对老pool的内存进行再使用。此时的pool示例图为:
我们讨论最后一种情况,当需要的内存大于pool最大可分配内存大小时,此时首先判断size已经大于pool->max的大小了,所以直接调用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;
// 重新申请一块大小为 size 的新内存
// 注意:此处不使用 ngx_memalign 的原因是,新分配的内存较大,对其也没太大必要
// 而且后面提供了 ngx_pmemalign 函数,专门用户分配对齐了的内存
p = ngx_alloc(size, pool->log);
if (p == NULL) {
return NULL;
}
n = 0;
// 查找可复用的 large 指针
for (large = pool->large; large; large = large->next) {
// 判断当前 large 指针是否指向真正的内存,否则直接拿来用
// ngx_free 可使此指针为 NULL
if (large->alloc == NULL) {
large->alloc = p;
return p;
}
// 如果当前 large 后串的 large 内存块数目大于 3 (不等于3),
// 则直接去下一步分配新内存,不再查找了
if (n++ > 3) {
break;
}
}
// 为 ngx_pool_large_t 分配一块内存
large = ngx_palloc(pool, sizeof(ngx_pool_large_t));
if (large == NULL) {
ngx_free(p);
return NULL;
}
// 将新分配的 large 串到链表后面
large->alloc = p;
large->next = pool->large;
pool->large = large;
return p;
}
由如上代码可知,函数首先申请一块大小为size的内存,然后判断当前 large 链表中是否有存在复用的可能性,有的话,当然直接赋值返回;如果没有,则新分配一块大小为sizeof(ngx_pool_large_t)的内存,串到large链表的后面。我们继续上面的例子,由于之前没有分配过large内存,所以此时直接将新内存块串起来。此时pool示例图为:
至此,在pool中分配普通内存的情况我们就讨论完了。如果有新内存需要分配,无非也就是在pool中直接移动last指针,next、large next指针后面串接新的内存块而已。
我们接下来看看函数ngx_pool_cleanup_add,在pool中分配带有handler的内存,先上源码:
ngx_pool_cleanup_t *
ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
{
ngx_pool_cleanup_t *c;
// 首先申请 sizeof(ngx_pool_cleanup_t) 大小的内存作为header信息
c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
if (c == NULL) {
return NULL;
}
if (size) {
// cleanup 中有内存大小的话,分配 size 大小的内存空间
c->data = ngx_palloc(p, size);
if (c->data == NULL) {
return NULL;
}
} else {
c->data = NULL;
}
// 对 cleanup 数据结构其他项进行赋值
c->handler = NULL;
c->next = p->cleanup;
// 将 cleanup 数据串进去
p->cleanup = c;
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);
return c;
}
我们看到源码首先分配 header 大小的头信息内存,然后判断是否要真正分配内存,如果要的话,分配内存,最后将新的数据块串起来。我们继续上面的示例图,将分配一个 cleanup 之后的示例图画出。此时 pool 示例图为:
在此顺带提一点,pool 中的 chain 指向一个 ngx_chain_t 数据,其值是由宏 ngx_free_chain 进行赋予的,指向之前用完了的,可以释放的ngx_chain_t数据。由函数ngx_alloc_chain_link进行使用。
接下来我们通过上面的图讨论一下ngx_reset_pool函数,源码:
void
ngx_reset_pool(ngx_pool_t *pool)
{
ngx_pool_t *p;
ngx_pool_large_t *l;
// 释放 large 数据块的内存
for (l = pool->large; l; l = l->next) {
if (l->alloc) {
ngx_free(l->alloc);
}
}
// 将 pool 直接下属 large 设为 NULL 即可,无需再上面的 for 循环中每次都进行设置
pool->large = NULL;
// 重置指针位置,让 pool 中的内存可用
for (p = pool; p; p = p->d.next) {
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
}
}
可以看到,代码相当简单,将large、pool 中原有内存还原到初始状态而已。
最后我们讨论一下ngx_destory_pool函数,销毁创建的pool,源码:
void
ngx_destroy_pool(ngx_pool_t *pool)
{
ngx_pool_t *p, *n;
ngx_pool_large_t *l;
ngx_pool_cleanup_t *c;
// 调用 cleanup 中的 handler 函数,清理特定资源
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);
}
}
// 释放 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);
}
}
// 释放整个 pool
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
ngx_free(p);
if (n == NULL) {
break;
}
}
}
代码也相当简单,首先调用 cleanup 中的handler函数来清理特定资源,然后释放large内存,最终释放整个pool。
最终整个pool就销毁的无影无踪了。细心的朋友可能会发现,销毁时似乎忘了释放 cleanup 内存块分配的内存了,真的是这样吗?呃,这个还是留给大家自己想吧。