Nginx 内存池 - 原理解析
Nginx 内存池 - 原理解析
Nginx的内存池模块定义在ngx_palloc.h
和ngx_palloc.c
文件中,其中ngx_palloc.h
文件中主要定义了相关的结构体,ngx_palloc.c
文件中则是主要函数的实现。
在认识一个模块前,首先要对一个模块有大致的认识,而对一个模块产生大致认识最直观的方法就是:看图。
Nginx内存池的基本结构
我们可以从头文件下手来观察整个内存池的基本结构,我们先看看这个池子里有什么,再逐一分析里面具体的每一个组件的意义。以下为Nginx内存池结构体:
struct ngx_pool_s {
ngx_pool_data_t d; //内存池数据管理
size_t max; //ngx_pool_data_t可分配的最大内存值,超过此值则使用ngx_pool_large_t分配内存
ngx_pool_t *current; //当前内存池指针
ngx_chain_t *chain; //挂接一个ngx_chain_t结构
ngx_pool_large_t *large; //大块内存链表
ngx_pool_cleanup_t *cleanup; //释放内存池的操作集(callback函数)
ngx_log_t *log; //日志信息
};
其中的d
、large
分别为两个链表的头结点,而这两个链表为真正的内存分配的地方,小块内存分配在ngx_pool_t
节点中,大块内存分配在ngx_pool_large_t
节点中,current
则是指向当前内存池,max
表示内存池数据块ngx_pool_data_t
中可分配的最大值其,cleanup
则存放了释放内存池时用户自定义的callback
函数。
Nginx内存池中的关系图如图所示:
小块内存处理
对于用户所申请的小于等于内存池设定的max
大小的内存,内存池将内存分配在ngx_pool_data_t
这个结构体中,这个结构体的定义如下:
typedef struct {
u_char *last; //当前内存池分配到的末尾地址,即下一次分配开始地址
u_char *end; //内存池结束位置
ngx_pool_t *next; //下一块内存
ngx_uint_t failed; //当前块内存分配失败次数
} ngx_pool_data_t;
假设p
为指向ngx_pool_s
中的d
的指针,则对于小块内存的分配,Nginx内存池策略如下:
- 若当前所需的内存
size
大于当前block剩余的内存,则我们返回last
的指针作为分配的内存的首地址,并且将last
指向last+size
的位置。 - 若当前所需的小块内存
size
小于当前的p
指向的ngx_pool_data_t
的剩余的内存,则我们返回p
中last
的地址作为内存分配的首地址,并且调整p
中的last
为last + size
。 - 若当前所需的小块内存
size
大于当前的p
指向的ngx_pool_data_t
的剩余的内存,则我们开辟一块新的ngx_pool_s
结构体,并将当前d
指针指向d->next
,然后重复第一步的操作。
大块内存处理
对于用户所申请的大于内存池设定的max
大小的内存,内存池将内存分配在ngx_pool_large_t
这个结构体中,这个结构体的定义如下:
struct ngx_pool_large_s {
ngx_pool_large_t *next;
void *alloc;
};
这个结构体的定义就相对比较简单,next
指针指向大块内存链表的下一个位置,而alloc
则指向的是分配给用户的内存首地址。
释放内存操作集:ngx_pool_cleanup_s
Nginx内存池支持用户自定义回调函数来在释放内存时对外部资源进行自定义的释放操作。ngx_pool_cleanup_t
是回调函数结构体,它在内存池中一链表形式报错,在整个内存池销毁时,将循环调用该结构体中的回调函数来对资源进行释放操作。
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler; //回调函数指针
void *data; //回调函数参数
ngx_pool_cleanup_t *next; //链表中下一个结构体指针
}
Nginx内存池操作
在ngx_palloc.c
文件中,定义了所有的Nginx内存池的相关操作,包括申请内存、释放内存等。
创建内存池
创建内存池函数操作如下:
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
//为当前内存池分配第一块内存
ngx_pool_t *p;
//调用nginx的字节对齐内存分配函数为p分配size大小的内存块
p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
if (p == NULL) {
return NULL;
}
//last跨过内存块中ngx_pool_t结构体,指向紧接着的可分配内存的起始位置
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
//end指向当前size大小内存块的末尾
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;
}
可以看到对创建内存池的操作主要是对内存池结构体中的属性进行了初始化,当向系统申请内存时用到了ngx_memalign
函数,此函数在文件中的申明如下:
#define ngx_memalign(alignment, size, log) ngx_alloc(size, log)
此处的alignment主要是针对部分unix平台需要动态的对齐,大多数情况下编译器和C库都会帮我们处理对齐问题。而ngx_alloc
函数如下所示:
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_alloc
仅仅是对malloc做了一些简单的封装以及日志输出处理。
再来看看对max
的初始化处理:用户所设定的max
大小不能大于NGX_MAX_ALLOC_FROM_POOL
这个宏所设定的值,这个宏的值规定为 (ngx_pagesize - 1)
,也就是说x86中页的大小为4K,内存池最大不超过4095。
内存申请
内存申请函数如下所示:
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_palloc_small
和ngx_palloc_large
两个函数都做了什么。
ngx_palloc_small
static ngx_inline 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) {
//将last指针移向新位置
p->d.last = m + size;
return m;
}
//若当前内存块的剩余内存小于所需内存,则到下一个内存块中寻找
p = p->d.next;
} while (p);
//若所有内存块都没有合适的内存空间,则分配新的内存块。
return ngx_palloc_block(pool, size);
}
这里需要注意的几点是,ngx_align_ptr
,这是一个用来内存地址取整的宏,非常精巧,一句话就搞定了。作用不言而喻,取整可以降低CPU读取内存的次数,提高性能。因为这里并没有真正意义调用malloc
等函数申请内存,而是移动指针标记而已,所以内存对齐的活,C编译器帮不了你了,得自己动手。
#define ngx_align_ptr(p, a) \
(u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
这个函数在最后使用到了ngx_palloc_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;
//计算第一个内存块中总共可分配内存的大小
psize = (size_t) (pool->d.end - (u_char *) pool);
//分配和第一个内存块总共可分配内存同样大小的内存块
m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
if (m == NULL) {
return NULL;
}
//将申请的内存块指针转换为ngx_pool_t指针
new = (ngx_pool_t *) m;
//初始化新内存块的end、next及failed
new->d.end = m + psize;
new->d.next = NULL;
new->d.failed = 0;
//将指针m移动到d后面的一个位置,表示可分配内存的开始位置
m += sizeof(ngx_pool_data_t);
//对m指针按4字节对齐处理
m = ngx_align_ptr(m, NGX_ALIGNMENT);
//设置新内存块的last
new->d.last = m + size;
current = pool->current;
//这里的循环用来找最后一个链表节点,这里failed用来控制循环的长度,如果分配失败次数达到5次,
//就忽略,不需要每次都从头找起
for (p = current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
current = p->d.next;
}
}
p->d.next = new;
pool->current = current ? current : new;
return m;
}
ngx_palloc_large
ngx_palloc_large
用来分配申请内存大于内存池中max
的内存块,其实现主要是分配内存后,将其加入大块内存链表尾部。
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区,如果有,则将刚才分配的空间交由它管理
for (large = pool->large; large; large = large->next) {
if (large->alloc == NULL) {
large->alloc = p;
return p;
}
//查找3次都未找到空的large结构体则直接跳出循环直接创建
if (n++ > 3) {
break;
}
}
large = ngx_palloc(pool, sizeof(ngx_pool_large_t));
if (large == NULL) {
ngx_free(p);
return NULL;
}
large->alloc = p;
large->next = pool->large;
pool->large = large;
return p;
}
内存池重置
可以通过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);
}
}
//重置小块内存,并不调用free将内存交还给系统,只是指针的复位操作。
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;
}
内存释放
我们使用Nginx内存池手动申请的小块内存,在该内存池中并不提供相应小块内存的释放操作。关于Nginx内存池的释放操作,Nginx提供了一个接口:ngx_pfree
,该函数内部仅仅对指定的大块内存进行释放。
由于在分配大块内存时,只检查前三个内存块是否未分配,所以使用完大块内存之后必须及时释放。
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_destroy_pool
函数中,该函数循环调用ngx_pool_cleanup_s
中的handle函数释放资源,并释放大块内存和小块内存链表的内存块。
ngx_destroy_pool(ngx_pool_t *pool)
{
ngx_pool_t *p, *n;
ngx_pool_large_t *l;
ngx_pool_cleanup_t *c;
//循环调用handle数据清理函数
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);
}
}
//释放大块内存
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;
}
}
}
参考博文:https://www.cnblogs.com/xiekeli/archive/2012/10/17/2727432.html