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

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分配得到的内存的首地址
};


大致数据结构图示如下:

nginx源码解析(二)-内存池与内存管理ngx_pool_t_第1张图片

二、内存管理解析

  • 内存的创建、销毁和重置
  • 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的内存池:最后结果如下:

nginx源码解析(二)-内存池与内存管理ngx_pool_t_第2张图片

  • 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   
#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);  
nginx源码解析(二)-内存池与内存管理ngx_pool_t_第3张图片


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

nginx源码解析(二)-内存池与内存管理ngx_pool_t_第4张图片

图示如下

nginx源码解析(二)-内存池与内存管理ngx_pool_t_第5张图片

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

nginx源码解析(二)-内存池与内存管理ngx_pool_t_第6张图片

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

nginx源码解析(二)-内存池与内存管理ngx_pool_t_第7张图片

你可能感兴趣的:(nginx)