最近nginx的源码刚好研究到内存池,这儿就看下nginx内存池的相关的东西。
一,为什么要使用内存池
大多数的解释不外乎提升程序的处理性能及减小内存中的碎片,对于性能优化这点主要体现在:
(1)系统的malloc/free等内存申请函数涉及到较多的处理,如申请时合适空间的查找,释放时的空间合并。
(2)默认的内存管理函数还会考虑多线程的应用,加锁操作会增加开销。
(3)每次申请内存的系统态与用户态的切换也及为的消耗性能。
对于由于应用的频繁的在堆上分配及释放空间所带来的内存碎片化,其实主流的思想是认为存在的,不过也有人认为
这种考虑其实是多余的,在“内存池到底为我们解决了什么问题”一文中则认为,大量的内存申请与释放仅会造成短暂
的内存碎片化的产生,并不会引起大量内存的长久碎片化,从而导致最后申请大内存时的完全不可用性。文中认为对于
确定的应用均是“有限对象需求”,即在任一程序中申请与释放的对象种类总是有限的,大小也总是有一定重复性的,
这样在碎片产生一段时间后,会因为同样的对象申请而消除内存的临时碎片化。
不过,综上,内存池有利于提升程序在申请及释放内存时的处理性能这点是确定的。内存池的主要优点有:
(1)特殊情况的频繁的较小的内存空间的释放与申请不需要考虑复杂的分配释放方法,有较高的性能。
(2)初始申请时通常申请一块较大的连续空间的内存区域,因此进行管理及内存地址对齐时处理非常方便。
(3)小块内存的申请通常不用考虑实际的释放操作。
二,内存池的原理及实现方法
内存池的原理基本是内存的提前申请,重复利用。其中主要需要关注的是内存池的初始化,内存分配及内存释放。
内存池的实现方法主要分两种:
一种是固定式,即提前申请的内存空间大小固定,空间划分成固定大小的内存单元以供使用如下图示:
上图引用自http://www.ibm.com/developerworks/cn/linux/l-cn-ppp/index6.html。
另一种是动态式,即初始申请固定大小的单元,空间不做明确的大小划分,而是根据需要提供合适大小的空间以供使用。
不过,这两种方式在处理大空间的内存申请时的处理方法通常都是采用系统的内存申请调用。
三,nginx的内存池实现
nginx的内存池设计得非常精妙,它在满足小块内存申请的同时,也处理大块内存的申请请求,同时还允许挂载自己的数据区域
及对应的数据清楚操作。
nginx内存池实现主要是在core/ngx_palloc.{h,c}中,一些支持函数位于os/unix/ngx_alloc.{h,c}中,支持函数主要是对原有的
malloc/free/memalign等函数的封装,对就的函数为:
》ngx_alloc 完成malloc的封装
》ngx_calloc 使用malloc分配空间,同时使用memset完成初始化
》ngx_memalign 会根据系统不同而调用不一样的函数处理,如posix系列使用posix_memalign,windows则不考虑对齐。主要作用
是申请指定的alignment对齐的起始地址的内存空间。
》ngx_free 完成free的封装
nginx内存池中有两个非常重要的结构,一个是ngx_pool_s,主要是作为整个内存池的头部,管理内存池结点链表,大内存链表,
cleanup链表等,具体结构如下:
//该结构维护整个内存池的头部信息 struct ngx_pool_s { ngx_pool_data_t d; //数据块 size_t max; //数据块大小,即小块内存的最大值 ngx_pool_t *current; //保存当前内存值 ngx_chain_t *chain; //可以挂一个chain结构 ngx_pool_large_t *large; //分配大块内存用,即超过max的内存请求 ngx_pool_cleanup_t *cleanup; //挂载一些内存池释放的时候,同时释放的资源 ngx_log_t *log; };
另一重要的结构为ngx_pool_data_s,这个是用来连接具体的内存池结点的,具体如下:
//该结构用来维护内存池的数据块,供用户分配之用 typedef struct { u_char *last; //当前内存分配结束位置,即下一段可分配内存的起始位置 u_char *end; //内存池结束位置 ngx_pool_t *next; //链接到下一个内存池 ngx_uint_t failed;//统计该内存池不能满足分配请求的次数 } ngx_pool_data_t;
还有另两个结构ngx_pool_large_t,ngx_pool_cleanup_t,如下示:
//大内存结构 struct ngx_pool_large_s { ngx_pool_large_t *next; //下一个大块内存 void *alloc;//nginx分配的大块内存空间 }; struct ngx_pool_cleanup_s { ngx_pool_cleanup_pt handler; //数据清理的函数句柄 void *data; //要清理的数据 ngx_pool_cleanup_t *next; //连接至下一个 };
然后我们具体看一下nginx内存池的组成结构,如下图示:
上面的图中,current指针是指向的首结点,在具体的运行过程中是会根据failed值进行调整的。还有就是
ngx_pool_cleanup_s与ngx_pool_large_s的结构空间均来自内存池结点。
然后看nginx相关的操作:
1.创建内存池
内存池的创建是在ngx_create_pool函数中完成的,实现如下:
//创建内存池 ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log) { ngx_pool_t *p; //ngx_memalign实际上会依据os不用,分情况处理,在os不支持memalign情况的分配时,选择直接分配内存 p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log); // 分配内存函数,uinx,windows分开走 if (p == NULL) { return NULL; } p->d.last = (u_char *) p + sizeof(ngx_pool_t); //初始指向 ngx_pool_t 结构体后面 p->d.end = (u_char *) p + size; //整个结构的结尾后面 p->d.next = NULL; p->d.failed = 0; size = size - sizeof(ngx_pool_t); //实际上pool数据区的大小与系统页的大小有关的 p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL; //最大不超过 NGX_MAX_ALLOC_FROM_POOL,也就是getpagesize()-1 大小 p->current = p; p->chain = NULL; p->large = NULL; p->cleanup = NULL; p->log = log; return p; }
2.销毁内存池
内存池的销毁位于ngx_destroy_pool(ngx_pool_t *pool)中,此函数会清理所有的内存池结点,同时清理large链表的内存
并且对于注册的cleanup链表的清理操作也会进行。具体实现如下:
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); } } //这儿是作大数据块的清除 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); } } #if (NGX_DEBUG) /* * we could allocate the pool->log from this pool * so we cannot use this log while free()ing the pool */ for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) { ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p, unused: %uz", p, p->d.end - p->d.last); if (n == NULL) { break; } } #endif for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) { ngx_free(p); if (n == NULL) { break; } } }