NGINX 内存池

内存池结构

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. 图示内存池
根据用户申请内存的大小,将申请内存分为大块内存和小块内存,两种不同规格的内存进行区别对待。

NGINX 内存池_第1张图片
三个主要变量: last 与 end 限定了可用内存区间,next指向下一个内存块。 
注意:只有第一个内存块才记录max,current等变量,其余内存块,这块区间用于向用户提供可用内存。
对于小块内存,直接将last与end限定的内存分配给用户使用,而大块内存如下:

大块内存

struct ngx_pool_large_s {
    ngx_pool_large_t     *next;
    void                 *alloc;
};
alloc 指向分配的内存地址,用malloc进行分配,而next则指向下一个大块内存结构。
NGINX 内存池_第2张图片
通过上面的结束,我们对内存池的结构有了大致的了解,下面着重关心一下内存池的创建过程。


内存池创建

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链上。

具体结构如下图: NGINX 内存池_第3张图片

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说明前面分配失败)
NGINX 内存池_第4张图片

第一块,第二块内存已经耗尽,剩余小块不足以分配,直接舍弃。第四块是由于申请了一个过大的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.  减少内存碎片:其一,大小内存块区别对待,降低内部碎片; 其二,一次申请一片连续区域,减少外部碎片。

你可能感兴趣的:(NGINX 内存池)