读 nginx 源码 --- ngx_palloc

328 阅读7分钟

nginx 内存配置器

像每一个专注于高性能的服务/第三方库一样,nginx 也使用了自己的内存配置器,来减少频繁堆内存分配带来的内存和速度上的 overhead.对应的代码主要是 ngx_palloc.* 文件,下面让我们开始读源码吧。

主要结构体

typedef void (*ngx_pool_cleanup_pt)(void *data);

typedef struct ngx_pool_cleanup_s  ngx_pool_cleanup_t;
// 释放内存时的回调
struct ngx_pool_cleanup_s {
    ngx_pool_cleanup_pt   handler;
    void                 *data;
    ngx_pool_cleanup_t   *next;
};


typedef struct ngx_pool_large_s  ngx_pool_large_t;
// 申请超过 pool->max 内存时使用
struct ngx_pool_large_s {
    ngx_pool_large_t     *next; 
    void                 *alloc; // 所申请的大块内存地址
};

// 管理单次批发的内存 block
typedef struct {
    u_char               *last; // 本 block free memory 开始处
    u_char               *end; // 本 block free memroy 结束处
    ngx_pool_t           *next; // ngx_pool_t 下一个可以使用内存 block
    ngx_uint_t            failed; // 
} ngx_pool_data_t;

// 整个内存池对象
struct ngx_pool_s {
    ngx_pool_data_t       d; // 使用的 block 内存
    size_t                max; // 小于 max 的内存配置申请都会从 ngx_pool_data_t 中取出
    ngx_pool_t           *current; // list 管理 ngx_pool_t
    ngx_chain_t          *chain;
    ngx_pool_large_t     *large; // large 内存块链表的 head node
    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;

主要函数接口

// ngx_alloc 和 ngx_calloc 只是对底层 malloc 的简单封装
// 仅添加了打印 debug 日志逻辑,这里不再赘述
void *ngx_alloc(size_t size, ngx_log_t *log); 
void *ngx_calloc(size_t size, ngx_log_t *log);

ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log); // 构造函数
void ngx_destroy_pool(ngx_pool_t *pool); // 析构函数
void ngx_reset_pool(ngx_pool_t *pool); // 

// 配置内存接口族,最主要的接口
void *ngx_palloc(ngx_pool_t *pool, size_t size); 
void *ngx_pnalloc(ngx_pool_t *pool, size_t size);
void *ngx_pcalloc(ngx_pool_t *pool, size_t size);
void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment);
ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);

// 注册回调相关
ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size);
void ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd);
void ngx_pool_cleanup_file(void *data);
void ngx_pool_delete_file(void *data);

构造一个 pool

首先我们来看一下如何创建一个 ngx_pool_t

/*
    构造一个大小为 size 的内存池
*/
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
    ngx_pool_t  *p;
    // 申请一块以 NGX_POOL_ALIGNMENT 对齐的内存
    p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
    if (p == NULL) {
        return NULL;
    }
    // p 所指向的内存片区,包括 ngx_pool_t 对象的内存,所以可以使用的内存
    // 起始位置为 p + sizeof(ngx_pool_t)
    p->d.last = (u_char *) p + sizeof(ngx_pool_t);
    p->d.end = (u_char *) p + size;
    // 目前仅有一块 block
    p->d.next = NULL;
    p->d.failed = 0;

    // size 是可用于分配给客端的全部内存,p->max 不得高于这个数字
    size = size - sizeof(ngx_pool_t);
    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

    // 刚刚构造的 ngx_pool_t 对象只有一个 block
    p->current = p;
    p->chain = NULL;
    p->large = NULL;
    p->cleanup = NULL;
    p->log = log;

    return p;
}

可以看到,当你以 size 大小构造一个 ngx_pool_t 对象的时候,block 实际可以使用的内存是要小于 size。

分配内存

/*
    从 pool 中配置 size 大小的内存
*/
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
    u_char      *m;
    ngx_pool_t  *p;
    // pool 中的 block 仅用于配置小于 pool->max 大小的对象
    // 回想一下,你就可以想起来,如果大于 pool->max,我们现在也没有足够的内存啊
    if (size <= pool->max) {
        // 遍历所有的内存 block
        p = pool->current;
        
        do {
            // 因为我们不知道这个内存要构建什么对象,所以要先对齐
            m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);
            // p->d.end - m 即为该 block 可用内存
            if ((size_t) (p->d.end - m) >= size) {
                // 剩余内存足够分配,修改 p->d.last 指针后返回 m 即可
                p->d.last = m + size;

                return m;
            }
            // 本块 block 剩余内存不够,使用下一块可用 block
            p = p->d.next;

        } while (p);
        // 如果没有可用的 block 的话,为 pool 新添加一个 block
        return ngx_palloc_block(pool, size);
    }
    // 使用专门逻辑处理大对象
    return ngx_palloc_large(pool, size);
}

定制内存配置器的初衷往往是避免频繁的为小对象申请和释放内存,ngx 配置器也不例外,它对于小对象的标准就是 < p->max。可以看到如果p->d 的可用内存够用,则只需要简单的修改一下指针位置,就完成了内存配置,否则我们就需要使用ngx_palloc_block函数来申请新的block。假设我们现在的 block 内存已然不足,看看这时要如何处理

为小对象构造新 block

