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字节。开辟一个新内存块后,内存布局如下。


图:开辟小内存块

        需要注意的是,这个新开辟的内存块由由ngx_pool_t中的ngx_pool_data_t字段与数据空间组成,不包括ngx_pool_t的其它字段空间。也就是说,只有调用ngx_create_pool函数创建内存池,才会有内存池完整的头部结构。也只有内存池表头结构,才需要维护这些内存池相关的头部信息。


        而如果要从内存块中获取的空间大于pool中的max成员,则表示在所有小内存块中都没有空间容纳要获取的空间大小。因此应该使用malloc开辟一个大内存块,并加入到大内存块链表中。ngx_palloc_large函数用于开辟一个大内存块。并加入到大内存块链表

//功能:  使用malloc方式开辟一个size大小的空间,并插入到pool中大内存块链表large的头部
//<span style="white-space:pre">	</span> 注意:这个函数只有在size大于pool中的max成员时,才会被调用,用于开辟一个大内存块空间
//参数:  size 需要开辟的内存大小
//返回值:新开辟的内存块空间首地址
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;
    //遍历所有大内存链表,查找可以利用的节点
    for (large = pool->large; large; large = large->next) 
    {
	//找到可以利用的节点,则赋值内存指针。为什么有些大内存块的alloc会为null空,读者可以看下ngx_pfree这个函数就知道原因了
        if (large->alloc == NULL) 
        {
            large->alloc = p;
            return p;
        }

	//如果找了3次都没有可利用节点,则跳出循环,创建一个新节点。
	//目标是为了提高查找消息,避免内存块多时,如果没有找到空闲的内存块,需要一直遍历下去
	//这种硬编码方式可读性差,在开发过程中,最后不要采用这种方式
        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;
}

这个函数有一个判断n是否大于3的过程,这种硬编码方式不值得提倡,可读行差。虽然为了提高查找的效率,当需要查找的内存块过多时,如果前3个内存块都没满足条件,则直接分配一个大内存块。典型的空间换时间算法。

另外,为什么有些大内存块的alloc为空呢? 可以查看下ngx_pfree函数,在调用这个函数后大内存块alloc指向的空间被释放了。因此alloc指向的空间会为空,为空时表示这个大内存块可以被利用,可以使用alloc指向一个新开辟的空间。

图:获取空间中,如果需要在获取一个1k大小的空间,由于1k大于984(max成员的大小), 则需要开辟一个大内存块,内存布局如下;

图:开辟大内存块

下面是一张内存池可能的内存布局图,线条有点多,看了都头晕啊!


                                                                                                                            图: 内池池布局

在这张图中,多出了两个链表。clain链表本身用于存放http的请求报文以及响应报文。这个链表的空间不需要使用malloc开辟,而是直接从小内存块或者大内存块中获取空间。

cleanup链表: nginx处理http请求时,一个请求需要多个模块协作完成,因此各模块都可以注册一个清理回调,在http结束请求时,会调用各个模块的清理函数,释放资源,例如关闭文件等。cleanup链表也不需要使用malloc开辟,而是直接从小内存块或者大内存块中获取空间。对这两个链表知道它们是从小内存块或者大内存块获取空间就可以了,后续博文中会进行详细分析。

3、释放内存池

nginx服务器在结束一个客户端请求时,会同时销毁内池池。

//销毁内存池
void ngx_destroy_pool(ngx_pool_t *pool)
{
    ngx_pool_t          *p, *n;
    ngx_pool_large_t    *l;
    ngx_pool_cleanup_t  *c;

    //清理cleanup链表
   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);
        }
    }

    //释放大内存(在这里只是把alloc指向的使用malloc开辟的空间释放了,而并没有释放ngx_pool_large_s
    //因为ngx_pool_large_s是在小内存块中获取的空间,因此只需要把小内存块给释放就可以了)
    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);
        }
    }
    //释放小内存块
    for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) 
    {
        ngx_free(p);

        if (n == NULL) 
        {
            break;
        }
    }
}
内池池、线程池、进程池等是都通过预先分配方式避免频繁的进行系统调用,预先分配这些资源后,由应用层进行统一管理。nginx这种内存池的设计思想还是值得学习。至此nginx内存池分析完了,后续会对nginx源码的其它数据结构、http请求、http应答、反向代理、upstream进行详细分析。也会建立一个QQ群,方便大家互相交流,共同进步!


  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值