Nginx 内存池 - 原理解析

Nginx 内存池 - 原理解析


​   Nginx的内存池模块定义在ngx_palloc.hngx_palloc.c文件中,其中ngx_palloc.h文件中主要定义了相关的结构体,ngx_palloc.c文件中则是主要函数的实现。

  ​ 在认识一个模块前,首先要对一个模块有大致的认识,而对一个模块产生大致认识最直观的方法就是:看图。

Nginx内存池的基本结构

​   我们可以从头文件下手来观察整个内存池的基本结构,我们先看看这个池子里有什么,再逐一分析里面具体的每一个组件的意义。以下为Nginx内存池结构体:

struct ngx_pool_s {
    ngx_pool_data_t       d;			//内存池数据管理
    size_t                max;			//ngx_pool_data_t可分配的最大内存值,超过此值则使用ngx_pool_large_t分配内存
    ngx_pool_t           *current;		//当前内存池指针
    ngx_chain_t          *chain;		//挂接一个ngx_chain_t结构
    ngx_pool_large_t     *large;		//大块内存链表
    ngx_pool_cleanup_t   *cleanup;		//释放内存池的操作集(callback函数)
    ngx_log_t            *log;			//日志信息
};

​   其中的dlarge分别为两个链表的头结点,而这两个链表为真正的内存分配的地方,小块内存分配在ngx_pool_t节点中,大块内存分配在ngx_pool_large_t节点中,current则是指向当前内存池,max表示内存池数据块ngx_pool_data_t中可分配的最大值其,cleanup则存放了释放内存池时用户自定义的callback函数。

​    Nginx内存池中的关系图如图所示:

image

小块内存处理

​   对于用户所申请的小于等于内存池设定的max大小的内存,内存池将内存分配在ngx_pool_data_t这个结构体中,这个结构体的定义如下:

typedef struct {
    u_char               *last;		//当前内存池分配到的末尾地址,即下一次分配开始地址
    u_char               *end;		//内存池结束位置
    ngx_pool_t           *next;		//下一块内存
    ngx_uint_t            failed;	//当前块内存分配失败次数
} ngx_pool_data_t;		

​   假设p为指向ngx_pool_s中的d的指针,则对于小块内存的分配,Nginx内存池策略如下:

  1. 若当前所需的内存size大于当前block剩余的内存,则我们返回last的指针作为分配的内存的首地址,并且将last指向last+size的位置。
  2. 若当前所需的小块内存size小于当前的p指向的ngx_pool_data_t的剩余的内存,则我们返回plast的地址作为内存分配的首地址,并且调整p中的lastlast + size
  3. 若当前所需的小块内存size大于当前的p指向的ngx_pool_data_t的剩余的内存,则我们开辟一块新的ngx_pool_s结构体,并将当前d指针指向d->next,然后重复第一步的操作。

大块内存处理

​   对于用户所申请的大于内存池设定的max大小的内存,内存池将内存分配在ngx_pool_large_t这个结构体中,这个结构体的定义如下:

struct ngx_pool_large_s {
    ngx_pool_large_t     *next;
    void                 *alloc;
};

​   这个结构体的定义就相对比较简单,next指针指向大块内存链表的下一个位置,而alloc则指向的是分配给用户的内存首地址。
  

释放内存操作集:ngx_pool_cleanup_s

​   Nginx内存池支持用户自定义回调函数来在释放内存时对外部资源进行自定义的释放操作。ngx_pool_cleanup_t是回调函数结构体,它在内存池中一链表形式报错,在整个内存池销毁时,将循环调用该结构体中的回调函数来对资源进行释放操作。

struct ngx_pool_cleanup_s {
    ngx_pool_cleanup_pt handler;	//回调函数指针
    void *data;					   //回调函数参数
    ngx_pool_cleanup_t *next;		//链表中下一个结构体指针
}

Nginx内存池操作

​   在ngx_palloc.c文件中,定义了所有的Nginx内存池的相关操作,包括申请内存、释放内存等。

创建内存池

  创建内存池函数操作如下:

ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
    //为当前内存池分配第一块内存
    ngx_pool_t  *p;		
	
    //调用nginx的字节对齐内存分配函数为p分配size大小的内存块
    p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
    if (p == NULL) {
        return NULL;
    }
	
    //last跨过内存块中ngx_pool_t结构体,指向紧接着的可分配内存的起始位置
    p->d.last = (u_char *) p + sizeof(ngx_pool_t);
    //end指向当前size大小内存块的末尾
    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;

    p->current = p;
    p->chain = NULL;
    p->large = NULL;
    p->cleanup = NULL;
    p->log = log;

    return p;
}

​   可以看到对创建内存池的操作主要是对内存池结构体中的属性进行了初始化,当向系统申请内存时用到了ngx_memalign函数,此函数在文件中的申明如下:

#define ngx_memalign(alignment, size, log)  ngx_alloc(size, log)

