目录:

1.Nginx内存管理介绍

2.Nginx内存池的逻辑结构

3.Nginx内存池的基本数据结构

4.内存池基本操作介绍

5.内存池管理源码详解

6.内存池使用源码详解

7.小结

 

 

 

1.Nginx内存管理介绍

  在C/C++语言程序设计中,通常由程序员自己管理内存的分配和释放,其方式通常是malloc(free)和new(delete)等API。这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片从而降低性能。通常我们所使用的解决办法就是内存池。

  什么是内存池呢?内存池就是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。而不是每次需要了就调用分配内存的系统API(如malloc)进行申请,每次不需要了就调用系统释放内存的API(如free)进行释放。这样做的一个显著优点是,使得内存分配效率得到提升。因此使用内存池的方式对程序所使用的内存进行统一的分配和回收,是当前最流行且高效的内存管理方法,能够在很大程度上降低内存管理的难度,减少程序的缺陷,提高整个程序的稳定性。

  通过减少频繁的内存申请和释放可以提升效率很容易理解,那么内存池究竟是怎么提高程序的稳定性的呢?我们知道在C/C++语言中,并没有提供直接可用的垃圾回收机制,因此在程序编写中, 一个特别容易发生的错误就是内存泄露,对于运行时间短,内存需求小的程序来说,泄露一点内存除了影响程序运行效率之外可能并不会造成多大的问题。但是类似于Ngnix这样需要长期运行的web服务器程序来说,内存泄露是一件非常严重的灾难,这会使得程序由于内存耗尽而崩溃,重启之前不再能够提供相应的web服务。还有一种情况就是当内存分配与释放的逻辑在程序中相隔较远时,很容易发生内存被释放两次乃至多次的情况。使用内存池使得我们在开发程序时,只用关心内存的分配,而释放就交给内存池来完成。

  那么内存池在Nginx中究竟是怎么使用的呢?通常我们对于每个请求或者连接都会建立相应的内存池,建立好内存池之后,我们可以直接从内存池中申请所需要的内存,而不用去管内存的释放,唯一需要注意的就是当内存池使用完成之后需要记得销毁内存池。此时,内存池会调用相应的数据清理函数(如果有的话),之后会释放在内存池中管理的内存。

  大家可能会问,既然申请的内存在内存池销毁的时候才会被释放,这不会存在内存的浪费么?毕竟使用完了不再需要的内存为什么不立即释放而非要等到销毁内存池时才释放呢?确实存在这个问题,不过大家不用担心。在Nginx中,对于大块内存可以使用ngx_pfree()函数提前释放。并且由于Nginx是一个纯粹的web服务器,而web服务器通常使用的协议是Http协议,并且在传输层使用的是Tcp协议,我们知道每一个tcp连接都是由生命周期的,因此基于tcp的http请求都会有一个很短暂的生命周期。对于这种拥有很短暂生命周期的请求,我们所建立的内存池的生命周期也相应会很短暂,因此其所占用的内存资源很快就可以得到释放,不会出现太多的资源浪费的问题。毕竟工程就是一种折中嘛,我们需要在内存资源浪费和减低程序内存管理难度、提升效率之间选择一个合适的权衡。

  说了这么多,现在就让我们开始研究和学习Nginx内存管理的机制和源码吧。注:本文的讲解都是基于nginx-1.10.3版本。

 

 

 

2.Nginx内存池的逻辑结构

  前面提到Nginx内存管理机制其实就是内存池,其底层实现就是一个链表结构。我们需要对内存池进行管理和分配,依赖的就是ngx_pool_t结构体,可以认为该结构就是内存池的分配管理模块。那么内存池的逻辑结构究竟是什么样呢?其实就是一个ngx_pool_t结构体,在这个结构体中包含了三个部分:小块内存形成的单链表,大块内存形成的单链表和数据清理函数形成的单链表。先给出一张整个内存池内部实现的结构图,方便大家理解。具体如图2.1所示:

 

 Nginx内存管理详解_Nginx

