Nginx内存管理详解

目录:

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所示:

 

 

图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 ngx_pool_s { 2  ngx_pool_data_t d; 3  size_t max; 4     ngx_pool_t           *current; 5     ngx_chain_t          *chain; 6     ngx_pool_large_t     *large; 7     ngx_pool_cleanup_t   *cleanup; 8     ngx_log_t            *log; 9 };
复制代码

 

   并且在src/core/ngx_core.h中:

1
typedef  struct  ngx_pool_s        ngx_pool_t;

 

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

 d:ngx_pool_data_t结构体,描述内存池中的小块内存。当小块内存不足时,会再分配一个ngx_pool_t(里面含有一个新分配且未使用的小块内存空间和用于管理这块内存空间的ngx_pool_data_t结构体)。这些小块内存块之间通过d中的next成员链接形成的单链表。挂在d成员上。

 

max:评估申请内存属于小块还是大块的标准,在x86上默认是4095字节。

 

current:多个小块内存构成单链表时,指向分配内存时遍历的第一个小块内存。

 

chain:与内存池关系不大,略过。

 

large:ngx_pool_large_t结构体当用户申请的内存空间大于max时,就会分配大块内存。而多个大块内存之间是通过ngx_pool_large_t中的next成员链接形成的单链表。挂在large成员上。

 

cleanup:ngx_pool_cleanup_t结构体,所有待清理的资源(例如需要关闭或者删除的文件)以ngx_pool_cleanup_t对象中的next成员链接形成单链表。挂在cleanup成员上。

 

log:内存池中执行时输出日志的地方。

 

 

(a).ngx_pool_data_t

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

 

1
2
3
4
5
6
typedef  struct  {
     u_char               *last;
     u_char               *end;
     ngx_pool_t           *next;
     ngx_uint_t            failed;
} ngx_pool_data_t;

 

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

last:指向小块内存中未分配的空闲内存的首地址。

 

end:指向当前小块内存的尾部。

 

next:同属于一个内存池的多个小块内存之间,通过next成员链接形成单链表。

 

failed: 每当当前的小块内存由于空闲部分较少而不能满足用户提出的内存申请请求时,failed成员就会加1。当failed成员大于4后,ngx_pool_t的current成员就会移向下一个小块内存,在以后分配内存时,将从下一个小块内存开始遍历。

 

 

(b).ngx_pool_large_t

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

 

1
2
3
4
5
6
typedef  struct  ngx_pool_large_s  ngx_pool_large_t;
 
struct  ngx_pool_large_s {
     ngx_pool_large_t     *next;
     void                  *alloc;
};

 

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

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

 

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

 

 

(c).ngx_pool_cleanup_t

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

 

1
2
3
4
5
6
7
typedef  struct  ngx_pool_cleanup_s  ngx_pool_cleanup_t;
 
struct  ngx_pool_cleanup_s {
     ngx_pool_cleanup_pt   handler;
     void                  *data;
     ngx_pool_cleanup_t   *next;
};

 

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

handler:初始化为NULL,需要设置的清理函数。

 

1
typedef  void  (*ngx_pool_cleanup_pt)( void  *data);

 

根据上面的声明,可以看出,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).内存池操作:

 

1
2
3
ngx_pool_t *ngx_create_pool( size_t  size, ngx_log_t * log );
void  ngx_destroy_pool(ngx_pool_t *pool);
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).基于内存池的分配、释放操作

 

1
2
3
4
5
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_int_t ngx_pfree(ngx_pool_t *pool,  void  *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).随着内存池释放同步释放资源的操作

 

1
2
3
4
ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p,  size_t  size);
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);

    

    ngx_pool_cleanup_add

  添加一个需要在内存释放时同步释放的资源。该方法会返回一个ngx_pool_cleanup_t结构体,而我们得到该结构体后需要设置ngx_pool_cleanup_t的handler成员为释放资源时执行的方法。ngx_pool_clean_add的参数size,当它不为0时,会分配size大小的内存,并将ngx_pool_cleanup_t的data成员指向该内存,这样可以利用这段内存传递参数,供资源清理函数使用。当size为0时,data将为NULL。

 

  ngx_pool_run_cleanup_file

  在内存释放前,如果需要提前关闭文件(调用ngx_pool_cleanup_add添加的文件,同时ngx_pool_cleanup_t的handler成员被设置为ngx_pool_cleanup_file),则调用该方法。

 

  ngx_pool_cleanup_file

  以关闭文件来释放资源的方法,可以设置到ngx_pool_cleanup_t的handler成员。

 

  ngx_pool_delete_file 

  以删除文件来释放资源的方法,可以设置到ngx_pool_cleanup_t的handler成员。

 

 

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

 