/*
    申请新内存块
*/
static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
    u_char      *m;
    size_t       psize;
    ngx_pool_t  *p, *new, *current;
    // 计算 block 大小,注意这个值对于每个 pool 来说,都是不变的
    psize = (size_t) (pool->d.end - (u_char *) pool);
    // 新申请一个 block
    m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
    if (m == NULL) {
        return NULL;
    }

    new = (ngx_pool_t *) m;
    // 设置 block 的 end
    new->d.end = m + psize;
    new->d.next = NULL;
    new->d.failed = 0;
    // 注意这里是 sizeof(ngx_pool_data_t),我们后面新构造的 pool 除了 pool->d 字段,其他都不再需要了
    // ngx 对内存真的是抠到爆炸,这样后面新创建的 block 的可用内存要比第一个 block 略高一点
    m += sizeof(ngx_pool_data_t);
    // 对齐地址
    m = ngx_align_ptr(m, NGX_ALIGNMENT);
    // 分配 size 大小的内存
    new->d.last = m + size;
    
    // 遍历 block list 
    current = pool->current;
    for (p = current; p->d.next; p = p->d.next) {
        // failed 代表该无法满足内存分配的次数,如果一个 block 已经4次无法直接分配给客端(需要分配新block)
        // 就将 current 指向其 next 所指 block
        if (p->d.failed++ > 4) {
            current = p->d.next;
        }
    }
    
    // 插入到链表最后
    p->d.next = new;
    // 设置 current 域,如果 block list 内所有的 block 都已经 failed 4 次或以上
    // 则将 current 指向新分配的 block,否则仍指向已有 block 中的某个
    pool->current = current ? current : new;

    return m;
}

ngxin 中对于内存的使用可以说是锱铢必较,在分配新 block 的时候便可以看出。同时还要注意,当现存 block 无法满足内存申请,不代表现有 block 中就没有可用的内存了。pool.current域指向的 block 是第一个用于提供内存的 block,ngx_pool_data_t.failed 则代表的是该 block 无法满足分配请求的次数。 小内存的处理已经了解了,现在来看一下大片内存申请如何处理:

处理大片内存申请

/*
    处理大块内存申请
*/
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;
    // 查找已有的 large 块,看看是不是有已经被释放的 large 块
    for (large = pool->large; large; large = large->next) {
        // 已经申请的 large 块里面有已经释放过的,则直接将其地址指向新申请的内存即返回
        if (large->alloc == NULL) {
            large->alloc = p;
            return p;
        }
        // 仅搜索4个 large block
        if (n++ > 3) {
            break;
        }
    }
    // 没有找到已经释放过的 large 块,那么申请一个新的 large 块,注意这里申请 ngx_pool_large_t 使用的是我们的配置器!所以这个内存不用专门 free
    large = ngx_palloc(pool, sizeof(ngx_pool_large_t));
    if (large == NULL) {
        ngx_free(p);
        return NULL;
    }

    // 指针指向内存,同时使用头插法插入到 pool 所管理的 large block list 中
    large->alloc = p;
    large->next = pool->large;
    
    // 修正 large 指针
    pool->large = large;

    return p;
}

上面的逻辑比较简单,但是需要注意一点:当我们在已有链表中找到了已经释放过的 large 块的时候,我们是不可以修改 p->large 指针的,因为 p->large 指向的必须是 large block list 的头。 因为所有内存由 pool 对象管理,所以我们不需要内存后,不能自己释放,常见的内存配置器都会提供归还内存的接口,但是 nginx 中没有,只简单的提供了释放大内存的接口,还有reset 整个 pool 的接口

归还内存

/*
    释放 large mem
*/
ngx_int_t
ngx_pfree(ngx_pool_t *pool, void *p)
{
    ngx_pool_large_t  *l;
    // 遍历链表寻找内存地址匹配的 large mem
    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;
}

/*
    reset pool,相当于回收所有分配出去的内存
*/
void
ngx_reset_pool(ngx_pool_t *pool)
{
    ngx_pool_t        *p;
    ngx_pool_large_t  *l;
    // 释放所有 large mem
    for (l = pool->large; l; l = l->next) {
        if (l->alloc) {
            ngx_free(l->alloc);
        }
    }
    // 还记得 ngx_pool_large_t 是 pool block 内的内存吧,所以不需要 free
    pool->large = NULL;
    // 修改 last 指针,回收所有内存
    for (p = pool; p; p = p->d.next) {
        p->d.last = (u_char *) p + sizeof(ngx_pool_t);
    }
}

比较有趣的一点时,对比 ngx_palloc_block 中对内存的锱铢必较,这里 reset 的时候并没有显得特别精打细算,按照前面分配的逻辑,完全可以这样

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

销毁

/*
    销毁 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) {
            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                           "run cleanup: %p", c);
            c->handler(c->data);
        }
    }
    // 释放 large block list 所指内存,因为 ngx_pool_large_t 都是通过配置器申请的,所以不需要 ngx_free(l)
    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);
        }
    }
    // 释放链表中的每个 block
    for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
        ngx_free(p);

        if (n == NULL) {
            break;
        }
    }
}

销毁一个 pool 的时候代码也很直观,除了简单的 free memory 外,还引入了回调函数的设计,比较有趣。回调的机制比较简单,这里不再详细讲述,感兴趣的同学可以自己查看一下源代码。

写在最后

本次学习代码使用的是 release-1.0.0 tag,内存池的实现其实比较简单、直观,并未使用特别精细的内存配置策略。