内存池结构
1. 主要数据结构
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;
ngx_pool_large_t *large;
ngx_pool_cleanup_t *cleanup;
ngx_log_t *log;
};
上述各变量的解释将在下文中陆续予以介绍。
2. 图示内存池
根据用户申请内存的大小,将申请内存分为大块内存和小块内存,两种不同规格的内存进行区别对待。
三个主要变量: last 与 end 限定了可用内存区间,next指向下一个内存块。
注意:只有第一个内存块才记录max,current等变量,其余内存块,这块区间用于向用户提供可用内存。
对于小块内存,直接将last与end限定的内存分配给用户使用,而大块内存如下:
大块内存
struct ngx_pool_large_s {
ngx_pool_large_t *next;
void *alloc;
};
alloc 指向分配的内存地址,用malloc进行分配,而next则指向下一个大块内存结构。
通过上面的结束,我们对内存池的结构有了大致的了解,下面着重关心一下内存池的创建过程。
内存池创建
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
ngx_pool_t *p;
p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
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;
}
主要就是创建第一块内存块,初始化相关变量,last与end限定可用空间, max 为 size 与 NGX_MAX_ALLOC_FROM_POOL的小者。
这里有一个内存地址对齐的概念,NGX_POOL_ALIGNMENT 为16, 即希望对齐系数为16。这样,系统返回的内存地址为16的倍数。
void *
ngx_memalign(size_t alignment, size_t size, ngx_log_t *log)
{
return memalign(alignment, size);
}
max的取值: NGX_MAX_ALLOC_FROM_POOL默认为一页,即小块内存最大为一页,当然,前提是size要大于一页。
当申请的内存大于max后,将启动大块内存分配策略。
内存的分配
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
u_char *m;
ngx_pool_t *p;
if (size <= pool->max) {
p = pool->current;
do {
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);
}
return ngx_palloc_large(pool, size);
}
ngx_palloc负责内存的分配工作, 当申请的size大于max时,采用ngx_palloc_large大块内存分配策略。而如果当前current指向的内存块剩余空间小于size,
则调用ngx_palloc_block重新分配一个新的内存块。
内存池销毁
内存销毁之前,需要进行一些清理工作,由cleanup指向挂载函数。
typedef void (*ngx_pool_cleanup_pt)(void *data);
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler;
void *data;
ngx_pool_cleanup_t *next;
};
handler指向清理函数,data为传递给handler的参数,next指向下一个清理结构。
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 (size) {
c->data = ngx_palloc(p, size);
if (c->data == NULL) {
return NULL;
}
} else {
c->data = NULL;
}
c->handler = NULL;
c->next = p->cleanup;
p->cleanup = c;
return c;
}
简单来说做了两件事:其一,分配ngx_pool_cleanup_t 与 data 空间;其二,将新分配的结构串在cleanup链上。
具体结构如下图:
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; /* void */; p = n, n = n->d.next) {
ngx_free(p);
if (n == NULL) {
break;
}
}
}
内存池的销毁总共分三步:
1. 运行清理函数
2. 释放大块内存
3. 释放内存块
清理函数
上面介绍了清理函数的调用过程,下面说一下,清理函数的挂载过程。以关闭文件描述符为例:
数据结构:
typedef struct {
ngx_fd_t fd;
u_char *name;
ngx_log_t *log;
} ngx_pool_cleanup_file_t;
void
ngx_pool_cleanup_file(void *data)
{
ngx_pool_cleanup_file_t *c = data;
ngx_close_file(c->fd);
}
ngx_pool_cleanup_t *cln = ngx_pool_cleanup_add(pool, sizeof(ngx_pool_cleanup_file_t));
if (cln == NULL) {
return NGX_ERROR;
}
cln->handler = ngx_pool_cleanup_file;
ngx_pool_cleanup_file_t *cln_f = cln->data;
cln_f->fd = /* fd */;
cln_f->name = /* name */;
cln_f->long = pool->log;
杂项
current 变量:
static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
new = (ngx_pool_t *) m;
new->d.failed = 0;
current = pool->current;
for (p = current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
current = p->d.next;
}
}
p->d.next = new;
pool->current = current ? current : new;
return m;
}
其一:ngx_palloc负责分配内存,由current变量指向目前正在使用的内存块,而current之前的内存块都已经被耗尽。
其二:ngx_palloc_block 被调用时,说明已有内存池的空闲内存不足以分配size大小,因此需要增加新内存块。而此时,不能说明内存池可用内存已经耗尽,可能由于此次申请的size过大。所以,不能轻易调整current的指向。
因此nginx有一个策略是,分配新块后,从current指向的内存块依次向后扫描,failed > 4, 则默认current指向的内存块
可用空间已经耗尽,需要调整current,否则不予调整。(failed++说明:调用ngx_palloc_block说明前面分配失败)
第一块,第二块内存已经耗尽,剩余小块不足以分配,直接舍弃。第四块是由于申请了一个过大的size导致新建内存块,但第三块仍然有未用空间,所以current不能指向第四块。
内存释放:
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;
}
可见,内存的释放只针对大块内存,仅仅释放alloc指向的内存,而不释放ngx_pool_large_t结构体,这个结构体是要被重用的。
static void *
ngx_palloc_large(ngx_pool_t *pool, size_t size)
{
p = ngx_alloc(size, pool->log);
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(pool, sizeof(ngx_pool_large_t));
large->alloc = p;
large->next = pool->large;
pool->large = large;
return p;
}
从头开始查找,若找到空余位置,则放置大块内存。如果前三块都没有空闲ngx_pool_large_t结构体,则重新分配。
内存池优点
1. 避免内存泄露:分配的内存会在请求结束时一次性销毁
2. 加快分配速度:减少系统调用次数,快速分配内存
3. 减少内存碎片:其一,大小内存块区别对待,降低内部碎片; 其二,一次申请一片连续区域,减少外部碎片。