Nginx内存池源码剖析
Nginx 源码版本: 1.13.1
Nginx 内存池的定义主要位于如下两个文件中:
ngx_palloc.h
ngx_palloc.c
首先是几个重要的宏定义:
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
#define NGX_DEFAULT_POOL_SIZE (16 * 1024)
#define NGX_POOL_ALIGNMENT 16
#define NGX_MIN_POOL_SIZE \
ngx_align((sizeof(ngx_pool_t) + 2 * sizeof(ngx_pool_large_t)), \
NGX_POOL_ALIGNMENT)
它们的含义分别如下:
NGX_MAX_ALLOC_FROM_POOL
: 最多可以从内存池中取得的大小,在 x86 机器上为 4095NGX_DEFAULT_POOL_SIZE
: 默认的内存池大小,16KNGX_POOL_ALIGNMENT
: 内存池字节对齐相关NGX_MIN_POOL_SIZE
: 最小的内存池大小
其中的 ngx_align
的定义如下:
#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))
上述两个宏函数的作用分别是:(1) 将数值 d 调整到 a 的临近倍数;(2) 将指针 p 调整到 a 的临近倍数。类似于 SGI STL
中的位运算设计。
然后介绍几个重要的数据类型,它们被用来表示内存池的头部信息:
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;
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_pool_s ngx_pool_t;
![](https://img2023.cnblogs.com/blog/2026333/202212/2026333-20221220010300736-389602678.png)
1. 调整内存边界
函数 ngx_memalign
是一个调整内存对齐的函数,分为 Windows 平台和 Unix 两种平台实现,其中 Unix 平台下的实现如下,通过两个宏 NGX_HAVE_POSIX_MEMALIGN
和 NGX_HAVE_MEMALIGN
进行控制:
/*
* Linux has memalign() or posix_memalign()
* Solaris has memalign()
* FreeBSD 7.0 has posix_memalign(), besides, early version's malloc()
* aligns allocations bigger than page size at the page boundary
*/
#if (NGX_HAVE_POSIX_MEMALIGN || NGX_HAVE_MEMALIGN)
void *ngx_memalign(size_t alignment, size_t size, ngx_log_t *log);
#else
#define ngx_memalign(alignment, size, log) ngx_alloc(size, log)
#endif
其中的 ngx_alloc
函数实现如下,可以看到,其内部实现实际上调用的就是 malloc
函数来分配动态内存:
void* ngx_alloc(size_t size, ngx_log_t *log) {
void *p;
p = malloc(size);
if (p == NULL) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "malloc(%uz) failed", size);
}
ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, log, 0, "malloc: %p:%uz", p, size);
return p;
}
如果定义了 NGX_HAVE_POSIX_MEMALIGN
宏,则会调用如下函数:
void* ngx_memalign(size_t alignment, size_t size, ngx_log_t *log) {
void *p;
int err;
err = posix_memalign(&p, alignment, size);
if (err) {
ngx_log_error(NGX_LOG_EMERG, log, err, "posix_memalign(%uz, %uz) failed", alignment, size);
p = NULL;
}
ngx_log_debug3(NGX_LOG_DEBUG_ALLOC, log, 0, "posix_memalign: %p:%uz @%uz", p, size, alignment);
return p;
}
如果定义了 NGX_HAVE_MEMALIGN
宏,则会调用如下函数:
void* ngx_memalign(size_t alignment, size_t size, ngx_log_t *log) {
void *p;
p = memalign(alignment, size);
if (p == NULL) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "memalign(%uz, %uz) failed", alignment, size);
}
ngx_log_debug3(NGX_LOG_DEBUG_ALLOC, log, 0, "memalign: %p:%uz @%uz", p, size, alignment);
return p;
}
如上两个函数会根据传入的 alignment
函数参数进行字节对齐。
2. 创建内存池
首先来看一下函数 ngx_create_pool
,其作用是创建一个内存池,其源码如下:
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);
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;
}
该函数根据用户传入的 size
大小来开辟内存池,首先调用了 ngx_memalign
函数来进行字节对齐和动态内存分配,字节对齐使用的是上方的 NGX_POOL_ALIGNMENT
宏。同时可以根据不同的平台所定义的宏来调用不同的内存分配函数,如果没有相关的宏,则实质调用的是 malloc
函数来进行动态内存分配。
然后,分别初始化 d.last
、d.end
、d.next
和 d.failed
,可以看出来,d.last
指向了内存池头部信息的末尾位置,d.end
则指向了内存池的最末尾位置,如下图所示:
![](https://img2023.cnblogs.com/blog/2026333/202212/2026333-20221220010349277-417815878.png)
然后通过用 size
减去内存池头部数据的长度,得到内存池的可用空间大小。而 max
则调整为 size
和 NGX_MAX_ALLOC_FROM_POOL
的最小值,保证内存池的最大容量不超过一页。然后 current
指针则指向了当前内存池的起始地址,示意图如下:
![](https://img2023.cnblogs.com/blog/2026333/202212/2026333-20221220010426026-1404367402.png)
创建成功后,返回内存池头部地址即可。其它头部信息后面再说。
3. 向内存池申请内存
如果需要向内存池申请内存,则可用调用如下几个函数:
函数 ngx_palloc
的作用是向内存池申请 size
大小的内存,同时使用字节对齐:
void* ngx_palloc(ngx_pool_t *pool, size_t size) {
#if !(NGX_DEBUG_PALLOC)
if (size <= pool->max) {
return ngx_palloc_small(pool, size, 1);
}
#endif
return ngx_palloc_large(pool, size);
}
函数 ngx_pnalloc
的作用是向内存池申请 size
大小的内存,但不使用字节对齐:
void* ngx_pnalloc(ngx_pool_t *pool, size_t size) {
#if !(NGX_DEBUG_PALLOC)
if (size <= pool->max) {
return ngx_palloc_small(pool, size, 0);
}
#endif
return ngx_palloc_large(pool, size);
}
函数 ngx_pcalloc
的作用是先申请内存,然后对内存块清零:
void* ngx_pcalloc(ngx_pool_t *pool, size_t size) {
void *p;
p = ngx_palloc(pool, size);
if (p) {
ngx_memzero(p, size);
}
return p;
}
综上可以看到,向内存池申请内存时,Nginx 会根据用户传入的 size
参数来选择调用 ngx_palloc_small
函数和 ngx_palloc_large
函数,前者用来申请小块内存,后者用来申请大块内存,可以看到,小块内存和大块内存的分界线便是头部信息中的 max
参数。
4. 申请小块内存
如果申请的是小块内存,则调用 ngx_palloc_small
函数,其代码如下:
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);
}
可以看到,在循环中,先获取了内存池头部信息的末尾位置,然后根据用户传入的 align
参数来确定是否调用 ngx_align_ptr
对 d.last
进行字节调整,即调整内存池头部信息的末尾位置。
此后,如果内存池末尾位置减去头部信息末尾位置的大小大于等于 size
参数,即内存池可用空间大小要大于用户需要的大小,则简单的调整 d.last
指针即可,这也是 Nginx 内存池分配内存快的原因。
而如果可用空间小于用户的需求量,那么会通过 d.next
指针进入下一个内存块,由于初始化时该指针为空,则会跳出循环,转而调用 ngx_palloc_block
函数创建一个新的内存池。
我们也可以根据 next
字段的存在大概猜到,Nginx 的小块内存采用的是链表结构。
5. 创建次级内存池
这里我暂且称 ngx_palloc_block
函数所创建的内存池为次级内存池,其代码如下:
void* ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
u_char *m;
size_t psize;
ngx_pool_t *p, *new;
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;
for (p = pool->current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
pool->current = p->d.next;
}
}
p->d.next = new;
return m;
}
该函数会先创建一个新的内存池,该内存池的大小与之前创建的内存池大小相同,不同的只是该次级内存池只保留有 ngx_pool_data_t
的相关信息。然后在这个次级内存池中取出用户需要的部分,并调整相关指针、调整边界对齐等。
在最后的循环中,从内存池链表的 current
指针开始,遍历内存池链表,如果某个内存池的 failed
字段比 4 大,则表明该内存池已经分配失败至少 4 次了,说明该内存池的可用空间大小已经不足以分配新的内存空间了,于是就让 current
指向下一个内存池节点。
最后将新创建的次级内存池插入到内存池链表的末尾,返回用户所需的内存空间。
下图为小块内存池链表的相关信息,可见,由于第一个小块内存池的 failed
字段为 5,则其 current
字段则指向了下一个 failed
字段不为 4 的小块内存池,各个小块内存池之间通过 next 指针形成链表形式的数据结构。同时,可以看到,除了第一个内存池之外,后面的所有次级内存池都只有 last、end、next、failed 这四个头部信息:
![](https://img2023.cnblogs.com/blog/2026333/202212/2026333-20221220010747572-853188701.png)
6. 创建大块内存
首先来看一个关于大块内存信息的数据结构:
typedef struct ngx_pool_large_s ngx_pool_large_t;
struct ngx_pool_large_s {
ngx_pool_large_t *next;
void *alloc;
};
其中的 next
指针用于指向下一个大块内存池,和小块内存池一样,其也是一个链表形式的数据结构。另外的 alloc
参数则用于指向在堆中申请的大块内存空间。
如果用户需要的内存空间大于 max
字段,则会调用 ngx_palloc_large
函数来创建大块内存池,其源码如下:
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;
}
该函数先调用了 ngx_alloc
函数来申请堆内存,前面阅读源码我们知道,ngx_alloc
函数的底层就是调用的 malloc
函数。然后遍历大块内存池链表,如果有某个大块内存的的 alloc
字段为空,则让该字段指向新申请的堆内存。
为了效率考虑,只寻找 3 次,如果没有找到,则在小块内存池中申请一部分空间用于存放 ngx_pool_large_t
类型,且该结构的 alloc
字段指向新创建的大块堆内存,然后使用头插法放入大块内存的链表中。
大块内存的相关示意图如下所示:
![](https://img2023.cnblogs.com/blog/2026333/202212/2026333-20221220010903921-643799545.png)
7. 释放大块内存
函数 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_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_free
,即底层的 free
来释放大块内存空间。释放完成后,将 alloc
字段置为空,用于下次存放重新申请的大块内存。
注意:Nginx 内存池不存在对于小块内存的释放函数,因为从小块内存池中取出区块是通过偏移 d.last
指针来完成的,如果现在在小块内存池中有 3 块连续的内存:1、2、3,现在需要释放内存块 2,很显然,内存块 2 释放后还需要将内存块 1 和内存块 3 拼接在一起,并不高效。
如此设计的原因是因为 Nginx 的应用场景,由于 Nginx 是一个 短链接 的服务器,浏览器(即客户端)发送一个 Request 到达 Nginx 服务器并处理完成,Nginx 会给客户端返回一个 Response 响应,此时 HTTP 服务器就会主动断开 TCP 连接。即使在 HTTP 1.1 中有了 60s 的 Keep Alive 心跳时间(即返回响应后,等待 60s,如果这 60s 内客户端又发来请求,就重置这个时间,否则就主动断开连接),在超过心跳时间之后,Nginx 就可以调用 ngx_reset_pool
来重置内存池,等待下一个连接的到来。
而如果将该内存池的分配方案应用于一个长连接的服务器,那么内存池模块会持续申请小块内存,而得不到释放,则会一直申请直到服务器资源耗尽。如果需要在长连接的服务器中使用内存池模块,那么可以使用 SGI 的二级空间配置器方案。
8. 内存池重置
内存池重置操作是通过 ngx_reset_pool
函数来完成的,其源码如下:
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;
}
该函数先遍历大块内存池链表,释放大块内存池。然后遍历小块内存池链表,调整 d.last
指针的偏移,并将 failed
字段重置为 0。
注意,释放小块内存池的循环代码中,存在些许问题,由于只有 pool 指针所指的第一个小块内存池具有全局的数据信息,而后面的次级小块内存池则仅仅包含 last
、end
、next
、failed
这四个信息,但是在该循环中,是按照 ngx_pool_t
的长度来调整 last
指针的,这会使得后面的次级小块内存池在重置后浪费掉部分空间。
9. 清理回调函数
现在来考虑如下场景,如果需要申请一个大块内存,该大块内存用于存放一个如下的结构体类型:
struct Data {
char* str;
... // 其它成员
};
其中的 str
字段则指向了堆上的一块内存区域,如果现在调用 ngx_pfree
对该大块内存池进行释放,观察 ngx_pfree
的相关源代码可知,其并未处理 str
字段所指向的堆内存,这就会造成内存泄漏。同时由于 C 语言并不存在析构函数来进行内存的清理工作,因此 Nginx 设计了一个回调函数,用于进行内存的清理工作。
位于 ngx_pool_s
结构体中的 cleanup
字段便是做的如此工作:
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;
};
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;
};
其中的三个字段的作用如下:
handler
: 存放清理数据的回调函数data
: 用于存放回调函数的函数参数next
: 表示回调函数也是一个链表形式的数据结构,指向下一个回调函数结构
10. 绑定回调函数
函数 ngx_pool_cleanup_add
的作用便是用来绑定回调函数,其源代码如下:
ngx_pool_cleanup_t* ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
{
ngx_pool_cleanup_t *c;
c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
if (c == NULL) {
return NULL;
}
if (size) {
c->data = ngx_palloc(p, size);
if (c->data == NULL) {
return NULL;
}
} else {
c->data = NULL;
}
c->handler = NULL;
c->next = p->cleanup;
p->cleanup = c;
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);
return c;
}
可见,ngx_pool_cleanup_t
也是存放于小块内存池中的,函数最终返回一个 ngx_pool_cleanup_t
的结构,用于用户绑定回调函数。
其示意图如下:
![](https://img2023.cnblogs.com/blog/2026333/202212/2026333-20221220011213128-838709654.png)
11. 清理内存池
函数 ngx_destory_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);
}
}
#if (NGX_DEBUG)
/*
* we could allocate the pool->log from this pool
* so we cannot use this log while free()ing the pool
*/
for (l = pool->large; l; l = l->next) {
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
}
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p, unused: %uz", p, p->d.end - p->d.last);
if (n == NULL) {
break;
}
}
#endif
for (l = pool->large; l; l = l->next) {
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;
}
}
}
可见,它首先遍历清理用户数据的回调函数链表,调用相应的回调函数来清理内存。然后遍历大块内存池链表以释放大块内存,最后遍历小块内存池链表清理小块内存。
12. 总结
Nginx 申请内存的流程如下:
![](https://img2023.cnblogs.com/blog/2026333/202212/2026333-20221220011304571-2030540902.png)