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,内存池的实现其实比较简单、直观,并未使用特别精细的内存配置策略。