Nginx底层设计与源码分析
上QQ阅读APP看书,第一时间看更新

3.2 Nginx内存池

Nginx使用内存池管理进程内内存,当接收到请求时,创建一个内存池。处理请求过程中需要的内存都从这个内存池中申请,请求处理完成后释放内存池。Nginx将内存池中的内存分为两类:小块内存、大块内存。大、小块内存的分界点是由创建内存池时的参数以及系统页大小决定的。对于小块内存,其在用户申请后并不需要释放,而是等到释放内存池时再释放。对于大块内存,用户可以调用相关接口进行释放,也可以等内存池释放时再释放。Nginx内存池支持增加回调函数,当内存池释放时,自动调用回调函数以释放用户申请的其他资源。值得一提的是,回调函数允许增加多个,通过链表进行链接,在内存池释放时被逐一调用。

3.2.1 内存池结构

与Nginx内存池相关的结构主要有3个:ngx_pool_s、ngx_pool_data_t和ngx_pool_large_s。

// ngx_pool_large_s的结构
typedef struct ngx_pool_large_s  ngx_pool_large_t;
struct ngx_pool_large_s {
    ngx_pool_large_t     *next; // 用于构成链表
    void                 *alloc; // 指向真正的大块内存
};
// ngx_pool_data_t的结构
typedef struct {
    u_char               *last;
    u_char               *end;
    ngx_pool_t           *next;
    ngx_uint_t            failed;
} ngx_pool_data_t;
// ngx_pool_s的结构
typedef struct ngx_pool_s  ngx_pool_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;
};

应用程序首先需要通过ngx_create_pool函数创建一个新的内存池,之后从新的内存池中申请内存或者释放内存池中的内存。下面先看一下如何创建内存池以及内存池的基本结构。

// 返回创建的内存池地址,size为内存池每个内存块大小,log为打印日志
ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log){
    ngx_pool_t  *p;
    // 申请内存,如果系统支持内存地址对齐,则默认申请16字节对齐地址
    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;
}

初始化的Nginx内存池结构如图3-1所示。

图3-1 初始化的Nginx内存池结构

3.2.2 申请内存

创建完内存池后,我们可以从内存池中申请内存。Nginx提供了3个API:ngx_palloc、ngx_pcalloc、ngx_pnalloc。其中,ngx_palloc是最基本的内存申请API,获取内存后并不进行初始化操作。ngx_pcalloc对ngx_ palloc进行了简单封装,其通过ngx_palloc申请内存后,对内存进行初始化。相比于ngx_pnalloc,ngx_palloc申请内存的首地址是对齐的(也就是说,申请到的内存首地址是4或者8的整数倍,与系统相关),而ngx_pnalloc没有考虑内存对齐。下面重点介绍ngx_palloc。

void * ngx_palloc(ngx_pool_t *pool, size_t size){
        if (size <= pool->max) return ngx_palloc_small(pool, size, 1);
    return ngx_palloc_large(pool, size);
}

可以看出:如果申请的内存小于等于pool->max,则认为是小块内存申请;如果大于pool->max,则认为是大块内存申请。

申请小块内存时,Nginx会先查看内存池中当前的内存块是否还有可以分配的空间,如果没有,则逐一遍历内存池中的内存块,找到则返回。如果没有找到,Nginx则会申请一个新的内存块。值得一提的是:如果某次申请没有从内存池的现有内存块中申请到内存,而是申请了一块新的内存,则会增加内存池中每个内存块的分配失败次数;如果内存块的分配失败次数超过4[1],则不会再尝试从这个内存块中申请内存。申请小块内存的源码可以参考ngx_palloc_small函数。限于篇幅,此处仅给出核心部分的代码。

static void *
ngx_palloc_small(ngx_pool_t *pool, size_t size, ngx_uint_t align){
    u_char      *m;
    ngx_pool_t  *p;
    p = pool->current;
    do {
        m = p->d.last;
        // 内存对齐
        if (align) m = ngx_align_ptr(m, 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);
}

对于大块内存申请,Nginx直接申请相应大小的内存块,通过链表将已经申请的内存块进行链接。值得一提的是,大块内存管理的链表节点ngx_pool_large_t所占用的内存是从内存池中申请的,因为这仅仅是一小块内存,便于释放。

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;
    for (large = pool->large; large; large = large->next) {
        if (large->alloc == NULL) {
            large->alloc = p;
            return p;
        }
        if (n++ > 3) break;
    }
    // 直接从内存池中申请大块内存管理的链表节点
    large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
    if (large == NULL) {
        ngx_free(p);
        return NULL;
    }
    large->alloc = p;
    // 将大块内存放到内存池中,也就是放到链表头部,便于维护管理
    large->next = pool->large;
    pool->large = large;
    return p;
}

综上,Nginx内存池基本结构如图3-2所示。

图3-2 Nginx内存池基本结构

3.2.3 释放内存

我们从内存池中申请内存后,可能需要释放内存。对于内存池中的小块内存,其并不需要进行释放,因为在释放整个内存池时会随之释放。我们可以通过ngx_pfree进行大块内存释放。下面先介绍如何释放大块内存。

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

讲解完如何释放大块内存后,我们需要知道如何释放内存池。释放内存池的逻辑比较简单,首先查看内存池是否挂载清理函数,如果是,则逐一调用链表中的所有回调函数,之后再释放大块内存,最后释放内存池中的内存块。释放内存池API接口为ngx_destroy_pool。

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;; p = n, n = n->d.next) {
        ngx_free(p);
        if (n == NULL) break;
    }
}

[1]可以看出:前面加入的内存块会首先加到5,这些内存块按照申请顺序构成链表。