Nginx内存池源码剖析
Nginx 内存池源码剖析
Nginx 源码版本: 1.13.1
Nginx 内存池的定义主要位于如下两个文件中:
- ngx_palloc.h
- ngx_palloc.c
首先是几个重要的宏定义:
它们的含义分别如下:
NGX_MAX_ALLOC_FROM_POOL
: 最多可以从内存池中取得的大小,在 x86 机器上为 4095NGX_DEFAULT_POOL_SIZE
: 默认的内存池大小,16KNGX_POOL_ALIGNMENT
: 内存池字节对齐相关NGX_MIN_POOL_SIZE
: 最小的内存池大小
其中的 ngx_align
的定义如下:
上述两个宏函数的作用分别是:(1) 将数值 d 调整到 a 的临近倍数;(2)
将指针 p 调整到 a 的临近倍数。类似于 SGI STL
中的位运算设计。
然后介绍几个重要的数据类型,它们被用来表示内存池的头部信息:
typedef struct { |
1. 调整内存边界
函数 ngx_memalign
是一个调整内存对齐的函数,分为 Windows
平台和 Unix 两种平台实现,其中 Unix 平台下的实现如下,通过两个宏
NGX_HAVE_POSIX_MEMALIGN
和 NGX_HAVE_MEMALIGN
进行控制:
/* |
其中的 ngx_alloc
函数实现如下,可以看到,其内部实现实际上调用的就是 malloc
函数来分配动态内存:
void* ngx_alloc(size_t size, ngx_log_t *log) { |
如果定义了 NGX_HAVE_POSIX_MEMALIGN
宏,则会调用如下函数:
void* ngx_memalign(size_t alignment, size_t size, ngx_log_t *log) { |
如果定义了 NGX_HAVE_MEMALIGN
宏,则会调用如下函数:
void* ngx_memalign(size_t alignment, size_t size, ngx_log_t *log) { |
如上两个函数会根据传入的 alignment
函数参数进行字节对齐。
2. 创建内存池
首先来看一下函数
ngx_create_pool
,其作用是创建一个内存池,其源码如下:
ngx_pool_t* ngx_create_pool(size_t size, ngx_log_t *log) { |
该函数根据用户传入的 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) { |
函数 ngx_pnalloc
的作用是向内存池申请 size
大小的内存,但不使用字节对齐:
void* ngx_pnalloc(ngx_pool_t *pool, size_t size) { |
函数 ngx_pcalloc
的作用是先申请内存,然后对内存块清零:
void* ngx_pcalloc(ngx_pool_t *pool, size_t size) { |
综上可以看到,向内存池申请内存时,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) { |
可以看到,在循环中,先获取了内存池头部信息的末尾位置,然后根据用户传入的
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) |
该函数会先创建一个新的内存池,该内存池的大小与之前创建的内存池大小相同,不同的只是该次级内存池只保留有
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; |
其中的 next
指针用于指向下一个大块内存池,和小块内存池一样,其也是一个链表形式的数据结构。另外的
alloc
参数则用于指向在堆中申请的大块内存空间。
如果用户需要的内存空间大于 max
字段,则会调用
ngx_palloc_large
函数来创建大块内存池,其源码如下:
void* ngx_palloc_large(ngx_pool_t *pool, size_t size) { |
该函数先调用了 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_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) { |
该函数先遍历大块内存池链表,释放大块内存池。然后遍历小块内存池链表,调整
d.last
指针的偏移,并将 failed
字段重置为
0。
注意,释放小块内存池的循环代码中,存在些许问题,由于只有 pool
指针所指的第一个小块内存池具有全局的数据信息,而后面的次级小块内存池则仅仅包含
last
、end
、next
、failed
这四个信息,但是在该循环中,是按照 ngx_pool_t
的长度来调整
last
指针的,这会使得后面的次级小块内存池在重置后浪费掉部分空间。
9. 清理回调函数
现在来考虑如下场景,如果需要申请一个大块内存,该大块内存用于存放一个如下的结构体类型:
struct Data { |
其中的 str
字段则指向了堆上的一块内存区域,如果现在调用
ngx_pfree
对该大块内存池进行释放,观察
ngx_pfree
的相关源代码可知,其并未处理 str
字段所指向的堆内存,这就会造成内存泄漏。同时由于 C
语言并不存在析构函数来进行内存的清理工作,因此 Nginx
设计了一个回调函数,用于进行内存的清理工作。
位于 ngx_pool_s
结构体中的 cleanup
字段便是做的如此工作:
struct ngx_pool_s { |
其中的三个字段的作用如下:
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
也是存放于小块内存池中的,函数最终返回一个
ngx_pool_cleanup_t
的结构,用于用户绑定回调函数。
其示意图如下:
11. 清理内存池
函数 ngx_destory_pool
的作用是清理内存池,其相关代码如下:
void ngx_destroy_pool(ngx_pool_t *pool) |
可见,它首先遍历清理用户数据的回调函数链表,调用相应的回调函数来清理内存。然后遍历大块内存池链表以释放大块内存,最后遍历小块内存池链表清理小块内存。
12. 总结
Nginx 申请内存的流程如下: