菜鸟学习Nginx之内存池

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xxb249/article/details/81783612

从今天开始深入介绍Nginx框架。

首先来谈谈我对《深入理解Nginx模块开发与架构解析》看法,这本书应该是到目前为止,市面写的最详细,最充实的书籍(没有之一),值得拥有。然而此书对于一个小白来说,并不太适合,此书适合有相关使用经验或者开发经验,适合于进一步深造的同学。如果是小白,建议先浏览一下网上的博客,对Nginx各个方面有一定了解,然后在深入阅读此书。这是仅仅是我个人经验,毕竟我是这样走过来的。最后建议阅读此书的朋友,最好多阅读几遍,每一次都有不少的收获。

开始我们今天的主题,任何一款软件都离不开数据结构,良好的数据结构对于软件的发展会起到事半功倍的效果。

一、Nginx很奇葩

Nginx这款软件很奇葩,但同时体现出它的优秀。说它奇葩之处体现在:为了节约内存,不会轻易主动申请内存,而经常复用,例如利用一个指针最低2bit始终为0这个特性,来存储一个字段。但是它为了提升性能,却又申请大块空间,例如在共享内存方面,它为每个变量申请128字节(考虑CPU二级缓存)。

二、数据结构

2.1数据结构

本篇将介绍始终贯穿整个软件的对象--ngx_pool_t内存池。内存池存在意义,不用多说,直接上数据结构定义。

typedef struct ngx_pool_large_s  ngx_pool_large_t;

//申请大内存段

struct ngx_pool_large_s {

    ngx_pool_large_t     *next;

    void                 *alloc; /* 保存通过malloc返回的指针 */

};

//内存池元数据 用于把内存池节点使用链表方式关联起来

typedef struct {

    u_char               *last; /* 可分配起始位置 */

    u_char               *end;  /* 当前内存池块 最后有效位置 */

    ngx_pool_t           *next; /* 指向下一个内存池块 */

    ngx_uint_t            failed; /* 代表从池中申请内存失败次数 */

} ngx_pool_data_t;



/* 内存池头,跳过内存池头是数据区 */

typedef struct ngx_pool_s            ngx_pool_t;

struct ngx_pool_s {

    ngx_pool_data_t       d; /* 当前内存池元数据 */

    size_t                max; /* 申请空间大于max则表示需要申请大内存,只在创建内存池时赋值 */

    ngx_pool_t           *current; /* current用于加速遍历 */

    ngx_chain_t          *chain; /* 在http filter模块中使用 主要用于http response */

    ngx_pool_large_t     *large;  /* 大内存 */

    ngx_pool_cleanup_t   *cleanup;  /* 设置回调 ngx_pool_cleanup_add */

    ngx_log_t            *log;

};

2.2、内存池组织形态

2.3 特点

Nginx实现的内存池有如下特点:

  1. 为了满足大内存需求(一个内存池节点,实际可用内存大小是固定的max,当申请的内存大于max则认为是大内存),nginx设计一个独立链表(large)用于保存大内存块。大内存块采用malloc/free直接申请堆内存,可见大内存适用生命周期较短场景,否则会把内存耗尽。大内存头部ngx_pool_large_t内存是从当前内存池中申请。为什么这样设计呢?为了复用。
  2. 对于大内存,始终存放到内存池首节点中,对大内存头sizeof(ngx_pool_large_t)的内存空间一定是来自current所指向内存池节点。如上图所示,可参考后续源码分析中。
  3. 往往的应用层业务逻辑很复杂,为了方便通常在业务逻辑最后环节进行资源的回收,Nginx也考虑到此需求,所以在内存池中增加ngx_pool_cleanup_t结构。注意:虽然这里名称叫做pool_cleanup,业务层只需要设置好自己的回调函数即可。Nginx框架在释放内存池之前就会调用回调函数。至于回调函数内容就非常灵活,完全取决于当前业务逻辑。例如:关闭各种文件句柄,删除各种临时数据文件等。
  4. 数据结构 ngx_pool_data_t,是将内存池节点以链表形式进行关联即next,每次创建新内存节点挂在链表最后zuiho。其中end-last(减法)等于可用内存空间。

 三、相关接口

下面介绍相关代码,毕竟只有看懂代码才能深入理解其中的内涵

/**
 * 创建内存池
 */
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);//16字节对齐
    if (p == NULL) {
        return NULL;
    }

    /* 初始化内存池头 */
    p->d.last = (u_char *) p + sizeof(ngx_pool_t);
    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;
}

