Nginx专题(八、内存池)

在之前把,一遇到什么线程池,内存池,连接池,啥池啥池的就觉得好高大上,现在也觉得好高大上,但是现在已经知道线程池和内存池的基本原理了,不过线程池还没写,下个月会安排上,内存池比较特殊,不能单独讲,其实也可以,不过内存池是每个项目中实现的不一样,也就是说内存池就是根据自己需要设计的,所以比较难单独讲,比如nginx中的内存池和zmq中的内存池就不一样,不过我们既然 遇到了nginx下的内存池,那就 分析分析。

8.1 内存池介绍

我们写c语言代码都比较清楚,c语言并没有做垃圾回收机制(Java有),所以申请和释放内存都是我们程序员自己设计,像nginx这种异步处理,在没有内存池使用的情况下,应该是需要一个全局变量把申请内存的指针统统保存,等到最后请求结束了之后,在一个一个指针的释放,到那会nginx提供了内存池,我们之前在写模块的时候,是不是感觉我们只是在申请内存,并没有释放内存,那就是因为nginx提供了内存池,我们只管申请,等到请求结束了,nginx自行帮我们释放内存 。

这样也引进一个问题,如果这个内存池的生命周期比较长,而每一块内存的生命周期很短,早期申请的内存会一直占着资源,所以一半应用中没有见过这样的内存池设计,但是nginx内存池就是这样设计的,这是因为nginx是一个很纯粹的web服务器,与客户端的每一个TCP连接有明确的生命周期,TCP连接上每一个HTTP请求有非常短暂的生命周期,如果每一个请求连接都有各自的内存池,开发模块的时候就可以在一个请求或者一个连接的内存池上申请内存,算了,不说这么多了,直接看内存池设计的源码分析。

8.2 内存池数据结构

我们先看看内存池的数据结构,从数据结构分析起来,容易讲解。

8.2.1 总体结构

struct ngx_pool_s {
    //描述小块内存池,当分配小块内存时,剩余的预分配空间不足时,会再分配1个ngx_pool_t
    //他们会通过d中的next成员构成单链表
    ngx_pool_data_t       d;		
    //评估申请内存属于小块还是大块的标准
    size_t                max;
    //多个小块内存池构成链表时,current指向分配内存时遍历的第1个小块内存池
    ngx_pool_t           *current;
    //用于ngx_output_chain,与内存池无关
    ngx_chain_t          *chain;
    //大块内存都是直接从进程的堆中分配,为了能够在销毁内存池时同时释放大块内存
    //就把每一个分配的大块内存通过ngx_pool_large_t组成单链表挂在large成员上
    ngx_pool_large_t     *large;
    //所有待清理资源(例如需要关闭或者删除文件)以ngx_pool_cleanup_t对象构成单链表
    //挂在cleanup成员上
    ngx_pool_cleanup_t   *cleanup;
    //内存池执行中输出日志的对象
    ngx_log_t            *log;
};

从上面的代码注释中我们知道,申请的内存是要分大块小块的,判断的标准就是max,如果是大块的内存就直接调用ngx_alloc从进程的堆中分配的,同时会再分配一个ngx_pool_large_t结构体挂在large链表中,

8.2.2 大块内存结构

typedef struct ngx_pool_large_s  ngx_pool_large_t;

struct ngx_pool_large_s {
    //所有大块内存通过next指针联在一起,单链表
    ngx_pool_large_t     *next;
    //alloc指向ngx_alloc分配出的大块内存,调用ngx_pfree后alloc可能是NULL
    void                 *alloc;
};

可以借鉴一下nginx这种设计,申请的内存都挂在一个链表上,释放的时候遍历链表,找到想对应的地址,释放内存,这样就不用保存大量的指针了。nginx提供了ngx_pfree这个方法,大概实现就是遍历链表,然后找到需要释放的,就释放,但是ngx_pool_large_t不释放,只是把alloc置为NULL,这样下面需要申请的时候,就直接用了,不能在创建链表结点,遍历链表的一个明显缺点就是,当元素很多的时候,性能不好。

我们来看看ngx_alloc和ngx_pfree源码:

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_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;			//置为NULL

            return NGX_OK;
        }
    }

    return NGX_DECLINED;
}

看了源码,发现确实是我们分析的那样

8.2.3 小块内存

我们在看看小块内存,通过从进程的堆中预分配更多的内存,而后直接使用这块内存的一部分作为小块内存返回给申请者,以此实现减少碎片和调用malloc的次数,他们都是在成员d中维护管理的,我们看看这个定义:

