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

Nginx 内存池 - 原理解析

2020-08-07 17:52 806 查看

Nginx 内存池 - 原理解析

  • Nginx内存池操作
  • 内存池重置
  • 内存释放
  • 内存池销毁
  • Nginx 内存池 - 原理解析


    ​   Nginx的内存池模块定义在

    ngx_palloc.h
    ngx_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;			//日志信息
    };

    ​   其中的

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

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


    小块内存处理

    ​   对于用户所申请的小于等于内存池设定的

    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
      的剩余的内存,则我们返回
      p
      last
      的地址作为内存分配的首地址,并且调整
      p
      中的
      last
      last + 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_small
    ngx_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.geek-share.com/detail/2561445260.html

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