nginx内存池

Posted ApeLife

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了nginx内存池相关的知识,希望对你有一定的参考价值。

一、nginx内存池的使用

        nginx采用内存池对内存进行管理。即先开辟一个内存池空间,之后就从内存池中获取内存了,避免频繁的调用malloc/free操作。如果内存池空间不够,才会调用malloc

分配一个新的内存块,并加入到内存池中。

        1、对于每一个客户端发起的http请求,nginx服务器都需要开辟空间来接收客户端的请求行,请求包头、以及请求的包体。这些都是从内存池中获取空间,并把数据存储到这些空间。

        2、nginx服务器对客户端请求的响应,分为响应包头,响应包体。这些数据也需要从内存池中获取空间并进行存储

        3、nginx作为反向代理服务器时,也要从内存池中获取空间,并存储上游服务器的响应包体。进而转发给下游客户端。

总之,nginx对内存的操作,都是从内存池中获取空间。如果内存池空间不够时,才会调用malloc重新开辟一个内存块,并加入到内存池中。当然,这样处理的缺点也是很明显的,申请一块大的内存必然会导致内存空间的浪费,但是比起频繁地malloc和free,这样做的代价是非常小的,这是典型的以空间换时间。

二、内存池源码分析

1、创建内存池

对于每一个客户端发起的请求,nginx都会为其创建一个请求结构。而每一个请求结构,都将创建一个内存池。用于接收客户端的http请求报文,以及存储发给客户端的http响应报文。创建内存池由ngx_create_pool函数完成,参数size为要创建的内存块大小,返回值为ngx_pool_t内存池句柄

ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log)
{
    ngx_pool_t  *p;

    //使用malloc开辟一个size大小的空间
    p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
    if (p == NULL)
	{
        return NULL;
    }

    //last指向数据空间
    p->d.last = (u_char *) p + sizeof(ngx_pool_t);

    //end指向整个内存块的结束位置
    p->d.end = (u_char *) p + size;
    p->d.next = NULL;
    p->d.failed = 0;

    //size表示数据空间的大小(真正存放数据的空间,不包括ngx_pool_t表头结构所占用的大小)
    size = size - sizeof(ngx_pool_t);
    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

    //current当前块执行刚创建的这个内存块
    p->current = p;
    p->chain = NULL;
    p->large = NULL;
    p->cleanup = NULL;
    p->log = log;

    return p;
}
例如:要创建一个1K大小的内存池,调用ngx_create_pool(1024, log)后的内存布局如下

图:创建内存池

last字段指向是数据空间的首地址,这片空间用于存放实际的数据。当从这个内存块获取内存后,last指针也会跟着移动,指向最新的可用空间的首地址。

end指向的是这个内存块的结束地址。

需要说明的是max字段,max表示的是小内存块最大的数据空间。在这个例子中sizeof(ngx_pool_s)占用40字节,因此申请1k的内存池,实际可以存储数据的空间只有984字节。

max字段有什么作用呢?  如果还需要在开辟一个小内存块,则这个小内存块的最大空间为max大小(由ngx_pool_data_t与实际存放的数据组成); 另外这个字段也是一个临界值,如果需要从内存池中获取的空间小于max, 则开辟一个小内存块就可以了,否则需要开辟一个大内存块。

2、从内存池中获取空间

在nginx处理客户端请求以及发送响应时,需要从内存池中获取空间,用于存储报文。以下这4个函数都可以用来从内存池中获取空间,区别是内存字节对其方式,以及获取后的内存是否进行清0处理。其它没什么差别。

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_pnalloc函数为例,分析从内存池中获取一片内存的流程。