1
void  *ngx_alloc( size_t  size, ngx_log_t * log ); void  *ngx_calloc( size_t  size, ngx_log_t * log );#define ngx_free           free

  

  这部分的声明和定义实际上并不在src/core/ngx_palloc.h中,而是在/src/os/unix/ngx_alloc.h中。

 

  ngx_alloc

  从操作系统中分配内存,通过调用malloc实现。

 

  ngx_calloc

  从操作系统中分配内存并全部初始化为0,通过调用malloc和memset实现。

 

  ngx_free

  从上面的宏定义可以看到,其就是free函数,释放内存到操作系统。

 

 

 

5.内存池管理源码详解

   本部分的源码可以在src/core/ngx_palloc.h、src/core/ngx_palloc.c、src/os/unix/ngx_alloc.h和src/os/unix/ngx_alloc.c中找到。内存池的管理主要包括内存池的创建、销毁以及重置操作。我们通过对源码的分析来研究和学习Nginx的内存管理技术。

 

(a).内存池的创建

  创建内存池的操作主要由ngx_create_pool()函数完成,代码如下:

 

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
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_memalign()函数申请对齐的内存,其大小为size个字节。如果内存申请失败,则返回NULL,否则对ngx_pool_t结构体中的成员进行初始化。在进行初始化之前,让我们先讨论以下什么是小块内存?

 

 

1
2
3
4
5
/*
  * NGX_MAX_ALLOC_FROM_POOL should be (ngx_pagesize - 1), i.e. 4095 on x86.
  * On Windows NT it decreases a number of locked pages in a kernel.
  */
#define NGX_MAX_ALLOC_FROM_POOL  (ngx_pagesize - 1)

 

 

   这是ngx_palloc.h中的一个注释及宏定义,从中我们可以看到在x86系统4095字节是一个标准。因为ngx_pagesize中存放的是当前Nginx服务器运行的系统中一页内存页的大小,而在x86的系统上就是4KB。由于存在减1的关系,这意味着在x86系统上,小于等于4095字节的内存被称为小块内存,而大于4095字节的内存被称为大块内存。当然这并不是绝对的,在上述源码中,我们看到如果传递的参数size满足:size - sizeof(ngx_pool_t) < NGX_MAX_ALLOC_FROM_POOL时,其max的值为size(小于NGX_MAX_ALLOC_FROM_POOL),而当size不满足上述不等式时,其值为NGX_MAX_ALLOC_FROM_POOL。也就是说NGX_MAX_ALLOC_FROM_POOL是一个最大的门限,申请的小块内存的大小应该不超过其大小。在初始化max之后,我们将last指向分配好的空闲内存空间的首地址,end指向内存池的尾部。并将next初始化为NULL,failed的值初始化为0。然后再将current指向这块内存池的首地址,large和cleanup也被初始化为NULL,最后返回指向分配好的内存空间的首地址。为了更加清晰地展示内存池的创建过程,下面将会举一个例子来说明。但是在这之前,我们先来分析以下ngx_memalign()函数的实现源码。

 

  关于ngx_memalign()的细节我们可以在src/os/unix/ngx_alloc.c中看到其源码,前面部分是声明,后面是定义。如下所示:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
  * Linux has memalign() or posix_memalign()
  * Solaris has memalign()
  * FreeBSD 7.0 has posix_memalign(), besides, early version's malloc()
  * aligns allocations bigger than page size at the page boundary
  */
 
#if (NGX_HAVE_POSIX_MEMALIGN || NGX_HAVE_MEMALIGN)
 
void  *ngx_memalign( size_t  alignment,  size_t  size, ngx_log_t * log );
 
#else
 
#define ngx_memalign(alignment, size, log)  ngx_alloc(size, log)
 
#endif

 

 

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
#if (NGX_HAVE_POSIX_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;
}
 
#elif (NGX_HAVE_MEMALIGN)
 
void  *
ngx_memalign( size_t  alignment,  size_t  size, ngx_log_t * log )
{
     void   *p;
 
     p = memalign(alignment, size);
     if  (p == NULL) {
         ngx_log_error(NGX_LOG_EMERG,  log , ngx_errno,
                       "memalign(%uz, %uz) failed" , alignment, size);
     }
 
     ngx_log_debug3(NGX_LOG_DEBUG_ALLOC,  log , 0,
                    "memalign: %p:%uz @%uz" , p, size, alignment);
 
     return  p;
}
 
