(nginx源码系列四)--nginx内存池分析

原创内容,转载请注明出处

Posted by Weakyon Blog on May 4, 2015

C的内存管理难已经成为了共识。记得我那时候写ydfs,第一次接触全异步编程这种模式,没有考虑到内存的统一管理,导致溢出泄漏问题层出不穷。最终搞定没BUG了,那代码的阅读质量也是让人不敢恭维。那么来看看nginx是怎么做的吧。

nginx的内存池分为池(pool)和块(block)两个概念

这是nginx使用到的内存结构

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;//块目前已分配内存的末尾
    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;//池中块支持的最大大小,大于该大小将分配在large结构中
    ngx_pool_t           *current;//指向当前的块
    ngx_chain_t          *chain;//
    ngx_pool_large_t     *large;//大内存链表
    ngx_pool_cleanup_t   *cleanup;//清理数据的回调
    ngx_log_t            *log;//日志指针
};


typedef struct {
    ngx_fd_t              fd; 
    u_char               *name;
    ngx_log_t            *log;
} ngx_pool_cleanup_file_t;

池中以链表的形式保存许多块以及large链表。这是从数据结构中能大致看出的东西。

然后通过实际的函数来看看池的具体使用方式吧。

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;

    //设定large块的最小临界值,这个值最大不会超过ngx_pagesize大小
    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;
}

ngx_destroy_pool函数很简单,先按链表调用所有的cleanup回调做一些上层模块所需操作,然后依次释放large链表的数据,最后依次释放块。

ngx_reset_pool函数将释放所有的large内存区,然后通过重置每个块的last指针来重置普通内存块(并不释放),另外cleanup回调也不会被调用或者释放。

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

如果size大于max,那么直接申请large内存,否则寻找能插入的块,如果找不到那么就通过ngx_palloc_block来新建块。

这里要提一下ngx_align_ptr

#define ngx_align(d, a)     (((d) + (a - 1)) & ~(a - 1))
#define ngx_align_ptr(p, a)                                                   \
    (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))

可以这样理解d & ~(a-1) + (a-1)&~(a-1)

首先来理解~(a-1),a为2的幂,设右数第n位为非零位,则a-1为右数的n-1位均为1, 则有~(a-1)为最后的n-1位全为0

一个数和最后n-1位全为0的数相与,那么得到了他减去(他模a的余数)的值

然后加上(a-1)&~(a-1),就得到了对齐的数

计算宏ngx_align(1, 64)=64,只要输入d<64,则结果总是64,如果输入d=65,则结果为128,以此类推。

CPU访问不对齐的数据时,可能要进行多次内存访问。这样的对齐还是有必要的。

而NGX_ALIGNMENT的值在不同的平台上大小不一样,大致为一个字。

#define NGX_ALIGNMENT   sizeof(unsigned long)    /* platform word */

另外可以看到ngx_align_ptr的其他地方ngx_crc32.c,ngx_hash.c也有用到。

是ngx_align_ptr(p, ngx_cacheline_size)这样使用的

ngx_cacheline_size是CPU二级cache的大小

这个变量的获取涉及到不同CPU的汇编调用了,都在core/ngx_cpuinfo.c中

可以看这篇文章的解释

Nginx源码完全注释(5)core/ngx_cpuinfo.c


下面看ngx_palloc_block是如何新建块的

ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
    u_char      *m;
    size_t       psize;
    ngx_pool_t  *p, *new, *current;

    psize = (size_t) (pool->d.end - (u_char *) pool);

    //相当于malloc,但是由于malloc的内存对齐一般是以8字节为单位的,
	//更大的粒度就需要memalign了
    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;

    //这里在寻找最后一个节点,并且把扫描到的分配失败的节点状态值都加1,
	//同时如果>4那么忽略他,并且把更新当前的节点
    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;
}

从m += sizeof(ngx_pool_data_t)可以看到,后续的next指针使用的实际上都是块结构,尽管是ngx_pool_t指针。

另外pool->current指针只有在这里才会进行更新,一般只有在所有的节点错误次数都大于5以后,才会把新建的这个节点作为current记录。否则current总是存在值的,也就不会进行更新。

ngx_pnalloc是ngx_palloc的一个不对齐版本,没有ngx_memalign调用

ngx_palloc_large是简单的链表操作罢了,新的大内存总是插入链表尾

cleanup函数相关也是把回调简单的通过链表连接起来,每次把新加入的链表加入链表尾

基本上nginx的内存池就是那么多干货拉

可以看到这个内存池的核心思想有两个:

1.统一申请接口,统一进行数据销毁

2.一切内存处理全部进行内存对齐

04 May 2015