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;

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
则指向了内存池的最末尾位置,如下图所示:

然后通过用 size
减去内存池头部数据的长度,得到内存池的可用空间大小。而 max
则调整为 size
和 NGX_MAX_ALLOC_FROM_POOL
的最小值,保证内存池的最大容量不超过一页。然后 current
指针则指向了当前内存池的起始地址,示意图如下:

创建成功后,返回内存池头部地址即可。其它头部信息后面再说。
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 这四个头部信息:

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
字段指向新创建的大块堆内存,然后使用头插法放入大块内存的链表中。
大块内存的相关示意图如下所示:

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
的结构,用于用户绑定回调函数。
其示意图如下:

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 申请内存的流程如下:

本文作者:Leaos
本文链接:https://www.cnblogs.com/tuilk/p/16993453.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步