#endif

 

  我们还需要知道的就是在linux系统下,分配内存有三个系统调用,如果不考虑内存对齐,则有malloc();如果考虑内存对齐,则有:memalign()和posix_memalign();从ngx_memalign()的具体声明和实现中,我们可以看出这其实一个条件编译。如果系统定义了NGX_HAVE_POSIX_MEMALIGN,则调用posix_memalign()申请对齐的内存;如果系统定义了NGX_HAVE_MEMALIGN,则调用memalign()申请对齐的内存;并且这两种内存对齐默认都是基于16字节的。否则直接调用ngx_alloc(),而ngx_alloc()直接调用malloc()申请不对齐的内存。讲完了内存池中三种申请内存的方式之后,我们可以开始讲解创建内存池的实例了。

  比如说我们需要创建一个大小为1024字节的内存池作为一个分配模块:

 

1
ngx_pool_t *pool = ngx_create_pool (1024,   log );

 

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

 

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

 

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

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef  struct  {
     u_char               *last; //4字节
     u_char               *end; //4字节
     ngx_pool_t           *next; //4字节
     ngx_uint_t            failed; //4字节
} ngx_pool_data_t;
 
 
struct  ngx_pool_s {
     ngx_pool_data_t       d; //16字节
     size_t                 max; //4字节
     ngx_pool_t           *current; //4字节
     ngx_chain_t          *chain; //4字节
     ngx_pool_large_t     *large; //4字节
     ngx_pool_cleanup_t   *cleanup; //4字节
     ngx_log_t            * log ; //4字节
};

 

   

  我们可以计算得到,在x86的系统中ngx_pool_t结构体各个成员变量占用的空间为40字节。因此last的值为50。end的值为10+1024=1034。max的值为1024-40=984。current=10。可以看到:

在物理内存中,申请到的内存空间被分为了两部分,前面一部分是ngx_pool_t内存管理结构各个成员变量所占用的空间,此处为40字节。后面部分的984字节的空闲空间才是我们可以在后续的程序中真正可以利用的,用来存放数据的。以上就是Nging内存池创建的主要原理和具体实现。

 

 

(b).内存池的销毁

  销毁内存池的工作主要由ngx_destroy_pool()函数完成。代码如下:

 

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
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);
         }
     }
 
#if (NGX_DEBUG)
 
     /*
      * we could allocate the pool->log from this pool
      * so we cannot use this log while free()ing the pool
      */
 
     for  (l = pool->large; l; l = l->next) {
         ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool-> log , 0,  "free: %p" , l->alloc);
     }
 
     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  (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 ;
         }
     }
}

 

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

 

 

(c).内存池的重置

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

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
         }
     }
 
     for  (p = pool; p; p = p->d.next) {
         p->d.last = (u_char *) p +  sizeof (ngx_pool_t);
         p->d.failed = 0;
     }
 
     pool->current = pool;
     pool->chain = NULL;
     pool->large = NULL;
}

   

  我们可以看到,重置内存池十分简单。首先将挂在large上的大块内存链表上的各个节点释放掉,并将pool->large赋值为NULL。之后,将所有小块内存池构成的单链表中的所有节点结尾的last指针重置到刚分配时的位置。小块内存中存储的数据并没有被释放,其在以后的内存池使用的过程中将会被覆盖更新。这可以减少内存分配的次数,提升内存重用率。但会浪费一些内存空间。

 

 

 

6.内存池使用源码详解

   内存池创建好之后,如何进行使用呢?这些内存使用完了之后是如何进行回收利用的呢?下面的部分将会详细的介绍内存池的使用。

 

(a).从内存池中申请内存

  在Nginx中,基于内存池的申请方法主要有ngx_palloc、ngx_pnalloc、ngx_pcalloc和ngx_pmemalign共4种方法。而不基于内存池,直接从操作系统中申请内存的主要有ngx_alloc和ngx_calloc共两种方法。在这一小节中,我们只讲述从内存池中申请内存相关的4中方法。而其他的部分将会在后面的小节进行讲解。

  基于内存池的4中内存申请方法的区别在第4章:内存池API介绍中已经详细阐述了。此处不再赘述。

 

(1).ngx_palloc

  下面给出源码:

 

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

 

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

 

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

 

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

 

