您的位置:首页 > 运维架构 > Nginx

nginx源码解析(二)-内存池与内存管理ngx_pool_t

2017-01-12 14:56 816 查看
ngx_pool_t是一个非常重要的数据结构,在很多重要的场合都有使用,很多重要的数据结构也都在使用它。那么它究竟是一个什么东西呢?简单的说,它提供了一种机制,帮助管理一系列的资源(如内存,文件等),使得对这些资源的使用和释放统一进行,免除了使用过程中考虑到对各种各样资源的什么时候释放,是否遗漏了释放的担心。
例如对于内存的管理,如果我们需要使用内存,那么总是从一个ngx_pool_t的对象中获取内存,在最终的某个时刻,我们销毁这个ngx_pool_t对象,所有这些内存都被释放了。这样我们就不必要对对这些内存进行malloc和free的操作,不用担心是否某块被malloc出来的内存没有被释放。因为当ngx_pool_t对象被销毁的时候,所有从这个对象中分配出来的内存都会被统一释放掉。
注:笔者的nginx版本为1.10.1

一、数据结构定义

ngx_pool_t定义在[nginx源码目录]/src/core/ngx_palloc.h以及[nginx源码目录]/src/core/ngx_palloc.c,与之相关的头文件和源文件好包括[nginx源码目录]/src/os/unix/ngx_alloc.h以及ngx_alloc.c中。
struct ngx_pool_t { //内存池的管理分配模块
ngx_pool_data_t d; //内存池的数据块
size_t max; //数据块大小,可分配小块内存的最大值
ngx_pool_t *current; //指向当前或本内存池,以后的内存分配从该指针指向的内存池中分配
ngx_chain_t *chain; //该指针挂接一个ngx_chain_t结构
ngx_pool_large_t *large; //指向大块内存分配,nginx中,大块内存分配直接采用标准系统接口malloc
ngx_pool_cleanup_t *cleanup; //析构函数,挂载内存释放时需要清理资源的一些必要操作
ngx_log_t *log; //内存分配相关的日志记录
};
其中内存池的数据块ngx_pool_data_t的数据结构定义如下:
typedef struct { //内存池的数据结构模块
u_char *last; //当前内存分配结束位置,即下一段可分配内存的起始位置
u_char *end; //内存池的结束位置
ngx_pool_t *next; //链接到下一个内存池,内存池的很多块内存就是通过该指针连成链表的
ngx_uint_t failed; //记录内存分配不能满足需求的失败次数
} ngx_pool_data_t; //结构用来维护内存池的数据块

还有一个很重要的大块内存分配数据结构模块定义:
struct ngx_pool_large_t {
ngx_pool_large_t *next; //指向下一个大内存块
void *alloc; //实际利用malloc分配得到的内存的首地址
};

大致数据结构图示如下:


二、内存管理解析

内存的创建、销毁和重置
2.1 创建内存块
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); //ngx_memlign定义在ngx_alloc.h中,主要用于分配16字节边界对其的的内存块,返回指向该内存块的指针
if (p == NULL) { //NGC_POOL_ALIGNMENT定义为16
return NULL;
}

p->d.last = (u_char *) p + sizeof(ngx_pool_t); //d.last指向当前已经分配的内存的末端地址,即下一个可以分配内存的首地址
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; //NGC_MAX_ALLOC_FROM_POOL最大不超过4095B

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;

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;
}


例如在Linux(Ubantu14.04 64位)系统上申请大小为1024Bytes的内存池:最后结果如下:


2.2 内存池销毁
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);
}
}
//cleanup指向析构函数,用于执行相关的内存池销毁之前的清理工作,如文件的关闭等,
//清理函数是一个handler的函数指针挂载。因此,在这部分,对内存池中的析构函数遍历调用。

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);
}
}
//这一部分用于清理大块内存,ngx_free实际上就是标准的free函数,
//即大内存块就是通过malloc和free操作进行管理的。

#if (NGX_DEBUG)