//从内存池获取获取大小为size的内存,返回值为获取到的内存地址
void * ngx_pnalloc(ngx_pool_t *pool, size_t size)
{
    u_char      *m;
    ngx_pool_t  *p;

    //待分配的数据空间小于等于内存块实际最大数据空间,则从小内存块获取内存
    if (size <= pool->max)
    {
        p = pool->current;
        do
        {
            m = p->d.last;
  	    //该内存块剩余空间满足待获取的数据大小,则直接返回内存地址
            if ((size_t) (p->d.end - m) >= size) 
            {
                p->d.last = m + size;

                return m;
            }

	    //如果内存块甚于空间不能满足需要获取的大小,查找下一个内存块
            p = p->d.next;

        } while (p);

	//如果所有的小内存块都没有找到合适的内存,则重新开辟一个小内存块
        return ngx_palloc_block(pool, size);
    }

    //待分配的数据空间大于内存块实际最大数据空间,则从大内存块获取内存
    return ngx_palloc_large(pool, size);
}
再次强调下,max成员表示的是每一个小内存块数据空间的最大值。

1、函数首先会判断要获取的内存size是否小于等于max,如果是,则从current指向的小内存块开始遍历,直到找到一个内存块的剩余空间能够存放size大小的空间,则返回这个内存块。如果所有的小内存块都没有空间容纳size大小的空间,则会重新开辟一个小内存块,并加入到ngx_pool_data_t小内存块链表中

2、如果要获取的内存size比max还大,则说明每一个小内存块都没有空间存放size大小的内容。则需要使用malloc开辟一个大内存块,并加入到large大内存块链表中。

调用ngx_pnalloc(pool, 200)获取200字节的内存布局如下图:


图:获取空间

在这里只要last指针发生了改变,指向了最新可用空间的首地址。max字段从ngx_create_pool创建内存池开始到销毁内存池,大小都不会发生变化,是一个固定值。


//功能: 开辟一个新的小内存块,小内存块大小为ngx_pool_t中的max成员。
//	注意:新开辟的小内存块由ngx_pool_t中的ngx_pool_data_t字段加上数据空间组成,
//	不包括ngx_pool_t的其它字段空间
//参数:  size表示需要从新开辟的小内存块获取多大的空间
//返回值:获取到的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;

    //psize为实际的数据空间,也就是ngx_pool_t中的max成员
    psize = (size_t) (pool->d.end - (u_char *) pool);

    //使用malloc生成一个psize大小的内存块
    m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
    if (m == NULL) 
    {
        return NULL;
    }

    new = (ngx_pool_t *) m;

    //end指向内存块的末尾
    new->d.end = m + psize;
    new->d.next = NULL;
    new->d.failed = 0;
    //m执向内存块的数据空间
    m += sizeof(ngx_pool_data_t);
    m = ngx_align_ptr(m, NGX_ALIGNMENT);
    //last指向可以使用的空间
    new->d.last = m + size;

    current = pool->current;

    for (p = current; p->d.next; p = p->d.next) 
    {
	//如果失败超过次数,则移动current指针。之后获取空间会从新的current指向的内存块获取
        if (p->d.failed++ > 4) 
        {
            current = p->d.next;
        }
    }

    //将新开辟的小内存块插入到链表尾部
    p->d.next = new;

    pool->current = current ? current : new;

    return m;
}

       如果在所有小内存块中都没有满足条件的空间,则会调用ngx_palloc_block重新分配一个小内存块。需要注意的是failed字段: 默认情况current指针都是指向第一个内存块,在需要从内存池中获取空间时,都是从current指向的内存块开始遍历,判断当前块剩余空间是否满足条件。 而在调用ngx_palloc_block分配的小内存块时过多时,必然会有一个内存块的failed字段值大于4,则会改变current指针。之后需要在从内存池中获取内存,就会从当前的current指针指向的内存块开始遍历了,直到找到满足条件的内存块为止。也就是failed指向的内存块不会在被利用了,但很好奇这个内存块不能在被利用了,为什么不马上free呢?估计也是为了减少频繁进行free系统调用吧,等到内存时销毁时在释放空间。

        在上图的例子中,内存块中只剩下784字节的空间了。如果此时需要获取800字节的空间,则由于这个内存块剩余的空间不能满足800字节,因此需要重新开辟一个小内存块,新开辟内存块大小为ngx_pool_s中的max成员,也就是984字节。开辟一个新内存块后,内存布局如下。