​   此处的alignment主要是针对部分unix平台需要动态的对齐,大多数情况下编译器和C库都会帮我们处理对齐问题。而ngx_alloc函数如下所示:

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_alloc仅仅是对malloc做了一些简单的封装以及日志输出处理。

​   再来看看对max的初始化处理:用户所设定的max大小不能大于NGX_MAX_ALLOC_FROM_POOL这个宏所设定的值,这个宏的值规定为 (ngx_pagesize - 1),也就是说x86中页的大小为4K,内存池最大不超过4095。

内存申请

​   内存申请函数如下所示:

void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)
    if (size <= pool->max) {
        return ngx_palloc_small(pool, size, 1);
    }
#endif

    return ngx_palloc_large(pool, size);
}

​   正如之前所说,在内存分配时针对用户所申请的内存的大小,将采用不同的分配策略,我们可以具体看看ngx_palloc_smallngx_palloc_large两个函数都做了什么。

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;

        //对齐处理
        if (align) {
            m = ngx_align_ptr(m, NGX_ALIGNMENT);
        }
		
        //判断当前内存块的剩余内存是否大于所需内存
        if ((size_t) (p->d.end - m) >= size) {
            //将last指针移向新位置
            p->d.last = m + size;

            return m;
        }
		
        //若当前内存块的剩余内存小于所需内存,则到下一个内存块中寻找
        p = p->d.next;

    } while (p);

    //若所有内存块都没有合适的内存空间,则分配新的内存块。
    return ngx_palloc_block(pool, size);
}

  这里需要注意的几点是,ngx_align_ptr,这是一个用来内存地址取整的宏,非常精巧,一句话就搞定了。作用不言而喻,取整可以降低CPU读取内存的次数,提高性能。因为这里并没有真正意义调用malloc等函数申请内存,而是移动指针标记而已,所以内存对齐的活,C编译器帮不了你了,得自己动手。

 #define ngx_align_ptr(p, a)                                       \
 (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))

  这个函数在最后使用到了ngx_palloc_block函数,这个函数时用来向系统申请新的内存块,并添加到链表的尾部。

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);
 
    //分配和第一个内存块总共可分配内存同样大小的内存块
    m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
    if (m == NULL) {
        return NULL;
    }
 	
    //将申请的内存块指针转换为ngx_pool_t指针
    new = (ngx_pool_t *) m;
 
    //初始化新内存块的end、next及failed
    new->d.end = m + psize;
    new->d.next = NULL;
    new->d.failed = 0;
 
    //将指针m移动到d后面的一个位置,表示可分配内存的开始位置
    m += sizeof(ngx_pool_data_t);
    //对m指针按4字节对齐处理
    m = ngx_align_ptr(m, NGX_ALIGNMENT);
    //设置新内存块的last
    new->d.last = m + size;
 
    current = pool->current;
    //这里的循环用来找最后一个链表节点,这里failed用来控制循环的长度,如果分配失败次数达到5次,
     //就忽略,不需要每次都从头找起
    for (p = current; p->d.next; p = p->d.next) {
        if (p->d.failed++ > 4) {
            current = p->d.next;
        }
    }
 
    p->d.next = new;
 
    pool->current = current ? current : new;
 
    return m;
}

ngx_palloc_large

  ngx_palloc_large用来分配申请内存大于内存池中max的内存块,其实现主要是分配内存后,将其加入大块内存链表尾部。

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;
    // 查找到一个空的large区,如果有,则将刚才分配的空间交由它管理  
    for (large = pool->large; large; large = large->next) {
        if (large->alloc == NULL) {
            large->alloc = p;
            return p;
        }
 		
        //查找3次都未找到空的large结构体则直接跳出循环直接创建
        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;
}

内存池重置

  可以通过ngx_reset_pool来重置内存池,此处重置的意思为将内存池中的大块内存链表与小块内存链表内的内存全部释放,函数源码如下:

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);
        }
    }
	//重置小块内存,并不调用free将内存交还给系统,只是指针的复位操作。
    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;
}

内存释放

  我们使用Nginx内存池手动申请的小块内存,在该内存池中并不提供相应小块内存的释放操作。关于Nginx内存池的释放操作,Nginx提供了一个接口:ngx_pfree,该函数内部仅仅对指定的大块内存进行释放。

  由于在分配大块内存时,只检查前三个内存块是否未分配,所以使用完大块内存之后必须及时释放。

ngx_pfree函数原型如下:

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

内存池销毁

  内存池的销毁操作在ngx_destroy_pool函数中,该函数循环调用ngx_pool_cleanup_s中的handle函数释放资源,并释放大块内存和小块内存链表的内存块。

ngx_destroy_pool(ngx_pool_t *pool)
{
    ngx_pool_t          *p, *n;
    ngx_pool_large_t    *l;
    ngx_pool_cleanup_t  *c;

    //循环调用handle数据清理函数
    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);
        }
    }

    //释放大块内存
    for (l = pool->large; l; l = l->next) {
        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;
        }
    }
}

  
参考博文:https://www.cnblogs.com/xiekeli/archive/2012/10/17/2727432.html

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值