图2.1 Nginx内存池示意图

 

   图2.1完整的展示了ngx_pool_t内存池中小块内存、大块内存和资源清理函数链表间的关系。图中,内存池预先分配的剩余空闲内存不足以满足用户申请的内存需求,导致又分配了两个小内存池。其中原内存池的failed成员已经大于4,所以current指向了第2块小块内存池,这样当用户再次从小块内存池中请求分配内存空间时,将会直接忽略第1块小内存池,从第2块小块内存池开始遍历。从这里可以看到,我们使用的内存池确实存在当failed成员大于4之后不能利用其空闲内存的资源浪费现象(由于current指针后移)。值得注意的是:我们的第2、3块小块内存池中只包含了ngx_pool_t结构体和数据区,并不包含max、current、...、log。这是由于后续第1块小内存池已经包含了这些信息,后续的小块内存池不必在浪费空间存储这些信息。我们在第6小节:内存池的使用中将会有所介绍。图中共分配了3个大块内存,其中第二块的alloc为NULL(提前调用了ngx_pfree())。图中还挂在了两个资源清理方法。提醒一下的是:如果在这里没有弄清楚,没有关系,看完了后面的部分再回过头来理解这个示意图就能够很好的理解了。这里只是先给出一个概括性的Nginx内存池逻辑结构的介绍,先给大家留下一个大概的印象。

 

 

 

3.Nginx内存池的基本数据结构

本部分主要介绍内存池中重要的数据结构,主要是ngx_pool_t,然后介绍ngx_pool_t中三个重要数据结构:ngx_pool_data_t,ngx_pool_large_t和ngx_pool_cleanup_t。

 

(1)ngx_pool_t

  我们可以在Nginx的源码的src/core/目录下的nax_palloc.h头文件中看到:

 

1
struct 
1
2
3
4
5
6
struct  u_char               *last;
     ngx_uint_t            failed;
1
2
3
4
5
6
struct  ngx_pool_large_s {
     };

 

下面将具体讲解ngx_pool_large_t结构体中每个成员的含义和用途:

next:所有大块内存通过next指针链接在一起形成单链表。

 

alloc:指向分配的大块内存,后面我们将会看到大块内存底层是通过ngx_alloc分配,ngx_free释放。释放完了之后赋值为NULL。

 

 

(c).ngx_pool_cleanup_t

   我们可以在Nginx的源码的src/core/nax_palloc.h头文件中看到:

 

ngx_pool_cleanup_s  ngx_pool_cleanup_t;
 
     void                  ngx_pool_cleanup_t   *next;
1
  *data);
void

 

根据上面的声明,可以看出,ngx_pool_clean_pt是一个函数指针,有一个通用型的参数data,返回类型为void。后面我们会看到当销毁内存池的时候,底层会遍历挂在cleanup成员上的单链表上的各个节点,调用各节点的数据清理函数完成相应的清理操作。这是通过回调函数实现的。

 

data:用于向数据清理函数传递的参数,指向待清理的数据的地址,若没有则为NULL。我们可以通过ngx_pool_cleanup_add函数添加数据清理函数,当其中的参数size>0时,data不为NULL。

 

next:用于链接所有的数据清理函数形成单链表。由ngx_pool_cleanup_add函数设置next成员,用于将当前ngx_pool_cleanup_t(由ngx_pool_cleanup_add函数返回)添加到cleanup链表中。

 

 

 

4.内存池基本操作介绍

  这一部分主要简单讲解与内存池管理有关的基本操作(共15个)。主要包括四个部分:(a).内存池操作 (b).基于内存池的分配、释放操作 (3).随着内存池释放同步释放资源的操作 (4).与内存池无关的分配、释放操作。在第5和第6节中,我们会对部分常用内存池的操作进行代码上的详细介绍。

 

(a).内存池操作:

 