简要说明:

  1. 为了保证访问速度,采用16字节方式对齐。
  2. 如果申请的内存大小大于一个内存页大小(一般是4k),虽然能够申请成功,但是有内存浪费。因此在使用内存时需要注意大小。
/**
 * 销毁内存池
 */
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);//执行回调函数
        }
    }


/*为了篇幅 删除debug调试信息 */

//释放大内存,由此可知所有的大内存均放在内存池首节点中
    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;
        }
    }
}


/**
 * 业务模块设置清理回调
 */

ngx_pool_cleanup_t *
ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
{

    ngx_pool_cleanup_t  *c;
    /* 从池中申请内存 */
    c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
    if (c == NULL) {
        return NULL;
    }


    if (size) {
        c->data = ngx_palloc(p, size);
        if (c->data == NULL) {
            return NULL;
        }

    } else {
        c->data = NULL;
    }


    c->handler = NULL;//设置null,由调用者在外部设置
    c->next = p->cleanup;
    p->cleanup = c;
    ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);
    return c;
}

那么如使用内存池呢?非常简单,直接调用ngx_palloc/ngx_pnalloc,流程图如下:

这里需要阐明一个观点,对于Nginx来说,所有申请的内存均来自内存池(除大内存),可以理解成万物皆池化。

void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC) //默认不开启
    if (size <= pool->max) {//检查待申请的内存是否大于max,如果大于则表明申请大内存
        return ngx_palloc_small(pool, size, 1);
    }
#endif
    return ngx_palloc_large(pool, size);

}

/**
 * 从内存块中分配内存
 */
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) {
            p->d.last = m + size;
            return m;
        }
        p = p->d.next;
    } while (p);

    return ngx_palloc_block(pool, size);//分配新内存池节点
}

/**
 * 向操作系统申请分配内存块
 * block 代表块
 */
static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
    u_char      *m;
    size_t       psize;
    ngx_pool_t  *p, *new;

    //向操作系统中申请新的内存
    psize = (size_t) (pool->d.end - (u_char *) pool);
    m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
    if (m == NULL) {
        return NULL;
    }

    new = (ngx_pool_t *) m;
    new->d.end = m + psize;
    new->d.next = NULL;
    new->d.failed = 0;

    m += sizeof(ngx_pool_data_t);
    m = ngx_align_ptr(m, NGX_ALIGNMENT);
    new->d.last = m + size;/* size表示业务从内存池中申请的空间大小 */

    /* 将每一个内存失败次数加1 如果失败次数大于4次 则修改current指针 提升遍历速度 */
    for (p = pool->current; p->d.next; p = p->d.next) {
        if (p->d.failed++ > 4) {
            pool->current = p->d.next;//始终条current指针
        }
    }

    p->d.next = new;//挂链表 放到链表最后
    return m;
}

/**
 * 申请大内存 直接向操作系统申请
 */
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;
        }

        if (n++ > 3) {
            break;
        }
    }


    /**
     * 创建新节点然后在链表头插入 large头部信息 也是从池中分配
     * 此处比较巧妙 体现万物皆池化的特点
     */
    large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
    if (large == NULL) {
        ngx_free(p);
        return NULL;
    }

    large->alloc = p;
    large->next = pool->large;
    pool->large = large;
    return p;

}

四、内存池生命周期

      在Nginx中有三种不同生命周期的内存池为:进程级、连接级、请求级。

级别

存活时长

说明

进程级

伴随整个进程,时间最长

ngx_cycle_t中内存池

连接级

伴随tcp会话,时间居中

ngx_connection_t中内存池

请求级

伴随http一次请求,时间最短

ngx_http_request_t中内存池

为什么会出现三种级别的内存池呢?仔细想想可知,对于Nginx万物皆池化,所有内存的申请必须通过内存池。

一个进程启动肯定需要一个(一些)用于保存全局数据。

Nginx是用于网络通信,自然需要维持tcp相关数据,例如:对于长连接http请求。

请求级,自然对应http请求,虽然http采用长连接方式,但是每一次http请求可能都不一样,自然需要为每个http请求分配一个内存池。

五、总结

       Nginx实现的内存池比较简单易懂,我们在开发自己的应用程序,只要保证所有内存均来自内存池这唯一标准,那么就不会出现内存问题。万物皆池化!!下一篇,我们来看看ngx_buf_t

猜你喜欢

转载自blog.csdn.net/xxb249/article/details/81783612
今日推荐