/**
* we could allocate the pool->log from this pool
* so we can not use this log while the 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;
}
}
//只有debug模式才会执行这个片段的代码,主要是log记录,用以跟踪函数销毁时日志记录。
#endif

for (p = pool, n = pool->d.next; /** void */; p = n, n = n->d.next) {
ngx_free(p);

if (n == NULL) {
break;
}
}
}
//该片段彻底销毁内存池本身。内存池是又多个链表相连接,d.next指向下一个内存池

2.3 重置内存块

void
ngx_reset_pool(ngx_pool_t *pool) //重置poo内存池,使得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); //使得last指向ngx_pool_t后的内存地址
p->d.failed = 0;
}

pool->current = pool;
pool->chain = NULL;
pool->large = NULL;
}


从内存池中分配内存
从内存池中取得size大小的内存主要通过ngx_palloc以及ngx_pnalloc,两者的区别是,前者从内存池中取得NGX_ALIGNMENT字节对其的内陈块,而后者不考虑字节对其,NGX_ALIGNMENT定义在ngx_config.h中:#ifndef NGX_ALIGNMENT
#define NGX_ALIGNMENT sizeof(unsigned long) /* platform word */
#endif
以ngx_palloc分析:void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)
if (size <= pool->max) { //若需要分配的内存大小大于内存池中的可分配内存,直接调用ngx_palloc_large分配,否则调用ngx_palloc_small分配,1代表直接对齐
return ngx_palloc_small(pool, size, 1);
}
#endif

return ngx_palloc_large(pool, size);
}

调用的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;
//执行对齐操作,
//即以last开始,计算以NGX_ALIGNMENT对齐的偏移位置指针,
if (align) {
m = ngx_align_ptr(m, NGX_ALIGNMENT);
}

//然后计算end值减去这个偏移指针位置的大小是否满足索要分配的size大小,
//如果满足,则移动last指针位置,并返回所分配到的内存地址的起始地址;
if ((size_t) (p->d.end - m) >= size) {
p->d.last = m + size;

return m;
}
//如果不满足,则查找下一个内存池。
p = p->d.next;

} while (p);
//如果遍历完整个内存池链表均未找到合适大小的内存块供分配,则执行ngx_palloc_block()来分配。

//ngx_palloc_block()函数为该内存池再分配一个block,该block的大小为链表中前面每一个block大小的值。
//一个内存池是由多个block链接起来的。分配成功后,将该block链入该poll链的最后,
//同时,为所要分配的size大小的内存进行分配,并返回分配内存的起始地址。
return ngx_palloc_block(pool, size);
}

当需要进行ngx_palloc_block进行小内存分块时,ngx_palloc_block定义如下:
ngx_palloc_block分配的可分配内存大小和pool指向的可分配内存大小一样
该函数分配一块内存后,last指针指向的是ngx_pool_data_t结构体(大小16B)之后数据区的起始位置,而创建内存池时时,last指针指向的是ngx_pool_t结构体(大小40B)之后数据区的起始位置。
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);
//计算pool的大小,即需要分配的block的大小

m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
if (m == NULL) {
return NULL;
}
//执行按NGX_POOL_ALIGNMENT对齐方式的内存分配,假设能够分配成功,则继续执行后续代码片段。

//这里计算需要分配的block的大小
new = (ngx_pool_t *) m;

new->d.end = m + psize;
new->d.next = NULL;
new->d.failed = 0;
//执行该block相关的初始化。

m += sizeof(ngx_pool_data_t);
//让m指向该块内存ngx_pool_data_t结构体之后数据区起始位置
m = ngx_align_ptr(m, NGX_ALIGNMENT);
new->d.last = m + size;
//在数据区分配size大小的内存并设置last指针

current = pool->current;

for (p = current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
current = p->d.next;
//失败4次以上移动current指针
}
}

p->d.next = new;
//将分配的block链入内存池

pool->current = current ? current : new;
//如果是第一次为内存池分配block,这current将指向新分配的block。

return m;
}