size, ngx_log_t * void  ngx_reset_pool(ngx_pool_t *pool);

 

  ngx_create_pool

  创建内存池,其参数size为整个内存的大小,包括结构管理(ngx_pool_t)和后续可分配的空闲内存。这意味着,size必须大于等于sizeof(ngx_pool_t),通常在32位的系统是是40字节,后面我们介绍源码时会详细的介绍。通常size的默认大小为NGX_DEFAULT_POOL_SIZE(#define NGX_DEFAULT_POOL_SIZE    (16 * 1024)),可以看到为16k。不用担心其不够用,因为当不够用时,Nginx会对内存池进行内存空间的扩展,也就是申请一个新的内存池(链表)节点(程序中成为一个block),然后挂在内存池的最后面。

 

  ngx_destory_pool

  销毁内存池,它会执行通过ngx_pool_cleanup_add函数添加的各种资源清理方法,然后释放大块内存,最后把整个pool分配的内存释放掉。

 

  ngx_reset_pool

  重置内存池,即将在内存池中原有的内存释放后继续使用。后面我们会看到,这个方法是把大块的内存释放给操作系统,而小块的内存则在不释放的情况下复用。

 

 

(b).基于内存池的分配、释放操作

 

size_t  *ngx_pnalloc(ngx_pool_t *pool,  void  size);
size_t  alignment);
*p);

  

      ngx_palloc

  分配地址对齐的内存。内存对齐可以减少cpu读取内存的次数,代价是存在一些内存浪费。

 

  ngx_pnalloc

  同ngx_palloc,区别是分配内存时不考虑对齐。

 

  ngx_pcalloc

  同ngx_palloc,区别是分配完对齐的内存后,再调用memset全部初始化为0。

 

  ngx_pmemalign

  按参数alignment进行地址对齐来分配内存。注意,这样分配的内存不管申请的size有多小,都不会使用小块内存,它们直接从进程的堆中分配,并挂在大块内存组成的large单链表中。

 

  ngx_pfree

  提前释放大块内存。由于其实现是遍历large单链表,寻找ngx_pool_large_t对应的alloc成员后调用ngx_free(alloc),实际上是直接调用free(alloc),释放内存给操作系统,将ngx_pool_large_t移出链表并删除。效率不高。

 

 

(c).随着内存池释放同步释放资源的操作

 

