Nginx源码学习——内存池

    Nginx特点是占有内存少,并发能力强,这得益于Nginx所采用的内存管理手段——内存池。内存池的目的是预先分配较大的内存块留作备用,当有新的内存需求时,可直接在内存池中分配(指针移动),避免了频繁系统调用(malloc/free)造成的内存碎片和性能下降等问题。Nginx内存池本质上是一个链表结构,链表的每一个节点称为一个数据块,由ngx_pool_data_t结构体描述。我们需要对内存池进行管理和分配,这依赖于ngx_pool_t结构,可以认为该结构体描述了Nginx内存池的分配管理模块。遇到分配大数据内存的情况时,还要使用ngx_pool_large_t结构体,也形成一个链表,挂接在ngx_pool_t结构体上便于Nginx程序的管理。分配的内存使用完成后,需要对内存进行释放和回收,这需要借助ngx_pool_cleanup_t结构体,同样形成一个链表,挂接在ngx_pool_t结构体上,下图形象地展示了各个结构体之间的关联关系。

Nginx源码学习——内存池_第1张图片


内存池基础数据结构

1.ngx_pool_data_t:负责内存池小块数据的管理

//内存块包含的数据   
typedef struct {
    u_char               *last;//申请过的内存的尾地址,可申请的首地址    pool->d.last ~ pool->d.end 中的内存区便是可用数据区。
    u_char               *end;//当前内存池节点可以申请的内存的最终位置  
    ngx_pool_t           *next;//下一个内存池节点ngx_pool_t,见ngx_palloc_block
    ngx_uint_t            failed;//当前节点申请内存失败的次数,   如果发现从当前pool中分配内存失败四次,则使用下一个pool,见ngx_palloc_block 
} ngx_pool_data_t

2.ngx_pool_s:负责整个内存池的管理

//内存池数据结构,链表形式存储
struct ngx_pool_s {
    ngx_pool_data_t       d;//节点数据
    size_t                max;//当前内存节点可以申请的最大内存空间
    ngx_pool_t           *current;//内存池中可以申请内存的第一个节点
    ngx_chain_t          *chain;// pool 当前可用的 ngx_chain_t 数据,注意:由 ngx_free_chain 赋值
    ngx_pool_large_t     *large;//节点中大内存块指针
    ngx_pool_cleanup_t   *cleanup;// pool 中指向 ngx_pool_cleanup_t 数据块的指针
    ngx_log_t            *log; // pool 中指向 ngx_log_t 的指针,用于写日志的
};

3.ngx_pool_large_s:负责内存池大块数据(size>max)的管理,大块内存直接向系统申请

//大块内存结构体,链表结构  
struct ngx_pool_large_s {
    ngx_pool_large_t     *next;
    void                 *alloc;//申请的内存块地址   
};

4.ngx_pool_cleaniup_s:负责内存池数据的回收,不仅可以管理内存的回收,还可以管理任何需要释放的资源,如文件资源等

//内存池pool中清理数据的用的
struct ngx_pool_cleanup_s {
    ngx_pool_cleanup_pt   handler;// 当前 cleanup 数据的回调函数  ngx_destroy_pool中执行    例如清理文件句柄ngx_pool_cleanup_file等
    void                 *data;// 内存的真正地址     回调时,将此数据传入回调函数;
    
    ngx_pool_cleanup_t   *next;// 指向下一块 cleanup 内存的指针
};


内存池基本操作

    Nginx内存池的管理涉及内存池的创建、销毁、重置、分配等操作,相关源码可以在/nginx/src/core/ngx_palloc.c文件中找到。

(1)创建内存池

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);  分配一块 size 大小的内存  内存空间16字节对齐
    if (p == NULL) {
        return NULL;
    }

    // 对pool中的数据项赋初始值
    p->d.last = (u_char *) p + sizeof(ngx_pool_t); //可用空间要减去这个头部 首sizeof(ngx_pool_t)便是pool的header信息,header信息中的各个字段用于管理整个pool
    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; //不能超过NGX_MAX_ALLOC_FROM_POOL// pool 中最大可用大小
    
    p->current = p;
    p->chain = NULL;
    p->large = NULL;
    p->cleanup = NULL;
    p->log = log;

    return p; //指向空间最顶部头部
}

该函数通过调用ngx_memalign()函数进行内存对齐和申请操作,其大小为size个字节。


(2)销毁内存池

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) { //cleanup在ngx_pool_cleanup_add赋值
        if (c->handler) {
            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                           "run cleanup: %p", c);
            c->handler(c->data);
        }
    }

    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);
        }
    }
...
    for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
        ngx_free(p);

        if (n == NULL) {
            break;
        }
    }
}
从上面源码可以看到,在整个内存池销毁过程中,主要做了三个工作:①调用数据清理函数清理内存池中cleanup链表中每个节点的数据;②清理并释放挂接在内存池分配模块上的大数据块内存;③释放内存池本身占用的内存空间。


(3)重置内存池

       重置的目的是将内存池恢复到初始分配的状态,该过程首先将挂接在内存池分配模块上的大数据块内存释放掉,然后将指向小数据块内存结尾的last指针重置到刚分配时的位置。这里需要注意小数据块内存并没有被释放,其在以后内存池使用的过程中将被覆盖更新。