当请求的内存分配大小size大于当前内存池的可分配内存时(由于内存池是一个链表,随着不断地申请内存,current的指针会指向不同的内存池首地址),此时会调用ngx_palloc_large,ngx_palloc_large实现如下:
//这个函数在头文件里面并没有明确声明出来,而是在源文件中定义
//即nginx在进行内存分配需求时,不会自行去判断是否是大块内存还是小块内存,
//而是交由内存分配函数去判断,对于用户需求来说是完全透明的。
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);  //下文紧接着将分析此ngx_alloc函数
if (p == NULL) {
return NULL;
}

n = 0;

//以下几行,将分配的内存链入pool的large链中,
//这里指原始pool在之前已经分配过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来管理大块内存
//执行ngx_pool_large_t结构体的分配,用于来管理large内存块。ngx_pool_large_t所需内存依然遵循从内存池中取所需内存的原则
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_alloc定义在文件src/os/unix/ngx_alloc.c中: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_destroy_pool对内存池的所有内存进行统一的释放,避免了内存泄露的可能。

三、案例分析下面以一个案例简单的分析内存池分配内存的过程:整个示例代码如下: /**
* ngx_pool_t test, to test ngx_palloc, ngx_palloc_block, ngx_palloc_large
*/

#include <stdio.h>
#include "ngx_config.h"
#include "ngx_conf_file.h"
#include "nginx.h"
#include "ngx_core.h"
#include "ngx_string.h"
#include "ngx_palloc.h"

void dump_pool(ngx_pool_t* pool)
{
while (pool)
{
printf("pool = 0x%x\n", pool);
printf("\t.d = 0x%x\n",&pool->d);
printf("\t.last = 0x%x\n", pool->d.last);
printf("\t.end = 0x%x\n", pool->d.end);
printf("\t.next = 0x%x\n", pool->d.next);
printf("\t.failed = %d\n", pool->d.failed);
printf("\t.max = %d\n", pool->max);
printf("\t.current = 0x%x\n", pool->current);
printf("\t.chain = 0x%x\n", pool->chain);
printf("\t.large = 0x%x\n", pool->large);
printf("\t.cleanup = 0x%x\n", pool->cleanup);
printf("\t.log = 0x%x\n", pool->log);
printf("available pool memory = %d\n\n", pool->d.end - pool->d.last);
pool = pool->d.next;
}
}

int main()
{
ngx_pool_t *pool;
printf("--------------------------------\n");
printf("the size of ngx_pool_t is:%d,and the size of intptr_t is:%d\n",sizeof(ngx_pool_t),sizeof(intptr_t));
printf("--------------------------------\n");
printf("the size of size_t is:%d\n",sizeof(size_t));
printf("--------------------------------\n");
printf("create a new pool:\n");
printf("--------------------------------\n");
pool = ngx_create_pool(1024, NULL);
dump_pool(pool);

printf("--------------------------------\n");
printf("alloc block 1 from the pool:\n");
printf("--------------------------------\n");
ngx_palloc(pool, 1024);
dump_pool(pool);

printf("--------------------------------\n");
printf("alloc block 2 from the pool:\n");
printf("--------------------------------\n");
ngx_palloc(pool, 1024);
dump_pool(pool);

printf("--------------------------------\n");
printf("alloc block 3 from the pool :\n");
printf("--------------------------------\n");
ngx_palloc(pool, 512);
dump_pool(pool);

ngx_pool_large_t *p;
p=pool->large;
int i=1;
while(p)
{
printf("%dth large block address is:0x%x\n",i,p);
++i;
p=p->next;
}

ngx_destroy_pool(pool);
return 0;
} 1、首先创建内存池:pool = ngx_create_pool(1024, NULL);


2、随着向内存池中申请1024Bytes数据,由于当前可分配内存为944Bytes,所以需要调用ngx_palloc_large取得所需内存:


图示如下


3、再往内存池中申请1024Bytes数据:


4、从内存池中申请512Bytes内存,由于512Bytes小于当前可分配内存912Bytes,因此调用ngx_palloc_small进行内存分配

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息