size);
void  *data);
void 
1
log *ngx_calloc( log
size, ngx_log_t * {
     );
(p == NULL) {
NULL;
     (ngx_pool_t);
     p->d.failed = 0;
 
sizeof p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
 
     p->large = NULL;
          }
*ngx_alloc(
ngx_pool_t *

 

   

  在这段代码中,首先通过ngx_memalign()函数申请对齐的内存,其大小为size个字节。如果内存申请失败,则返回NULL,否则对ngx_pool_t结构体中的成员进行初始化。在进行初始化之前,让我们先讨论以下什么是小块内存?

 

 

* NGX_MAX_ALLOC_FROM_POOL should be (ngx_pagesize - 1), i.e. 4095 on x86.
 
  * FreeBSD 7.0 has posix_memalign(), besides, early version's malloc()
  void  alignment,  log #define ngx_memalign(alignment, size, log)  ngx_alloc(size, log)
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
  size_t  size, ngx_log_t * {
*p;
err;
 
              , err,
, alignment, size);
     ngx_log_debug3(NGX_LOG_DEBUG_ALLOC,                           }
 
*
alignment,  log           if  ngx_log_error(NGX_LOG_EMERG,                              ngx_log_debug3(NGX_LOG_DEBUG_ALLOC,                           }
 
1
);
/*
void log

 

  为了方便,我们不妨假设申请的这块内存的起始地址为10。执行完创建内存池的操作后,内存中的分布情况如图5.1所示:

 

Nginx内存管理详解_Nginx_02

图5.1 创建内存池内存片段图

 

  从执行结果可以看出:创建的内存池总共占用了1024个字节,起始地址为10,结束地址为1034。指向内存池的指针为pool。last指针为50(10+40),因为起始地址是10,而ngx_pool_t结构体所占用的内存空间为40字节,怎么计算得到的呢?其实很简单,只需要考虑结构体在内存中的对齐问题即可。在x86中(x64中指针在内存中占用8字节而不是4字节)如下所示:

 

 

{
//4字节
//4字节
//4字节
//4字节
ngx_pool_s {
//16字节
max; ngx_pool_t           *current; ngx_chain_t          *chain; ngx_pool_large_t     *large; ngx_pool_cleanup_t   *cleanup; ngx_log_t            * //4字节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
ngx_pool_t          *p, *n;
     for  if  ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->                                          }
#if (NGX_DEBUG)
 
      * so we cannot use this log while free()ing the pool
              , 0,       for  ; p = n, n = n->d.next) {
log "free: %p, unused: %uz" if  break }
#endif
 
(l = pool->large; l; l = l->next) {
(l->alloc) {
         }
 
(p = pool, n = pool->d.next;           if  break }
}
ngx_destroy_pool(ngx_pool_t *pool)

 

  我们可以看到,销毁内存池的主要步骤为:先通过遍历挂在cleanup上数据清理函数链表,通过回调函数handler做相应的数据清理;中间输出部分只与调试程序相关,可忽略。然后遍历挂在large上的大块内存链表,调用ngx_free()函数释放节点所占的大块内存空间;最后,遍历挂在d->next上的小块内存池链表,释放小块内存池(包括管理结构和数据区)占用的空间,在这一步中,我们首先清理了第一块ngx_pool_t(包括了large、cleanup等成员)代表的小块内存池,然后再清理剩下的其他小块内存池。经过以上三个过程,就可以完成数据清理、释放整个内存池占用的内存空间,并销毁内存池。需要注意的是:由于内存池的结构,我们必须最后清理管理结构ngx_pool_t(第一块小块内存池),因为如果先清理第一块ngx_pool_t代表的内存池的话,我们就找不到挂在large和cleanup上的单链表了,因为我们清理了其单链表的第一个节点。

 

 

(c).内存池的重置

  重置内存池,就是将内存池分配到初始分配的状态。这是由ngx_reset_pool()函数完成的。代码如下:

 

{
     for  if  ngx_free(l->alloc);
     for  p->d.last = (u_char *) p +           }
 
     pool->large = NULL;
1
2
3
4
5
6
7
8
9
10
11
size)
                        }
*

 

  从其实现中,我们可以看出,ngx_palloc()总共有两个参数,第一个是在那个内存池上申请内存(之前我们曾经提到过通常为每个Http请求或者连接创建一个内存池,此处需要传递的参数就是这些内存池对应的指针),另一个参数是size,表示申请内存的大小。进入函数后,首先是判断申请的内存大小和max(小块内存标准)的关系,如果size<max,就调用ngx_palloc_small()函数申请内存。否则调用ngx_palloc_large()函数申请内存。下面让我们先来看ngx_palloc_small()函数的源码,如下所示:

 

void  size_t       ngx_pool_t  *p;
 
              if  m = ngx_align_ptr(m, NGX_ALIGNMENT);
         size_t p->d.last = m + size;
 
m;
              }

 

  从上述源码中,我们可以看到,该函数从current指向的内存池(小块内存池链表)中开始循环遍历。在每一次遍历中,我们首先获得目前内存池中未分配的空闲内存的首地址last,并赋值给m,然后由于从ngx_palloc()函数中传递过来的align=1,因此调用ngx_align_ptr(),这是个什么呢?仅从此我们不能判断其是函数还是宏,下面我们给出其源码,在src/core/ngx_config.h中,如下所示:

 

(u_char *) ((( uintptr_t ) a - 1))

 

  可以看出,这是一个宏定义,该操作比较巧妙,用于计算以参数a对齐后的偏移指针p。实际上,我们最后分配的内存空间就是从对齐后的偏移指针开始的,这可能会浪费少数几个字节,但却能提高读取效率。接着分析ngx_palloc-small()函数中的源码,在调用完宏ngx_align_ptr(m, NGX_ALIGNMENT)后我们得到了以默认参数16对齐的偏移指针m。此时,我们已经拥有了对齐后的空闲内存地址空间的首地址m和尾部地址end,我们就可以计算出该块内存池(一个block)剩余的空闲内存空间大小:p->d.end - m。那么这个剩余的空闲内存空间是否一定能满足用户的内存申请请求(size个字节)呢?答案是否定的。因此我们需要将从current开始的每一个小块内存池的剩余空闲内存空间和size进行比较,遍历链表直到找到满足申请大小(size个字节)的小块内存池。如果小块内存池链表上的某块小块内存能够满足需求,那么我们就将从Nginx的内存池中划分出内存空间,并更新last的值(将last的值后移size个字节),然后返回m。

  如果遍历完整个小块内存池都没有找到满足申请大小的内存,则程序调用ngx_palloc_block()函数。其源码如下所示:

 

*
size)
u_char      *m;
psize;
new psize = (      );
(m == NULL) {
NULL;
                         (ngx_pool_data_t);
                                }
     ;
 
m;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
  size_t            ngx_pool_large_t  *large;
 
log if  return  }
 
                           return  }
 
(n++ > 3) {
;
     large = ngx_palloc_small(pool,                return  }
 
     pool->large = large;
 
p;
1
2
3
4
5
6
7
8
9
10
11
size)
                        }
void *

 

  我们可以看到,ngx_pnalloc()和ngx_palloc()非常相似,唯一的区别就是ngx_pnalloc()中调用的是ngx_palloc_small(pool, size, 0),而ngx_palloc()中调用的是ngx_palloc_small(pool, size, 1)。那么实际上的含义有什么区别呢?ngx_pnalloc()分配内存时不考虑内存数据对齐,而ngx_palloc()分配内存时考虑内存数据对齐。

 

 

(3).ngx_pcalloc

  我们先给出其源码,如下所示:

 

ngx_pcalloc(ngx_pool_t *pool,  {
*p;
 
              }
 
p;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
size,  {
*p;
     );
(p == NULL) {
NULL;
     (ngx_pool_large_t), 1);
(large == NULL) {
              large->alloc = p;
     return 
*p)
ngx_pool_large_t  *l;
 
(l = pool->large; l; l = l->next) {
(p == l->alloc) {
log "free: %p" ngx_free(l->alloc);
                      }
 
NGX_DECLINED;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
     (ngx_pool_cleanup_t));
(c == NULL) {
NULL;
              if  return  }
 
else  c->data = NULL;
     c->next = p->cleanup;
 
     , 0,       }
*
ngx_int_t
ngx_pool_cleanup_add(ngx_pool_t *p, 

 

  从其实现中我们可以看出,我们首先调用ngx_palloc()函数申请cleanup单链表中的一个新节点(指向ngx_pool_cleanup_t结构体的指针),然后根据参数size是否为0决定是否需要申请存放目标数据的内存空间。当size>0时,调用ngx_palloc()函数申请大小为size个字节的用于存放待清理的数据的内存空间。这些要清理的数据存储在ngx_pool_cleanup_t结构体的data成员指向的内存空间中。这样可以利用这段内存传递参数,供清理资源的方法使用。当size=0时,data为NULL。最后将新生成的ngx_pool_cleanup_t结构体挂在cleanup单链表的头部。返回一个指向ngx_pool_cleanup_t结构体的指针。而我们得到后需要设置ngx_pool_cleanup_t的handler成员为释放资源时执行的方法。

返回的指向ngx_pool_cleanup_t结构体的指针具体怎么使用呢?我们对ngx_pool_cleanup_t结构体的data成员指向的内存空间填充目标数据时,将会为handler成员指定相应的函数。

 

 

(2).ngx_pool_run_cleanup_file()

  在内存池释放前,如果需要提前关闭文件,则调用该方法。下面给出其源码,如下所示:

 

{
     for  if  cf = c->data;
 
(cf->fd == fd) {
                 return }
    
       u_char               *name;
log
*data)
ngx_pool_cleanup_file_t  *c = data;
 
log ,
              , ngx_errno,
" \"%s\" failed" }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
     ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, c-> "file cleanup: fd:%d %s" c->fd, c->name);
 
(ngx_delete_file(c->name) == NGX_FILE_ERROR) {
                      , err,
" \"%s\" failed" }
              , ngx_errno,
" \"%s\" failed" }
1
2
3
4
5
6
 
 
  size_t  )
void   p =                , ngx_errno,
, size);
     , 0,       }
typedef
void
ngx_pool_delete_file( #define ngx_close_file_n         "close()"
void

 

  可以看到,其实现非常简单。仅仅是封装了malloc()函数,并做了一些日志和调试方面的处理。

 

 

(2).ngx_calloc()

  ngx_calloc()和ngx_alloc()非常相似,唯一的区别是在调用malloc()函数申请完内存之后,会调用ngx_memzero()函数将内存全部初始化为0。ngx_memzero()就是memset()函数。

(3).ngx_pcalloc

  我们先给出其源码,如下所示:

 

1
2
3
4
5
6
7
8
9
10
11
12
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_pcalloc()和ngx_palloc()非常的相似,唯一的区别就是ngx_pcalloc()函数将刚申请到的内存空间全部初始化为0。

 

 

(4).ngx_pmemalign

  我们给出其源码,如下所示:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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_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;
}

 

  从其源码实现中,我们可以看出ngx_pmemalign()函数首先调用ngx_memalign()函数来申请对齐的内存地址空间。然后ngx_palloc_small()函数来建立一个新的大数据块节点。并将ngx_pmemalign()函数申请的内存空间直接挂在新建的大块数据节点的alloc成员上。最后再将新建的大数据块节点挂在大块内存组成的单链表中。

  上面就是整个基于内存池申请内存的4种方法的源码实现及其分析。下面我们会继续讲解释放内存和回收内存。

  ngx_pfree()函数用于提前释放大块内存。

 

 

(b).释放内存

  此处我们将介绍基于内存池的内存释放操作函数ngx_pfree(),与内存池无关的内存释放操作ngx_free()将在后面被讲解。

  在Nginx中,小块内存并不存在提前释放这么一说,因为其占用的内存较少,不太需要被提前释放。但是对于非常大的内存,如果它的生命周期远远短于所属的内存池,那么在内存池销毁之前提前释放它就变得有意义了。下面先给出其源码:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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_pfree()函数的实现十分简单。通过遍历large单链表,找到待释放的内存空间(alloc所指向的内存空间),然后调用ngx_free()函数释放内存。后面我们会看到ngx_free()函数是free()函数的一个简单封装。释放alloc所占用的空间后,将alloc设置为NULL。我们需要注意的是:ngx_pfree()函数仅仅释放了large链表上每个节点的alloc成员所占用的空间,并没有释放ngx_pool_large_t结构所占用的内存空间。如此实现的意义在于:下次分配大块内存时,会期望复用这个ngx_pool_large_t结构体。从这里可以想到,如果large链表中的元素很多,那么ngx_pfree()的遍历耗损的性能是不小的,如果不能确定内存确实非常大,最好不要调用ngx_pfree。

 

 

(c).随着内存池释放同步释放资源的操作

  在Nginx服务器程序中,有些数据类型在回收其所占的资源时不能直接通过释放内存空间的方式进行,而需要在释放之前对数据进行指定的数据清理操作。ngx_pool_cleanup_t结构体的函数指针handler就是这么一个数据清理函数,其data成员就指向要清理的数据的内存地址。我们将要清理的方法和数据存放到ngx_pool_cleanup_t结构体中,通过next成员组成内存回收链表,就可以实现在释放内存前对数据进行指定的数据清理操作。而与这些操作相关的方法有:ngx_pool_cleanup_add()、ngx_pool_run_cleanup_file()、ngx_pool_cleanup_file()和ngx_pool_delete_file()共4种。下面我们将分别讲解这些操作。

 

(1).ngx_pool_cleanup_add()

  这个方法的目的是为了添加一个需要在内存池释放时同步释放的资源。我们依照惯例还是先给出其源码,然后对源码进行分析和学习。其源码如下所示:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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_palloc()函数申请cleanup单链表中的一个新节点(指向ngx_pool_cleanup_t结构体的指针),然后根据参数size是否为0决定是否需要申请存放目标数据的内存空间。当size>0时,调用ngx_palloc()函数申请大小为size个字节的用于存放待清理的数据的内存空间。这些要清理的数据存储在ngx_pool_cleanup_t结构体的data成员指向的内存空间中。这样可以利用这段内存传递参数,供清理资源的方法使用。当size=0时,data为NULL。最后将新生成的ngx_pool_cleanup_t结构体挂在cleanup单链表的头部。返回一个指向ngx_pool_cleanup_t结构体的指针。而我们得到后需要设置ngx_pool_cleanup_t的handler成员为释放资源时执行的方法。

返回的指向ngx_pool_cleanup_t结构体的指针具体怎么使用呢?我们对ngx_pool_cleanup_t结构体的data成员指向的内存空间填充目标数据时,将会为handler成员指定相应的函数。

 

 

(2).ngx_pool_run_cleanup_file()

  在内存池释放前,如果需要提前关闭文件,则调用该方法。下面给出其源码,如下所示:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 ;
             }
         }
     }
}

 

  再给出ngx_pool_cleanup_file结构体的声明和定义(在src/core/ngx_palloc.h头文件中),如下所示:

 

1
2
3
4
5
typedef  struct  {
     ngx_fd_t              fd;
     u_char               *name;
     ngx_log_t            * log ;
} ngx_pool_cleanup_file_t;

 

  从上述源码中,我们可以看出,ngx_pool_run_cleanup_file()通过遍历cleanup单链表,寻找单链表上的一个节点,这个节点满足handler(函数指针)等于ngx_pool_cleanup_file(在与函数名相关的表达式中,函数名会被编译器隐式转换成函数指针)。由于ngx_pool_cleanup_t结构体的data成员经常会指向ngx_pool_cleanup_file_t(在后面的ngx_pool_cleanup_file()函数中我们可以看到),我们将这个节点data指针赋值给cf(ngx_pool_cleanup_t结构指针)。之后如果传递过来的参数fd与cf->fd相同的话(代表我们找到了需要提前关闭的文件描述符fd),就提前执行ngx_pool_cleanup_file(fd),进行文件的关闭操作。

 

 

(3).ngx_pool_cleanup_file()

  该方法以关闭文件的方式来释放资源,可以被设置为ngx_pool_cleanup_t的handler成员(函数指针)。我们给出其源码实现,如下所示:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
     }
}

 

  可以看出,ngx_pool_cleanup_t结构的data成员指向ngx_pool_cleanup_file_t结构体(前面讲解ngx_pool_run_cleanup_file()提到过)。之后直接调用ngx_close_file()函数关闭对应的文件。而ngx_close_file()底层是是通过close()函数实现的。

 

 

(4).ngx_pool_delete_file()

  以删除文件来释放资源的方法,可以设置到ngx_pool_cleanup_t的handler成员。我们先给出其源码,如下所示:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void
ngx_pool_delete_file( void  *data)
{
     ngx_pool_cleanup_file_t  *c = data;
 
     ngx_err_t  err;
 
     ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, c-> log , 0,  "file cleanup: fd:%d %s" ,
                    c->fd, c->name);
 
     if  (ngx_delete_file(c->name) == NGX_FILE_ERROR) {
         err = ngx_errno;
 
         if  (err != NGX_ENOENT) {
             ngx_log_error(NGX_LOG_CRIT, c-> log , err,
                           ngx_delete_file_n  " \"%s\" failed" , c->name);
         }
     }
 
     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);
     }
}

 

  可以看出,ngx_pool_cleanup_t结构的data成员指向ngx_pool_cleanup_file_t结构体,在程序中我们先将传递过来的参数data(待清理的目标数据)赋值给c,然后对c的成员name(文件名称)调用ngx_delete_file()函数,完成对文件的删除操作,之后调用ngx_close_file()函数关闭相应的文件流(关闭这个文件流可以阻止删除的文件再次被访问,并且释放FILE结构使得它可以被做用于其他的文件),这就是我们为什么在删除对应的文件后还需要关闭打开的文件流的原因。

  补充一下:ngx_close_file和ngx_delete_file其实是一个宏定义,我们可以在src/os/unix/ngx_files.h中看到其具体实现,如下所示:

 

1
2
3
4
5
6
#define ngx_close_file           close
#define ngx_close_file_n         "close()"
 
 
#define ngx_delete_file(name)    unlink((const char *) name)
#define ngx_delete_file_n        "unlink()"

 

  可以看到,ngx_close_file其实就是close,在Nginx服务器程序编译阶段仅仅做一个简单的替换。ngx_delete_file(name)也是一个宏定义,本质上为unlink((const char *) name),该函数会删除参数name指定的文件。

  

 

(d).与内存池无关的资源分配、释放操作

   与内存池无关的内存分配和释放操作主要有ngx_alloc()、ngx_calloc()和ngx_free()共3中操作方法。下面我们将继续讲解它们的具体实现。

 

(1).ngx_alloc()

  ngx_alloc()函数直接从操作系统中申请内存,其实现是对malloc()函数的一个简单封装。我们可以在src/os/unix/ngx_alloc.c中找到其源码。如下所示:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}

 

  可以看到,其实现非常简单。仅仅是封装了malloc()函数,并做了一些日志和调试方面的处理。

 

 

(2).ngx_calloc()

  ngx_calloc()和ngx_alloc()非常相似,唯一的区别是在调用malloc()函数申请完内存之后,会调用ngx_memzero()函数将内存全部初始化为0。ngx_memzero()就是memset()函数。

 

 

(3).ngx_free()

  我们可以在src/os/unix/ngx_alloc.h中看到其源码,如下所示:

 

1
#define ngx_free          free

 

  可以看到Nginx程序释放内存的函数非常简单,和销毁内存池中用的是同一个(free)。这里需要再次说明的是:对于在不同场合下从内存池中申请的内存空间的释放时机是不一样的。一般只有大数据块才直接调用ngx_free()函数进行释放,其他数据空间的释放都是在内存池销毁的时机完成的,不需要提前完成。

  至此,Nginx与内存相关的操作的源码实现已基本讲完了。大家如果想进一步研究和学习Nginx内存管理机制,可以从官方下载Nginx源码,从源码中去发现Nginx降低系统内存开销的方法。

 

7.小结

  所有的讲解都讲述完了,我们来进行总结一下。在第1节中,我们介绍了Nginx的内存管理机制-内存池的基本原理和使用内存池管理Nginx服务器程序带来的好处。为了方便大家对内存池结构的理解,我们在第2节中特意给出了ngx_pool_t内存池的示意图2.1,并简单的阐述了这个图的具体含义。在此基础上,我们继续在第3节中讲述了与内存池相关的重要的数据结构,主要包括ngx_pool_t、ngx_pool_data_t、ngx_pool_large_t和ngx_pool_cleanup_t。然后为了给大家一个内存池操作方法的宏观介绍,我们在第4节讲述了内存的主要操作方法(共15个分成4类)。之后在第5节中我们详细介绍了内存池的管理,主要包括内存池的创建、销毁和重置。在第6节中我们详细介绍了内存池的使用,主要包括从内存池中如何申请内存、释放内存和回收内存。这两个小结是整个Nginx内存管理的精华部分,我们在这部分中详细的分析Nginx的源码实现,从源码的角度去讲解Nginx内存管理用到的技术,方便我们在以后的程序设计中可以借鉴和学习。最后,希望这篇文章能真正帮助到大家学习Nginx。