作者:July、dreamice、阿波、yixiao。
本文出处:http://blog.csdn.net/v_JULY_v/。
引言
Nginx(发音同 engine x)是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,并在一个BSD-like 协议下发行。由俄罗斯的程序设计师Igor Sysoev所开发,最初供俄国大型的入口网站及搜寻引擎Rambler(俄文:Рамблер)使用。
其特点是占有内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好,目前中国大陆使用nginx网站用户有:新浪、网易、 腾讯,另外知名的微网志Plurk也使用nginx,以及诸多暂不曾得知的玩意儿。
读者可以到此处下载Nginx最新版本的源码:http://nginx.org/en/download.html。同时,本文本不想给源码太多注释,因为这不像讲解算法,算法讲解的越通俗易懂越好,而源码剖析则不同,缘由在于不同的读者对同一份源码有着不同的理解,或深或浅,所以,更多的是靠读者自己去思考与领悟。
ok,本文之中有任何疏漏或不正之处,恳请批评指正。谢谢。
Nginx源码剖析之内存池
1、内存池结构
内存相关的操作主要在文件 os/unix/ngx_alloc.{h,c} 和 core/ngx_palloc.{h,c} 中实现,ok,咱们先来看内存管理中几个主要的数据结构:
- typedef struct {
- u_char *last;
- u_char *end;
- ngx_pool_t *next;
- ngx_uint_t failed;
- } ngx_pool_data_t;
- struct ngx_pool_t {
- ngx_pool_data_t d;
- size_t max;
- ngx_pool_t *current;
- ngx_chain_t *chain;
- ngx_pool_large_t *large;
- ngx_pool_cleanup_t *cleanup;
- ngx_log_t *log;
- };
再来看看大块数据分配的结构体:
- struct ngx_pool_large_t {
- ngx_pool_large_t *next;
- void *alloc;
- };
其它的数据结构与相关定义:
- typedef struct {
- ngx_fd_t fd;
- u_char *name;
- ngx_log_t *log;
- } ngx_pool_cleanup_file_t;
- #define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1) //在x86体系结构下,该值一般为4096B,即4K
上述这些数据结构的逻辑结构图如下(下图最左上角部分没有与上文的第一个数据结构内的ngx_uint_t对应起来,特此说明):
1.1、ngx_pool_t的逻辑结构
再看一下用UML绘制的ngx_pool_t的逻辑结构图:
在下一节,我们将会深入分析内存管理的主要函数。
Nginx源码剖析之内存管理
2、内存池操作
2.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;
- 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_create_pool(1024, 0x80d1c4c)后,创建的内存池物理结构如下图:
紧接着,咱们就来分析下上面代码中所提到的:ngx_memalign()函数。
- void *
- ngx_memalign(size_t alignment, size_t size, ngx_log_t *log)
- {
- void *p;
- int err;
-
- err = posix_memalign(&p, alignment, size);
-
-
- if (err) {
- ngx_log_error(NGX_LOG_EMERG, log, err,
- "posix_memalign(%uz, %uz) failed", alignment, size);
- p = NULL;
- }
-
- ngx_log_debug3(NGX_LOG_DEBUG_ALLOC, log, 0,
- "posix_memalign: %p:%uz @%uz", p, size, alignment);
-
- return p;
- }
-
-
- #define NGX_POOL_ALIGNMENT 16
因此,nginx的内存池分配,是以16字节为边界对齐的。
2.1、销毁内存池
接下来,咱们来看内存池的销毁函数,pool指向需要销毁的内存池
- 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);
- }
- }
-
-
-
- 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)
-
-
-
-
-
-
- for (p = pool, n = pool->d.next; ; 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; ; p = n, n = n->d.next) {
- ngx_free(p);
-
- if (n == NULL) {
- break;
- }
- }
- }
-
该函数将遍历内存池链表,所有释放内存,如果注册了clenup(也是一个链表结构),亦将遍历该cleanup链表结构依次调用clenup的handler清理。同时,还将遍历large链表,释放大块内存。
2.3、重置内存池
void ngx_reset_pool(ngx_pool_t *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);
- }
- }
-
-
- pool->large = NULL;
-
- for (p = pool; p; p = p->d.next) {
- p->d.last = (u_char *) p + sizeof(ngx_pool_t);
- }
- }
这里虽然重置了内存池,但可以看到并没有释放内存池中被使用的小块内存,而只是将其last指针指向可共分配的内存的初始位置。这样,就省去了内存池的释放和重新分配操作,而达到重置内存池的目的。
上面我们主要阐述了内存池管理的几个函数,接下来我们深入到如何从内存池中去申请使用内存。
2.4、分配内存(重点)
2.4.1、ngx_palloc 与ngx_pnalloc函数
这两个函数的参数都为(ngx_pool_t *pool, size_t size),且返回类型为void*,唯一的区别是ngx_palloc从pool内存池分配以NGX_ALIGNMENT对齐的内存,而ngx_pnalloc分配适合size大小的内存,不考虑内存对齐。
我们在这里只分析ngx_palloc,对于ngx_pnalloc其实现方式基本类似,便不再赘述。
文件:src/core/ngx_palloc.c
- void *
- ngx_palloc(ngx_pool_t *pool, size_t size)
- {
- u_char *m;
- ngx_pool_t *p;
-
-
-
- if (size <= pool->max) {
-
- p = pool->current;
-
- do {
-
-
- m = ngx_align_ptr(p->d.last, 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);
-
- }
-
- return ngx_palloc_large(pool, size);
- }
例如,在2.1节中创建的内存池中分配200B的内存,调用ngx_palloc(pool, 200)后,该内存池物理结构如下图:
a、待分配内存小于max值的情况
同样,紧接着,咱们就来分析上述代码中的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;
- }
-
-
-
- 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;
-
-
- current = pool->current;
-
- 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;
- }
注意:该函数分配一块内存后,last指针指向的是ngx_pool_data_t结构体(大小16B)之后数据区的起始位置,而创建内存池时时,last指针指向的是ngx_pool_t结构体(大小40B)之后数据区的起始位置。 结合2.8节的内存池的物理结构,更容易理解。
b、待分配内存大于max值的情况
如2.4.1节所述,如果分配的内存大小大于max值,代码将跳到ngx_palloc_large(pool, size)位置,
ok,下面进入ngx_palloc_large(pool, size)函数的分析:
-
-
-
- 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;
-
-
-
- for (large = pool->large; large; large = large->next) {
- if (large->alloc == NULL) {
- large->alloc = p;
- return p;
- }
-
- 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_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 bytes failed", size);
- }
-
- ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, log, 0, "malloc: %p:%uz", p, size);
-
- return p;
- }
2.4.2、ngx_pcalloc与ngx_pmemalign函数
ngx_pcalloc是直接调用palloc分配好内存,然后进行一次0初始化操作。ngx_pcalloc的源码如下:
- void *
- ngx_pcalloc(ngx_pool_t *pool, size_t size)
- {
- void *p;
-
- p = ngx_palloc(pool, size);
- if (p) {
- ngx_memzero(p, size);
- }
-
- return p;
- }
ngx_pmemalign将在分配size大小的内存并按alignment对齐,然后挂到large字段下,当做大块内存处理。ngx_pmemalign的源码如下:
- void *
- ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment)
- {
- void *p;
- ngx_pool_large_t *large;
-
- p = ngx_memalign(alignment, size, pool->log);
- if (p == NULL) {
- return NULL;
- }
-
- 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;
- }
其余的不再详述。nginx提供给我们使用的内存分配接口,即上述本2.4节中这4种函数,至此,都已分析完毕。
2.5、释放内存
- 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;
需要注意的是该函数只释放large链表中注册的内存,普通内存在ngx_destroy_pool中统一释放。
2.6、注册cleanup
- 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;
- }
2.7、文件相关
一些文件相关的操作函数如下,此处就不在详述了。
- void
- ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd)
- {
-
- }
-
- void
- ngx_pool_cleanup_file(void *data)
- {
-
- }
-
- void
- ngx_pool_delete_file(void *data)
- {
-
- }
2.8、内存池的物理结构
针对本文前几节的例子,画出的内存池的物理结构如下图。
从该图也能看出2.4节的结论,即内存池第一块内存前40字节为ngx_pool_t结构,后续加入的内存块前16个字节为ngx_pool_data_t结构,这两个结构之后便是真正可以分配内存区域。
全文总结
来自淘宝数据共享平台blog内的一篇文章对上述Nginx源码剖析之内存池,与内存管理总结得很好,特此引用之,作为对上文全文的一个总结:
Nginx的内存池实现得很精巧,代码也很简洁。总的来说,所有的内存池基本都一个宗旨:申请大块内存,避免“细水长流”。
3.1、创建一个内存池
nginx内存池主要有下面两个结构来维护,他们分别维护了内存池的头部和数据部。此处数据部就是供用户分配小块内存的地方。
-
- typedef struct {
- u_char *last;
- u_char *end;
- ngx_pool_t *next;
- ngx_uint_t failed;
- } ngx_pool_data_t;
-
- struct ngx_pool_s {
- ngx_pool_data_t d;
- size_t max;
- ngx_pool_t *current;
- ngx_chain_t *chain;
- ngx_pool_large_t *large;
- ngx_pool_cleanup_t *cleanup;
- ngx_log_t *log;
- };
有了上面的两个结构,就可以创建一个内存池了,nginx用来创建一个内存池的接口是:
ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log)(位于src/core/ngx_palloc.c中);
调用这个函数就可以创建一个大小为size的内存池了。
ngx_create_pool接口函数就是分配上图这样的一大块内存,然后初始化好各个头部字段(上图中的彩色部分)。红色表示的四个字段就是来自于上述的第一个结构,维护数据部分,
由图可知:last是用户从内存池分配新内存的开始位置,end是这块内存池的结束位置,所有分配的内存都不能超过end。蓝色表示的max字段的值等于整个数据部分的长度。用户请求的内存大于max时,就认为用户请求的是一个大内存,此时需要在紫色表示的large字段下面单独分配;用户请求的内存不大于max的话,就是小内存申请,直接在数据部分分配,此时将会移动last指针(具体见上文2.4.1节)。
3.2、分配小块内存(size <= max)
上面创建好了一个可用的内存池了,也提到了小块内存的分配问题。nginx提供给用户使用的内存分配接口有:
void *ngx_palloc(ngx_pool_t *pool, size_t size);
void *ngx_pnalloc(ngx_pool_t *pool, size_t size);
void *ngx_pcalloc(ngx_pool_t *pool, size_t size);
void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment);
ngx_palloc和ngx_pnalloc都是从内存池里分配size大小内存,至于分得的是小块内存还是大块内存,将取决于size的大小;
他们的不同之处在于,palloc取得的内存是对齐的,pnalloc则否。
ngx_pcalloc是直接调用palloc分配好内存,然后进行一次0初始化操作。
ngx_pmemalign将在分配size大小的内存并按alignment对齐,然后挂到large字段下,当做大块内存处理。下面用图形展示一下分配小块内存的模型:
上图这个内存池模型是由上3个小内存池构成的,由于第一个内存池上剩余的内存不够分配了,于是就创建了第二个新的内存池,第三个内存池是由于前面两个内存池的剩余部分都不够分配,所以创建了第三个内存池来满足用户的需求。
由图可见:所有的小内存池是由一个单向链表维护在一起的。这里还有两个字段需要关注,failed和current字段。failed表示的是当前这个内存池的剩余可用内存不能满足用户分配请求的次数,即是说:一个分配请求到来后,在这个内存池上分配不到想要的内存,那么就failed就会增加1;这个分配请求将会递交给下一个内存池去处理,如果下一个内存池也不能满足,那么它的failed也会加1,然后将请求继续往下传递,直到满足请求为止(如果没有现成的内存池来满足,会再创建一个新的内存池)。
current字段会随着failed的增加而发生改变,如果current指向的内存池的failed达到了4的话,current就指向下一个内存池了。猜测:4这个值应该是Nginx作者的经验值,或者是一个统计值(详见上文2.4.1节a部分)。
3.3、大块内存的分配(size > max)
大块内存的分配请求不会直接在内存池上分配内存来满足,而是直接向操作系统申请这么一块内存(就像直接使用malloc分配内存一样),
然后将这块内存挂到内存池头部的large字段下。内存池的作用在于解决小块内存池的频繁申请问题,对于这种大块内存,是可以忍受直接申请的。
同样,用图形展示大块内存申请模型:
注意每块大内存都对应有一个头部结构(next&alloc),这个头部结构是用来将所有大内存串成一个链表用的。
这个头部结构不是直接向操作系统申请的,而是当做小块内存(头部结构没几个字节)直接在内存池里申请的。
这样的大块内存在使用完后,可能需要第一时间释放,节省内存空间,因此nginx提供了接口函数:
ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);
此函数专门用来释放某个内存池上的某个大块内存,p就是大内存的地址。
ngx_pfree只会释放大内存,不会释放其对应的头部结构,毕竟头部结构是当做小内存在内存池里申请的;遗留下来的头部结构会作下一次申请大内存之用。
3.4、cleanup资源
可以看到所有挂载在内存池上的资源将形成一个循环链表,一路走来,发现链表这种看似简单的数据结构却被频繁使用。
由图可知,每个需要清理的资源都对应有一个头部结构,这个结构中有一个关键的字段handler,handler是一个函数指针,在挂载一个资源到内存池上的时候,同时也会注册一个清理资源的函数到这个handler上。即是说,内存池在清理cleanup的时候,就是调用这个handler来清理对应的资源。
比如:我们可以将一个开打的文件描述符作为资源挂载到内存池上,同时提供一个关闭文件描述的函数注册到handler上,那么内存池在释放的时候,就会调用我们提供的关闭文件函数来处理文件描述符资源了。
3.5、内存的释放
nginx只提供给了用户申请内存的接口,却没有释放内存的接口,那么nginx是如何完成内存释放的呢?总不能一直申请,用不释放啊。针对这个问题,nginx利用了web server应用的特殊场景来完成;
一个web server总是不停的接受connection和request,所以nginx就将内存池分了不同的等级,有进程级的内存池、connection级的内存池、request级的内存池。
也就是说,创建好一个worker进程的时候,同时为这个worker进程创建一个内存池,待有新的连接到来后,就在worker进程的内存池上为该连接创建起一个内存池;连接上到来一个request后,又在连接的内存池上为request创建起一个内存池。
这样,在request被处理完后,就会释放request的整个内存池,连接断开后,就会释放连接的内存池。因而,就保证了内存有分配也有释放。
小结:通过内存的分配和释放可以看出,nginx只是将小块内存的申请聚集到一起申请,然后一起释放。避免了频繁申请小内存,降低内存碎片的产生等问题。
参考文献
- 朋友dreamice:http://bbs.chinaunix.net/thread-3626006-1-1.html
- 友人阿波:http://blog.csdn.net/livelylittlefish/article/details/6586946
- 朋友dreamice’blog:http://blog.chinaunix.net/space.php?uid=7201775
- 淘宝数据共享平台博客:http://www.tbdata.org/archives/1390