void ngx_reset_pool(ngx_pool_t *pool)
{
    ngx_pool_t        *p;
    ngx_pool_large_t  *l;

    for (l = pool->large; l; l = l->next) {
        if (l->alloc) {
            ngx_free(l->alloc);
        }
    }

    for (p = pool; p; p = p->d.next) {
        p->d.last = (u_char *) p + sizeof(ngx_pool_t);
        p->d.failed = 0;
    }

    pool->current = pool;
    pool->chain = NULL;
    pool->large = NULL;
}


(4)申请内存

    ngx_palloc()函数传入的参数有两个,第一个是在哪个内存池上申请内存,第二个就是所要申请内存的大小。进入函数后,首先判断申请内存的大小是否超过该内存池允许分配的最大内存,如果没有,就从pool->current指针指向的内存池(链表)节点开始使用循环遍历以后的各个节点,找到满足申请大小的内存空间,设置p->d.last指针,并返回指向该空间的起始地址。如果遍历完整个内存池都没有找到满足申请大小的内存,则程序调用ngx_palloc_block()函数,该函数实现了对内存池中内存空间的扩展,也就是申请一个新的内存池(链表)节点,然后挂接在内存池的最后面。以上申请的内存大小没有超过该内存池允许分配的最大内存的情况,如果超过了就调用ngx_palloc_large()函数,此时申请的内存将被看作是大数据块,将从大数据块内存链表上分配。

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; //从current所在的pool数据节点开始往后遍历寻找那个节点可以分配size内存

        do {
            m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);// 将 m 对其到内存对齐地址
            if ((size_t) (p->d.end - m) >= size) {// 判断 pool 中剩余内存是否够用
                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);
}
#define ngx_align_ptr(p, a)                                                    \
    (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
可以看出,这是一个宏定义,该操作比较巧妙,用于计算以参数a对齐后的偏移指针p。实际上,我们最后分配的内存空间就是从对齐后的偏移指针开始的,这可能会浪费少数几个字节,但却能提高读取效率。

//ngx_palloc和ngx_palloc的区别是分片小块内存时是否需要内存对齐
void *ngx_pnalloc(ngx_pool_t *pool, size_t size)
{
    u_char      *m;
    ngx_pool_t  *p;

    if (size <= pool->max) {

        p = pool->current;

        do {
            m = p->d.last;

            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_pnalloc和ngx_palloc都是从内存池里分配size大小内存,他们的不同之处在于,palloc取得的内存是对齐的,pnalloc则否。

//如果前面开辟的pool空间已经用完,则从新开辟空间ngx_pool_t
static void *ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
    u_char      *m;
    size_t       psize;
    ngx_pool_t  *p, *new;

    // 先前的整个 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;

    // 判断在当前 pool 分配内存的失败次数,即:不能复用当前 pool 的次数,
    // 如果大于 4 次,这放弃在此 pool 上再次尝试分配内存,以提高效率
    //如果失败次数大于4(不等于4),则更新current指针,放弃对老pool的内存进行再使用
    for (p = pool->current; p->d.next; p = p->d.next) {
        if (p->d.failed++ > 4) {
            pool->current = p->d.next;// 更新 current 指针, 每次从pool中分配内存的时候都是从curren开始遍历pool节点获取内存的
        }
    }

    // 让旧指针数据区的 next 指向新分配的 pool
    p->d.next = new;

    return m;
}

 
  
 
  
//当需要的内存大于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;

    p = ngx_alloc(size, pool->log);
    if (p == NULL) {
        return NULL;
    }

    n = 0;

    // 查找largt链表上空余的large 指针
    for (large = pool->large; large; large = large->next) {
        if (large->alloc == NULL) { //就用这个没用的large
            large->alloc = p;
            return p;
        }

        /*
         // 如果当前 large 后串的 large 内存块数目大于 3 (不等于3),
        // 则直接去下一步分配新内存,不再查找了
        */
        if (n++ > 3) {//也就是说如果pool->large头后面连续4个large的alloc指针都被用了,则重新申请一个新的pool_larg并放到pool->large头部
            break;
        }
    }

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

(5)释放内存

    在Nginx中,小块内存并不存在提前释放这么一说,因为其占用的内存较少,不太需要提前释放。但是对于非常大的内存,如果它的生命周期远远短于所属的内存池,那么在内存池销毁之前提前释放它就变得有意义了。

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_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                           "free: %p", l->alloc);
            ngx_free(l->alloc);
            l->alloc = NULL;

            return NGX_OK;
        }
    }

    return NGX_DECLINED;
}
从其实现中可以看出,ngx_pfree()函数通过遍历内存池large链表,找到待释放的内存空间(alloc所指向的内存空间),然后调用ngx_free()函数释放内存。这里需要注意的是:ngx_pfree()函数仅仅释放了large链表上每个节点的alloc成员所占用的空间,并没有释放ngx_pool_large_t结构所占用的内存空间。如此实现的意义在于:下次分配大块内存时,会期望复用这个ngx_pool_large_t结构体。从这里可以想到,如果large链表中的元素很多,那么ngx_pfree()的遍历耗损的性能是不小的。


你可能感兴趣的:(Nginx,Nginx源码学习)