1
2
#define ngx_align_ptr(p, a)                                                   \
     (u_char *) ((( uintptr_t ) (p) + (( uintptr_t ) a - 1)) & ~(( 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()函数。其源码如下所示:

 

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

 

  既然当前整个内存池都不能满足用户内存的申请,而我们的操作系统明明还有内存可用(资源耗尽的情况除外),那我们总不能拒绝用户的合理请求吧。ngx_palloc_block()函数就是应对这种情况而出现的。该函数实现了对内存池的扩容。

  需要注意的是,由于我们遍历完了整个链表,因此此时的pool指针指向的是内存池链表的最后一个节点。所以说在ngx_palloc_block()中计算的是当前内存池最后一个节点的大小psize。该大小为需要扩展的空间大小。然后,我们调用前面提到过的ngx_memalgin()函数申请新的内存空间,大小为psize,作为新的小块内存池节点。之后,我们将这个节点挂在内存池的最后面。具体怎么实现的呢?我们来详细的看一看。

  首先将这个新节点进行初始化,包括d->end、d->next、d->failed。然后将指向这块内存的首地址m后移sizeof(ngx_pool_data_t),大家可能还记得我们在创建内存池ngx_pool_create()时,内存池中空闲地址的首地址是在整个内存池的首地址的基础上后移了sizeof(ngx_pool_t),那么为什么此处创建新的内存池节点只需要后移sizeof(ngx_pool_data_t)呢?在x86系统上,sizeof(ngx_pool_data_t)对应16个字节,而sizeof(ngx_pool_t)对应40个字节。其实大家仔细想一想,我们创建的内存池是小块内存池链表的第一个节点,这个节点中除了包含ngx_pool_data_t结构体之外,还需要包含large指针、cleanup指针等。而小块内存池后面的节点均没有必要包含这些成员,因为我们的large链表和cleanup链表是直接且仅仅挂在小块内存池链表的第一个节点上的。不需要再挂到后续的其他小块内存池链表的结构上。这么想是不是觉得比较合理呢?答案就是这样的。但是我们之前的重置内存池操作中,并没有把后续的从第二个节点开始的小块内存池链表上的空闲内存的起始地址初始化为(u_char *)p + sizeof (ngx_pool_data_t),而是将所有节点(包括第一个)的空闲内存地址初始化为(u_char *)p + sizeof (ngx_pool_t)。这样做会浪费一些内存空间,但是整个重置内存池操作会简单一点点。因为不用区分第一个节点和其他节点。如果区分的话,我们需要让第一个节点的空闲内存的起始地址初始化为(u_char *)p + sizeof (ngx_pool_t),将其他节点的空闲内存的起始地址初始化为(u_char *)p + sizeof (ngx_pool_data_t)。我们的Nginx源码就是这么实现的。大家知道就行了。因为这并不会影响内存池的使用。

  在完成对新的内存池节点的初始化之后。我们需要将这个节点加入到小块内存池链表的尾部。具体怎么实现的呢?

  首先我们找到current指针,并根据这个指针遍历小块内存池链表,在每一个遍历中,我们将每个节点的failed成员加1(这是因为你们这些节点不能给我分配内存啊,不然也不会调用我,因此对你们的failed成员统统加1)。并且加1之后,进行判断,如果某个节点的failed成员的值大于4,那么就将current指向下一个节点(下次再分配内存时将会自动忽略这个节点)。

  在遍历完小块内存池的链表后,我们的pool指针已经指向了链表的最后一个节点,因此在链表的尾部插入一个节点非常简单,p->d.next = new这个语句就能完成。之后返回这个指向这个新节点的空闲内存空间的首地址。

  上述就是ngx_palloc_small()函数完成的功能,内容比较多大家可能都忘了,我们还没有讲解ngx_palloc()函数的另外一个部分:ngx_palloc_large(),这个函数是用于当用户申请的内存大小大于我们的小块内存标准max的情况。下面我们将会看到,这种情况下,申请的内存将被当作是大数据块,将会被挂在large链表上。先给出ngx_palloc_large()的源码:

 

 

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
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_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_alloc()函数申请一块大小为size的内存空间,ngx_alloc()函数实际上就是简单的封装了以下malloc()函数,后面我们会详细的讲解。这里知道它是由malloc实现的就好了。申请完内存之后,开始遍历large链表,找到链表中alloc为NULL的节点,用alloc指向刚申请到的内存空间并返回。注意这段循环代码至多执行3次,如果在3次后都没有找到alloc为NULL的节点,就会退出循环,继续执行后面的代码。限制代码执行的次数是为了提升内存分配的效率,因为large链表可能会很大。

  之后,我们调用ngx_palloc_small()重新申请一块大小为sizeof(ngx_pool_large_t)结构体大小的内存,建立一个新节点。最后我们把新建立的节点插入到large链表的头部,返回申请的内存空间的起始地址。为什么是插入头部而不是插入尾部呢?这里面其实是有依据的,因为我们之前为了防止large过大将遍历large链表的次数设置为3,如果插在尾部,那么遍历链表前面的三个节点就没有意义了,因为每次都可能会遍历不到后面的空闲节点,而导致每次都需要重新建立新节点。并且插入头部,从头部开始遍历也会使得效率比较高。因为这样遍历到空闲的大块内存节点的概率会高很多。

 

 

(2).ngx_pnalloc

  先给出其源码:

 

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