typedef struct {
    //指向为分配的空间内存的首地址
    u_char               *last;
    //指向当前小块内存池的尾部
    u_char               *end;
    //同属于一个pool的多个小块内存池减,通过next相连
    ngx_pool_t           *next;
    //每当剩余空间不足以分配出小块内存时,failed成员就会加1,failed成员大于4后,ngx_pool_t的current将移向下一个小块内存池
    ngx_uint_t            failed;
} ngx_pool_data_t;

小块内存介绍就先这样,分配的机制到分析源码的时候再说。

8.2.4 资源释放

ngx_pool_t还能释放如文件扥资源,我们先看看结构体

//实现这个回调方法时,data参数将是ngx_pool_cleanup_t中的data成员
typedef void (*ngx_pool_cleanup_pt)(void *data);

typedef struct ngx_pool_cleanup_s  ngx_pool_cleanup_t;

struct ngx_pool_cleanup_s {
    //handler初始化为NULL,需要设置为清理方法
    ngx_pool_cleanup_pt   handler;
    //用于为handler指向的方法传递必要的参数
    void                 *data;
    //添加到ngx_pool_t的cleanup链表中
    ngx_pool_cleanup_t   *next;
};

8.3 内存池源码分析

上一节的数据结构还是要好好看一下,如果不想看,浏览一下也是可以的,反正看到这一节之后,就会发现这些数据结构的重要性了,在回去看也是可以的。

8.3.1 创建内存池

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);		//内存分配与内存对齐
    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;					//同属于一个pool的多个小块内存池减,通过next相连
    p->d.failed = 0;					 //每当剩余空间不足以分配出小块内存时,failed成员就会加1,failed成员大于4后,ngx_pool_t的current将移向下一个小块内存池

    size = size - sizeof(ngx_pool_t);
    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

    p->current = p;		 //多个小块内存池构成链表时,current指向分配内存时遍历的第1个小块内存池
    p->chain = NULL;
    p->large = NULL;		//大块内存链表
    p->cleanup = NULL;
    p->log = log;

    return p;
}

看着好像也没有啥,就是申请内存,这里用了一个memalign函数,其实就是为了内存对齐,然后就进行一些变量的初始化。

8.3.2 申请内存

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

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

    return ngx_palloc_large(pool, size);
}

像内存池申请内存的时候,先判断大小,如果小于max就申请小内存,如果大于max就申请大内存,max是在初始化的时候指定的。

8.3.3 小内存申请

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;			//m是空闲的首地址

        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;		//如果当前没有空的内存,就指向next1

    } while (p);		//一直找,如果没有的话,就退出循环

    return ngx_palloc_block(pool, size);		
}


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;					//这里就开始新的申请内存了

	//
    for (p = pool->current; p->d.next; p = p->d.next) {
        if (p->d.failed++ > 4) {
            pool->current = p->d.next;		//遍历更换当前的内存池地址
        }
    }

    p->d.next = new;					//挂在上来

    return m;
}

小内存的申请只要是先在当前的内存池中申请内存,如果当前内存池的内存不够,就需要通过d.next指针想后申请,如果d.next指针为空,那就需要重新申请一个内存池,然后把current指向改像当前内存池,差不多这个样子,等会画个图。

8.3.4 大内存申请

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 = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
    if (large == NULL) {
        ngx_free(p);
        return NULL;
    }

	//赋值p,并插入到链表头部,修改pool->large的指针
    large->alloc = p;
    large->next = pool->large;
    pool->large = large;

    return p;
}

大内存申请先遍历已有链表,看是否存在alloc为空的,如果存在就赋值,如果没有,就重新创建一个结点,并插入这个链表的头部。

8.3.5 文件释放

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;
    c->next = p->cleanup;  //插入到链表的头部

    p->cleanup = c;

    ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);

    return c;
}

往内存池中申请一个大文件,会返回一个ngx_pool_cleanup_t结构体,上一节分析过,可以回去看看。

void
ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd)
{
    ngx_pool_cleanup_t       *c;
    ngx_pool_cleanup_file_t  *cf;

    for (c = p->cleanup; c; c = c->next) {
        if (c->handler == ngx_pool_cleanup_file) {

            cf = c->data;

            if (cf->fd == fd) {
                c->handler(cf);
                c->handler = NULL;
                return;
            }
        }
    }
}


void
ngx_pool_cleanup_file(void *data)
{
    ngx_pool_cleanup_file_t  *c = data;

    ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d",
                   c->fd);

    if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
        ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
                      ngx_close_file_n " \"%s\" failed", c->name);
    }
}

handler可以绑定ngx_pool_run_cleanup_file这个函数,文件使用完之后会关闭,没有具体用过,这个就先这样写,等以后用过了之后,如果有问题再回来改。

8.4 内存池总结图

今天有点没空画了,就留着你们在文章中找了,其实看懂了源码的也就明白了图,以后有空再回来补补。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值