nginx的内存池相关文章已经很多了,这里写一下简单原理和最近碰到的问题。

用到的几个结构,相应说明请看注释:

//每次能从pool分配的最大内存块大小,ngx_pagesize在X86下一般是4096,即4k,也就是说每次能从pool分配的最大内存块大小为4095字节,将近4k
#define NGX_MAX_ALLOC_FROM_POOL  (ngx_pagesize - 1)

//默认pool大小:16k
#define NGX_DEFAULT_POOL_SIZE    (16 * 1024)

struct ngx_pool_large_s {  /*大块内存数据结构*/
    ngx_pool_large_t     *next;  /*其实是一个头插法的单链表,每次分配一个大块内存都将列表节点插入到这个链表的表头*/
    void                 *alloc; /*大块内存是直接用malloc来分配的,alloc就是用来保存分配到的内存地址*/
};


typedef struct {  /*一个内存池是由多个pool节点组成的链,这个结构用来链接各个pool节点和保存pool节点可用的内存区域起止地址*/
    u_char               *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;       /*当前pool节点信息*/
    size_t                max;     /*一次能分配的最大内存大小*/
    ngx_pool_t           *current; /*用来保存当前从哪个pool上分配内存的pool指针,每次分配内存都会从current指向的pool上分配*/
    ngx_chain_t          *chain;
    ngx_pool_large_t     *large;   /*大块内存列表,*/
    ngx_pool_cleanup_t   *cleanup;
    ngx_log_t            *log;
};


先看一下创建内存池的实现代码,不到20行的代码,很简单的:

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);
    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;
}



从上面的代码可以知道max最大为4095,也就是说每次申请的内存最大大小为4095字节,超出则使用大块内存(参考ngx_palloc和ngx_palloc_large的实现,这里不讲了)。

在X64下(下同),调用pool = ngx_create_pool(NGX_DEFAULT_POOL_SIZE, log)后,得到的pool内存大小及布局如下:

nginx从内存上下线_内存

last和end指针指向的内存区域就是可用空间,pool头已经占80个字节了,所以可用空间比创建时指定的pool大小少80个字节,这里是我觉得设计得不合理的地方,这个pool的大小应该等于用户在创建pool时指定的大小加上pool头大小,这样用户创建了多少就能用多少。

得到pool之后就可以从里面分配内存了,先来看一下分配内存的函数实现,也是几行代码:

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);
}



比如调用p = ngx_palloc(pool, 32);  last会向后移动32个字节,内存布局如下:

nginx从内存上下线_内存_02

假设这个pool可用空间不够了,那么会调用ngx_palloc_block分配一个与当前pool大小一样的pool,并将该pool挂在内存池链上和将last指针调整好之后直接返回给用户可用的内存地址,如下图是调用q = ngx_palloc(pool, 128);后的内存布局,左边是可用空间不够的pool,右边是ngx_palloc_block分配的pool:

nginx从内存上下线_nginx_03

以上就是nginx内存池的基本原理,首先分配一个大块内存,然后每次分配小块内存时直接修改last指针后直接返回内存地址,非常之高效。

基本原理讲完了,再来看看ngx_palloc的bug,这个bug隐藏得比较深,耗费了好几天才搞定。这个bug是我同事KawaruNagisa发现的,他也给官网提bug并accept了,相信下个版本会得到修复的。我接触nginx时间不是很长,这个bug正好有机会来研究研究nginx的内存池实现。先来看看ngx_palloc的几行代码:

void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
……
            m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);           //内存对齐,在64位系统下是8字节对齐

            if ((size_t) (p->d.end - m) >= size) {
                p->d.last = m + size;

                return m;
            }
……
}

上面的代码中本意是先得到对齐之后的m,并和end指针对比,如果end和m之间的可用空间大于等于要分配的size,那么就分配成功,并将last向后移动。但是还有另一种情况没有考虑到:假设 last=28, end=30, size = 32, end-last=2,即end和last之间只有2个字节的可用空间,那么将last 8字节对齐之后为32,即m=32,那么end-m=-2,-2再转换成size_t则变成了18446744073709551614,再跟size相比,肯定为true,接着调整last指针并返回m;而我们要分配32个字节,应该再分配一个pool并在这个pool上分配内存,显然是bug啊。用gdb模拟一下:

nginx从内存上下线_nginx从内存上下线_04

所以上面代码中if条件里应该加上p->d.end > m ,修复后的ngx_palloc应该如下:

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 (p->d.end > m && (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);
}

另一种避免这个Bug的方法是创建pool时(ngx_create_pool)指定的size要按NGX_ALIGNMENT字节对齐,否则比较容易出Bug。