nginx内存池分析
Posted 闲话技术
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了nginx内存池分析相关的知识,希望对你有一定的参考价值。
nginx 内存池管理
概述
现在程序开发中,大多数语言具备自己的gc,用来内存管理,而C、C++这种偏于底层的语言,一般的申请内存就是malloc/free,高级一点的C++采用new、delete来申请或者释放一块内存。为了避免由于频繁的malloc/free产生内存碎片,通常会在程序中实现自己的内存管理模块,即内存池。内存池的原理:程序启动时为内存池申请一块较大的内存,在程序中使用内存时,都由内存池进行分配,不再使用的内存交给内存池回收,用于再次分配。
Nginx 使用内存池对内存进行管理,内存管理的实现类似于python,把内存分配归结为大内存分配和小内存分配。一般思路就是比较申请内存的大小与同页的内存池最大值 max 比较,大于则是大内存分配,否则为小内存分配,和python的内存管理一样,小内存申请是频繁的,而大内存频率低于小内存,如何规定这个max看程序本身。
大内存的分配是在内存池之外直接向系统申请的(malloc),然后将这块内存挂到内存池头部large字段。
小块内存分配,则是从已有的内存池数据区中分配出一部分内存,回收时不会采用free释放,而是放入内存池中。
Nginx 内存管理相关文件:
src/os/unix/ngx_alloc.h/.c
内存相关的操作,封装了最基本的内存分配函数。
free / malloc / memalign / posix_memalign,分别被封装为 ngx_free,ngx_alloc / ngx_calloc, ngx_memalign
ngx_alloc:封装malloc分配内存
ngx_calloc:封装malloc分配内存,并初始化空间内容为0
src/core/ngx_palloc.h/.c
封装创建/销毁内存池,从内存池分配空间等函数。
Nginx 内存分配总流图如下:其中 size 是用户请求分配内存的大小,pool是现有内存池
其中各过程相关接口函数如下:
ngx_create_pool(size, log)
ngx_create_pool()负责创建内存池,动作序列如下:
向系统请求size参数指定大小的储备内存块(按NGX_POOL_ALIGNMENT对齐,默认16字节),逻辑上划分为(ngx_pool_t结构体+剩余内存区)两部分;
初始化ngx_pool_data_t结构体各个字段(参考前一节,last字段指向初始分配地址);
计算单次分配的内存块长度上限(max字段,最大不超过size参数值或NGX_MAX_ALLOC_FROM_POOL);
初始化其它字段(current字段指向自己)。
ngx_destroy_pool(pool)
ngx_destroy_pool()负责销毁内存池,动作序列如下:
调用各个清理回调函数;
释放各个ngx_pool_large_t结构体管理的储备内存块;
释放各个ngx_pool_data_t结构体管理的独立内存块。
ngx_reset_pool(pool)
ngx_reset_pool()负责释放零碎内存块,动作序列如下:
释放各个ngx_pool_large_t结构体管理的内存块;
简单地将各个ngx_pool_data_t结构体的last字段复原成初始分配地址,而不是释放。
ngx_palloc(pool, size)/ngx_pnalloc(pool, size)/ngx_pcalloc(pool, size)
ngx_palloc()负责分配零碎内存块,动作序列如下:
若size参数大于max字段值,转发给ngx_palloc_large()处理(调用更底层的分配函数);
以current字段所指ngx_pool_data_t结构体为起点,在单向链表中搜索第一个能执行分配动作的结构体,完成分配(分配首址按NGX_ALIGNMENT对齐);
若以上动作均告失败,转发给ngx_palloc_block()处理(先分配新的ngx_pool_data_t结构体,再分配零碎内存块)。
ngx_pnalloc()动作与ngx_palloc()类似,但不对齐分配首址。
ngx_pcalloc()将分配请求转发给ngx_palloc()处理,并对返回的零碎内存块进行清零初始化。
ngx_palloc_block(pool, size)
ngx_palloc_block()负责向系统请求新的储备内存块,最终完成分配,动作序列如下:
向系统请求指定大小的储备内存块(按NGX_POOL_ALIGNMENT对齐,默认16字节),逻辑上划分为(ngx_pool_data_t结构体+剩余内存区)两部分;
初始化各个字段(包括分配首址,按NGX_ALIGNMENT对齐);
调整current字段的指向(实际上维持着一个“... 5 5 5 4 3 2 1 0 0”的failed字段值序列),将新的ngx_pool_data_t结构体挂入单向链表中。
ngx_palloc_large(pool, size)/ngx_pmemalign(pool, size, alignment)
ngx_palloc_large负责分配不归第一层结构管理的独立内存块,动作序列如下:
调用ngx_alloc()分配新的独立内存块;
搜索第一个空闲的ngx_pool_large_t结构体,保存并返回独立内存块首址;
若前一步失败,从本内存池中分配新的ngx_pool_large_t结构体并加入单向链表中,保存并返回独立内存块首址。
ngx_pmemalign()动作与ngx_palloc_large()类似,但会按alignment参数对齐独立内存块首址。
ngx_pfree(pool, p)
ngx_pfree()负责“释放”内存块,动作序列如下:
如果待“释放”对象是独立内存块,则转调用ngx_free()释放。
实际上该函数并不真正释放零碎内存块,而是尽可能将释放动作推迟到ngx_reset_pool()/ngx_destroy_pool()中。
ngx_pool_cleanup_add(p, size)
ngx_pool_cleanup_add()负责分配新的ngx_pool_cleanup_t结构体,以便调用端注册回调函数。动作序列如下:
从本内存池中分配一个ngx_pool_cleanup_t结构体;
若size参数不为0,尝试从本内存池分配相应大小的一块内存并作为回调参数;
将新的结构体挂到单向链表中;
调整cleanup字段,保持回调函数按“先进后出”的性质(类似于压栈操作)。
ngx_pool_run_cleanup_file(p, fd)/ngx_pool_cleanup_file(data)/ngx_pool_cleanup_file(data)
ngx_pool_run_cleanup_file()负责搜索注册在回调函数链表中的、与fd参数指定文件句柄对应的回调函数并调用之。
ngx_pool_cleanup_file()提供一个关闭指定文件句柄的缺省实现函数。
ngx_pool_delete_file()提供一个删除文件并关闭相关文件句柄的缺省实现函数。
内存池基本结构
Nginx 内存池基本结构定义如下:
/* 内存池结构 */
/* 文件 core/ngx_palloc.h */
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_chain_t 结构 */
ngx_pool_large_t *large;/* 大块内存链表,即分配空间超过 max 的内存 */
ngx_pool_cleanup_t *cleanup;/* 析构函数,释放内存池 */
ngx_log_t *log;/* 内存分配相关的日志信息 */
};
/* 文件 core/ngx_core.h */
typedef struct ngx_pool_s ngx_pool_t;
typedef struct ngx_chain_s ngx_chain_t;
大内存分配相关数据结构:
typedef struct ngx_pool_large_s ngx_pool_large_t;
struct ngx_pool_large_s{
ngx_pool_large_t *next; //指向下一块大块内存
void *alloc; //指向分配的大块内存
};
内存清理相关:
typedef void (*ngx_pool_cleanup_pt)(void *data); //cleanup的callback类型
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; //下一个cleanup callback
};
typedef struct {
ngx_fd_t fd;
u_char *name;
ngx_log_t *log;
} ngx_pool_cleanup_file_t;
内存池基本机构之间的关系如下图所示:
ngx_pool_t 的逻辑结构
上面数据结构之间逻辑结构图如下: 该图是采用 UML 画的,第一行黑色粗体表示对应数据结构,第二行是结构内的成员,冒号左边是变量,冒号右边是变量的类型;
内存池的操作
创建内存池
/* 创建内存池,该函数定义于 src/core/ngx_palloc.c 文件中 */
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
ngx_pool_t *p; /* 执行内存池头部 */
/* 分配大小为 size 的内存 */
/* ngx_memalign 函数实现于 src/os/unix/ngx_alloc.c 文件中 */
p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
if (p == NULL) {
return NULL;
}
/* 以下是初始化 ngx_pool_t 结构信息 */
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); /* 可供分配的空间大小 */
/* 不能超过最大的限定值 4096B */
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 定义如下:
void *
ngx_memalign(size_t alignment, size_t size, ngx_log_t *log)
{
void *p;
int err;
err = posix_memalign(&p, alignment, size);
//该函数分配以alignment为对齐的size字节的内存大小,其中p指向分配的内存块。
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_POOL_ALIGNMENT字节对齐的size字节的内存,在src/core/ngx_palloc.h文件中:
#define NGX_POOL_ALIGNMENT 16
销毁内存池
销毁内存池由 是void ngx_destroy_pool(ngx_pool_t *pool) 函数完成。该函数遍历内存池链表,free所有内存,如果注册了clenup (也是一个链表结构),亦将遍历该 cleanup 链表结构依次调用 clenup 的 handler 清理。同时,还将遍历 large 链表,释放大块内存。
/* 销毁内存池 */
void
ngx_destroy_pool(ngx_pool_t *pool)
{
ngx_pool_t *p, *n;
ngx_pool_large_t *l;
ngx_pool_cleanup_t *c;
/* 若注册了cleanup,则遍历该链表结构,依次调用handler函数清理数据 */
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 链表,释放大块内存 */
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); /* 释放内存 */
}
}
/* 在debug模式下执行 if 和 endif 之间的代码;
* 主要是用于log记录,跟踪函数销毁时日志信息
*/
#if (NGX_DEBUG)
/*
* we could allocate the pool->log from this pool
* so we cannot use this log while free()ing the pool
*/
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 (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
ngx_free(p);
if (n == NULL) {
break;
}
}
}
重置内存池
重置内存池由 void ngx_reset_pool(ngx_pool_t *pool) 函数完成。该函数将释放所有 large,并且将 d->last 指针重新指向 ngx_pool_t 结构之后数据区的开始位置,使内存池恢复到刚创建时的位置。内存池刚开始是不包括大内存空间的,所以pool->large=NULL.
/* 重置内存池
* 定义于 src/core/ngx_palloc.c 文件中
*/
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;
}
内存分配
小块内存分配
小块内存分配,即请求分配空间 size 小于内存池最大内存值 max。小内存分配的接口函数如下四个函数:
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_palloc的过程为,先判断待分配的内存size > pool->max,如果大于则使用 ngx_palloc_large 在 large 链表里分配一段内存并返回, 如果小于测尝试从链表的 pool->current 开始遍历链表,尝试找出一个可以分配的内存,当链表里的任何一个节点都无法分配内存的时候,就调用 ngx_palloc_block 生成链表里一个新的节点, 并在新的节点里分配内存并返回, 同时, 还会将pool->current 指针指向新的位置(从链表里面pool->d.failed小于等于4的节点里找出) 。
/* 分配内存 */
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
u_char *m;
ngx_pool_t *p;
/* 若请求的内存大小size小于内存池最大内存值max,
* 则进行小内存分配,从current开始遍历pool链表
*/
if (size <= pool->max) {
p = pool->current;
do {
/* 执行对齐操作 */
m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);
/* 检查现有内存池是否有足够的内存空间,
* 若有足够的内存空间,则移动last指针位置,
* 并返回所分配的内存地址的起始地址
*/
if ((size_t) (p->d.end - m) >= size) {
p->d.last = m + size; /* 在该节点指向的内存块中分配size大小的内存 */
return m;
}
/* 若不满足,则查找下一个内存池 */
p = p->d.next;
} while (p);
/* 若遍历所有现有内存池链表都没有可用的内存空间,
* 则分配一个新的内存池,并将该内存池连接到现有内存池链表中
* 同时,返回分配内存的起始地址
*/
return ngx_palloc_block(pool, size);
}
/* 若所请求的内存大小size大于max则调用大块内存分配函数 */
return ngx_palloc_large(pool, size);
}
static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
u_char *m;
size_t psize;
ngx_pool_t *p, *new, *current;
/* 计算pool的大小,即需要分配新的block的大小 */
psize = (size_t) (pool->d.end - (u_char *) pool);
/* NGX_POOL_ALIGNMENT对齐操作 */
m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
if (m == NULL) {
return NULL;
}
/* 计算需要分配的block的大小 */
new = (ngx_pool_t *) m;
new->d.end = m + psize;
new->d.next = NULL;
new->d.failed = 0;
/* 初始化新的内存池 */
/* 让m指向该块内存ngx_pool_data_t结构体之后数据区起始位置 */
m += sizeof(ngx_pool_data_t);
/* 在数据区分配size大小的内存并设置last指针 */
m = ngx_align_ptr(m, NGX_ALIGNMENT);
new->d.last = m + size;
current = pool->current;
for (p = current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
/* 失败4次以上移动current指针 */
current = p->d.next;
}
}
/* 将分配的block连接到现有的内存池 */
p->d.next = new;
/* 如果是第一次为内存池分配block,这current将指向新分配的block */
pool->current = current ? current : new;
return m;
}
/* 直接调用palloc函数,再进行一次0初始化操作 */
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;
}
/* 按照alignment对齐分配size内存,然后将其挂到large字段,当做大块内存处理 */
void *
ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment)
{
void *p;
ngx_pool_large_t *large;
p = ngx_memalign(alignment, size, pool->log);
if (p == NULL) {
return NULL;
}
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;
}
小内存分配之后如下图所示:
上图是由3个小内存池组成的内存池模型,由于第一个内存池上剩余的内存不够分配了,于是就创建了第二个新的内存池,第三个内存池是由于前面两个内存池的剩余部分都不够分配,所以创建了第三个内存池来满足用户的需求。由图可见:所有的小内存池是由一个单向链表维护在一起的。这里还有两个字段需要关注,failed和current字段。failed表示的是当前这个内存池的剩余可用内存不能满足用户分配请求的次数,即是说:一个分配请求到来后,在这个内存池上分配不到想要的内存,那么就failed就会增加1;这个分配请求将会递交给下一个内存池去处理,如果下一个内存池也不能满足,那么它的failed也会加1,然后将请求继续往下传递,直到满足请求为止(如果没有现成的内存池来满足,会再创建一个新的内存池)。current字段会随着failed的增加而发生改变,如果current指向的内存池的failed达到了4的话,current就指向下一个内存池了。
大块内存分配
/* 分配大块内存 */
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;
/* 若在该pool之前已经分配了large字段,
* 则将所分配的大块内存挂载到内存池的large字段中
*/
for (large = pool->large; large; large = large->next) {
if (large->alloc == NULL) {
large->alloc = p;
return p;
}
if (n++ > 3) {
break;
}
}
/* 若在该pool之前并未分配large字段,
* 则执行分配ngx_pool_large_t 结构体,分配large字段内存,
* 再将大块内存挂载到pool的large字段中
*/
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;
}
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 bytes failed", size);
}
ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, log, 0, "malloc: %p:%uz", p, size);
return p;
}
/* 释放大块内存 */
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;
}
大块内存申请之后如下所示:
cleanup 资源
/* 注册cleanup;
* size 是 data 字段所指向的资源的大小;
*/
ngx_pool_cleanup_t * ngx_pool_cleanup_add(ngx_pool_t *p, size_t size);
/* 对内存池进行文件清理操作,即执行handler,此时handler==ngx_pool_cleanup_file */
void ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd);
/* 关闭data指定的文件句柄 */
void ngx_pool_cleanup_file(void *data);
/* 删除data指定的文件 */
void ngx_pool_delete_file(void *data);
/* 注册cleanup */
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;
}
/* 清理内存池的文件 */
void
ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd)
{
ngx_pool_cleanup_t *c;
ngx_pool_cleanup_file_t *cf;
/* 遍历cleanup结构链表,并执行handler */
for (c = p->cleanup; c; c = c->next) {
if (c->handler == ngx_pool_cleanup_file) {
cf = c->data;
if (cf->fd == fd) {
c->handler(cf);
c->handler = NULL;
return;
}
}
}
}
/* 关闭data指定的文件句柄 */
void
ngx_pool_cleanup_file(void *data)
{
ngx_pool_cleanup_file_t *c = data; /* 指向data所指向的文件句柄 */
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d",
c->fd);
/* 关闭指定文件 */
if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
ngx_close_file_n " \"%s\" failed", c->name);
}
}
/* 删除data所指向的文件 */
void
ngx_pool_delete_file(void *data)
{
ngx_pool_cleanup_file_t *c = data;
ngx_err_t err;
ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d %s",
c->fd, c->name);
/* 删除data所指向的文件 */
if (ngx_delete_file(c->name) == NGX_FILE_ERROR) {
err = ngx_errno;
if (err != NGX_ENOENT) {
ngx_log_error(NGX_LOG_CRIT, c->log, err,
ngx_delete_file_n " \"%s\" failed", c->name);
}
}
/* 关闭文件句柄 */
if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
ngx_close_file_n " \"%s\" failed", c->name);
}
}
以上是关于nginx内存池分析的主要内容,如果未能解决你的问题,请参考以下文章
Nginx 源码学习内存池 及 优秀案例赏析:Nginx内存池设计
14.VisualVM使用详解15.VisualVM堆查看器使用的内存不足19.class文件--文件结构--魔数20.文件结构--常量池21.文件结构访问标志(2个字节)22.类加载机制概(代码片段