关于Nginx中内存管理的这一块,个人觉的这个是非常值得看的。里面有很多思路都很精妙,需要细细的揣摩。总的来说内存池的管理分为两大块,以页面的大小为分界线,不超过一个页面大小的内存块在内存池中进行管理,当超过一个页面大小后,就按需来分配,需要多少直接从内存中malloc了。
这里先看下涉及到的几个结构体。
typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t;
//这个机构体主要是用来释放pool中特殊资源,比如说关闭文件
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler; //释放资源的操作符
void *data;
ngx_pool_cleanup_t *next;
};
typedef struct ngx_pool_large_s ngx_pool_large_t;
//当申请的空间超过一个内存页面大小的时候,直接由有这个结构体来管理
struct ngx_pool_large_s {
ngx_pool_large_t *next;
void *alloc; //按需申请空间
};
typedef struct {
u_char *last; //当前节点空间已经分配到位置, end - last就是剩余空间。
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; //描述当前内存池的使用情况
/*以下字段只在第一个节点上有效,在链表的后面节点都会当做数据区来处理*/
size_t max; //pool节点最大的可用空间
ngx_pool_t *current; //这个节点的含义比较复杂,可以参考后面ngx_plalloc_block函数的解析来理解
ngx_chain_t *chain; //这里实际上是缓存了整个系统中用到的chain节点,需要用的时候直接从这个链表中去获取
ngx_pool_large_t *large; //当pool的数据区完全分配都不能满足空间需求的时候,需要从这里申请空间
ngx_pool_cleanup_t *cleanup; //特殊资源释放的结构体
ngx_log_t *log; //
};
struct ngx_pool_s 等价于ngx_pool_t这个结构体在源码中随处可见,因为nginx中有关于内存申请和使用的都得从这来,所以这个结构体是相当的重要,各个字段的含义见结构体中的注释。 特别说明的是ngx_pool_t的数据区的最大空间大小为NGX_MAX_ALLOC_FROM_POOL=ngx_pagesize-1,也就是一个内存页的大小,linux上一般是4k。
在Nginx中的内存池管理,实际上每次空间的申请是从一个由ngx_pool_t的链表中从头到尾遍历所有节点去查看是否满足需求,一旦满足就分配,这个里面有一个优化:就是在每个节点上有有一个字段failed,这个字段是用来记录从当前节点查找空间时查找失败的次数,如果次数较多——当前设置的值是4次,那么下次再在分配空间的时候就不会从头开始了,而是从失败次数不多余4次的节点开始,这个地方就涉及current值的修改,所以说current这个指针不一定是指向自己节点的起始位置,而是整个内存池中第一个failed字段值不大于4的那个节点。
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
ngx_pool_t *p;
/*
这个函数已经在上一篇文章中介绍过了,这个函数的功能是申请size大小的空间,
按照NGX_POOL_ALIGNMENT对齐,这里对齐的位数为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;
}
这个函数很有意思,至少在我看代码之前没有这么用过,它先申请一个size大小的空间,很可能这个空间大小是远远大于sizeof(ngx_pool_t)的, 因为在对结构体进行内存分配的时候,是按照顺序来分配的,所以后面就空出了一大段空间,自然而然的后面那段空间就成了数据区了。然后用成员区的一个结构体 ngx_pool_data_t来管理,这样就不需要单独为数据区去再调用一次malloc了。大概示意图如下:
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;
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); //用回调函数来释放自己空间
}
}
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);
}
}
#if (NGX_DEBUG)
/*
* we could allocate the pool->log from this pool
* so we cannot use this log while free()ing the pool
*/
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
"free: %p, unused: %uz", p, p->d.end - p->d.last);
if (n == NULL) {
break;
}
}
#endif
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
ngx_free(p);
if (n == NULL) {
break;
}
}
}
销毁过程还是遵循一贯的原则: “自里向外”——先把结构体里面的结构体申请的空间释放掉,然后再释放外一层空间,值得注意的是: 在释放内存的nxg_pool_cleanup_t这个结构体内的空间的时候,这个结构体本身是挂载了自己的毁掉函数来销毁自己的空间,而不是用简单的free来处理,至于为什么这么做还需要往后看才能看明白原因。最后在释放空间的时候调用的ngx_free这个函数实际上是对free的一个宏定义,这里只是为了保持函数命名一致。
void
ngx_reset_pool(ngx_pool_t *pool)
{
ngx_pool_t *p;
ngx_pool_large_t *l;
for (l = pool->large; l; l = l->next) {
if (l->alloc) {
ngx_free(l->alloc);
}
}
pool->large = NULL;
for (p = pool; p; p = p->d.next) {
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
}
}
这是关于pool重置的函数,主要是把large区的空间都释放掉了, 然后把每个ngx_pool_t的节点指针初始化到其实位置。
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
u_char *m;
ngx_pool_t *p;
if (size <= pool->max) {
/*如果所申请的空间大小没有超过一个ngx_pool_t的数据区大小,那么必须从这个节点中申请,如果
在当前内存池中没有找到合适的,那么可以新开一个节点,然后分配空间满足这次申请。
*/
p = pool->current;
do {
/*按照NGX_ALIGNMENT对齐,实际上是系统的字长。
#define ngx_align_ptr(p, a) \
(u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
*/
m = ngx_align_ptr(p->d.last, 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); //当前的内存池中没有足够的空间了,那么需要申请新的pool节点,然后再进行分配。
}
//当当前申请的空间超过了单个内存池的上限后,需要从pool的large字段获取空间。
return ngx_palloc_large(pool, size);
}
这个函数从名字来看大概能知道是分配空间用的,因为有alloc这个片段,当时前面加了一个p,那么猜测一下就知道是从pool中分配空间了。这个函数里应用了ngx_palloc_block和ngx_palloc_large这两个函数,那么先看看这两个函数是怎么实现的。
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的大小,直接由当前的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);
//这里可以看出,新申请的节点从ngx_pool_data_t后面的空间都被当做数据区了,
//内存池管理的节点实际上就是第一个节点。
m = ngx_align_ptr(m, NGX_ALIGNMENT);
new->d.last = m + size;
current = pool->current;
for (p = current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
//如果当前pool节点失败次数>4次了,那么下次再申请空间的时候就
//从当前节点的下一个节点开始。
current = p->d.next;
}
}
p->d.next = new;
//更新遍历的起始节点
pool->current = current ? current : new;
return m;
}
创建一个新的节点,不过这个心的节点和之前 ngx_create_pool函数创建的节点有所不同,这里新串讲的节点结构体中除了ngx_pool_data_t之外其他的字段都被抹掉了,直接当做数据区来对待,并且会随着一个节点failed字段的变化会调整首节点中current指针指向的地址。
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) {
/*
在large链表中找到一个没有被使用的节点,如果在寻找
large链表的过程中,寻找到了一个空的large节点并且搜寻的节点数
不超过三个,那么直接把这个新节点加入到链表后面
*/
if (large->alloc == NULL) {
large->alloc = p;
return p;
}
if (n++ > 3) {
break;
}
}
/*
1,如果large链表中的节点数超过3个
2,如果是第一个节点
满足这两个条件中的一个都会走到这个分支来。
*/
//分配large节点
large = ngx_palloc(pool, sizeof(ngx_pool_large_t));
if (large == NULL) {
ngx_free(p);
return NULL;
}
//将新的节点加入链表头部
large->alloc = p;
large->next = pool->large;
pool->large = large;
return p;
}
按需分配新的节点,并且将新的节点加入的large链表中。
另外看看和ngx_palloc函数类似的函数
void *ngx_pnalloc(ngx_pool_t *pool, size_t size);
void *ngx_pcalloc(ngx_pool_t *pool, size_t size);
这两个函数实际上和ngx_palloc的功能几乎一样,略微有差别的地方是
ngx_pnalloc在申请空间的时候没有对齐操作
ngx_pcalloc实际上是调用了ngx_pcalloc,然后对返回来的内存做了一个赋值操作,初始化为0.
void *
ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment)
{
void *p;
ngx_pool_large_t *large;
//字节对齐分配内存
p = ngx_memalign(alignment, size, pool->log);
if (p == NULL) {
return NULL;
}
//分配large节点
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;
}
ngx_pmemalign这个函数实际上是分配大空间用的,但是在分配large的内存空间的时候做了字节对齐操作。
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) { //找到对应的指针。然后进行free操作
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
"free: %p", l->alloc);
ngx_free(l->alloc);
l->alloc = NULL;
return NGX_OK;
}
}
return NGX_DECLINED;
}
ngx_pfree这个函数其功能是用来释放large链表中某个指定节点的。
接下来看看pool中特殊资源的释放相关的API
/*添加特殊资源释放的操作*/
ngx_pool_cleanup_t *
ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
{
ngx_pool_cleanup_t *c;
//先为特殊资源释放的结构体申请空间
c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
if (c == NULL) {
return NULL;
}
if (size) {
//为具体资源分配空间,最后handler操作的也是这里的资源
//这个data里会描述具体的资源信息
c->data = ngx_palloc(p, size);
if (c->data == NULL) {
return NULL;
}
} else {
c->data = NULL;
}
//handler初始化为空,资源释放的回调函数
c->handler = NULL;
//加入链表
c->next = p->cleanup;
p->cleanup = c;
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);
return c;
}
下面来看一个具体的特殊资源释放操作——关闭文件。
void
ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd)
{
ngx_pool_cleanup_t *c;
ngx_pool_cleanup_file_t *cf;
for (c = p->cleanup; c; c = c->next) {
if (c->handler == ngx_pool_cleanup_file) {
//如果对应的handler是用来释放文件的操作
cf = c->data;
if (cf->fd == fd) {
c->handler(cf); //调用具体的handler来进行操作,这个handler需要在外面定义好。
c->handler = NULL;
return;
}
}
}
}
上面一直出现了一个handler来处理特殊资源,但是一直没有看到对应的实现,下面看看具体函数
void
ngx_pool_cleanup_file(void *data)
{
ngx_pool_cleanup_file_t *c = data;
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d",
c->fd);
if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
ngx_close_file_n " \"%s\" failed", c->name);
}
}
可以看到,实际上关闭文件的handler很简单,就是调用了ngx_close_file的一个函数,将文件关闭。
下面再看一个比关闭文件稍微复杂的函数——删除文件
void
ngx_pool_delete_file(void *data)
{
ngx_pool_cleanup_file_t *c = data;
ngx_err_t err;
ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d %s",
c->fd, c->name);
if (ngx_delete_file(c->name) == NGX_FILE_ERROR) {
err = ngx_errno;
if (err != NGX_ENOENT) {
ngx_log_error(NGX_LOG_CRIT, c->log, err,
ngx_delete_file_n " \"%s\" failed", c->name);
}
}
if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
ngx_close_file_n " \"%s\" failed", c->name);
}
}
实际上这个函数只是对关闭文件增加了一个文件删除操作,目前来看ngx_pool中的特殊资源也就涉及到文件的关闭和删除。