Nginx 内存池(pool)分析

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示例如为:

Nginx 内存池(pool)分析_第1张图片

 

最左边的便是创建的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 示例图为:

Nginx 内存池(pool)分析_第2张图片

 

我们继续讨论第二种情况,当需要的内存大于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示例图为:       

Nginx 内存池(pool)分析_第3张图片

 

我们讨论最后一种情况,当需要的内存大于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示例图为:

Nginx 内存池(pool)分析_第4张图片

 

         至此,在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 示例图为:

Nginx 内存池(pool)分析_第5张图片

 

    在此顺带提一点,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 内存块分配的内存了,真的是这样吗?呃,这个还是留给大家自己想吧。

你可能感兴趣的:(Nginx 内存池(pool)分析)