nginx源码分析之内存池实现原理

建议看本文档时结合nginx源码;

1.1   什么是内存池?为什么要引入内存池?

内存池实质上是接替OS进行内存管理,应用程序申请内存时不再与OS打交道,而是从内存池中申请内存或者释放内存到内存池,因此,内存池在实现的过程中,必然有一部分操作时从OS中申请内存,或者释放内存到OS,如下图所示:

nginx源码分析之内存池实现原理_第1张图片

 

图1

内存池的引入可有效解决两个问题:

(1) 降低应用程序与OS之间进行频繁内存和释放的系统调用,进而降低程序运行期间在两个空间的切换,提升了程序运行效率;

(2)内存池可根据应用特性组织内存管理方式,能有效降低操作系统的内存碎片。

内存池的实现方案非常多,例如以前写过的内存池demo:

http://blog.csdn.net/houjixin/article/details/7595817

内存池的实现过程中一般包括两个方面:(1)一套完整内存的合理组织和管理方式;(2)一套完善的接口函数对用户(使用内存池的应用程序)提供内存操作;

1.2  Nginx内存池的实现方案分析

1.2.1  与操作系统相关的内存操作函数

在nginx中,与OS直接相关的内存操作在文件:src\os\unix目录下的ngx_alloc.c和ngx_alloc.h中,主要函数有:

(1)void *ngx_alloc(size_t size, ngx_log_t *log);

该函数主要通过malloc函数从OS中申请一块内存;

(2)void *ngx_calloc(size_t size, ngx_log_t *log);

该函数首先通过ngx_alloc从OS中申请一块内存,然后再把所申请内存置零。

(3)void *ngx_memalign(size_t alignment, size_t size, ngx_log_t *log);

该函数提供一种内存对齐的方式从OS中申请内存,该函数所返回内存块的起始地址都是从对齐大小alignment的整数倍开始。

Nginx关于内存池相关的文件为目录src\core\下的 ngx_palloc.h、ngx_palloc.c,这两个文件提供了内存池的实现。

1.2.2  关于nginx对申请内存块的释放问题

Nginx的应用场景比较特殊,它对内存分配的回收分为两种管理方式,其详细描述如下:

 

  • 一般从内存池中分配出去的内存不做回收管理(通过ngx_pmemalign、ngx_palloc、ngx_pnalloc、ngx_pcalloc),当使用完内存池之后,重置整个内存池即可,让所有内存池的存储节点的可分配区直接初始化为全部可用,这一步只需要调整每个存储节点last成员即可。
  • 对于大块内存释放时,直接将其释放给操作系统;
  • 重置内存池时将回收所有的内存池内存,自然也就回收了所有大块内存的管理节点(结构体为ngx_pool_large_t,这些管理节点就是在内存池中进行分配的),并将所有的大块内存全部释放给操作系统;
  •  如果分配需要做特殊回收处理的内存,则需通过接口ngx_pool_cleanup_add来完成申请,申请出去的每个内存都通过内存池第一个节点的cleanup成员来管理,所有分配出去的需特殊回收内存以链表方式管理起来;

 

1.2.3  nginx内存池的结构

Nginx的内存池采用链表结构,每个内存池对应3个链表:内存池链表、大块内存链表和需特殊回收的已分配内存链表;这些个链表的主要区别为:

 

  • 内存池链表中每个节点初始可使用的存储空间大小是一样的,并且在内存池创建时指定,大块内存管理链表中,每个分配出去的内存大小不一定一样;
  • 大块内存管理链表中,每块分配应用的内存都大于内存池链表中所管理的内存块大小;
  • 内存池链表的每个内存池节点中,存储的管理信息(结构体ngx_pool_t和待分配的内存空间连在一起,并且在待分配的内存空间之前),大块内存的管理结构体和该结构体所管理的大内存块不在一个连续内存空间中;
  • 大内存块的管理结构体ngx_pool_large_s所占用的内存是在内存池链表中分配的;
  • 每个已分配出去的需特殊回收的内存都由一个结构体ngx_pool_cleanup_s来描述,所有需特殊回收的内存被组织成一个链表,链表的表头存放在内存池的第一个存储节点结构体ngx_pool_t的cleanup成员中。
  • 需特殊回收的内存块和其管理结构体ngx_pool_cleanup_s所占用的内存都从内存池中分配。

 

这两个链表通过内存池链表中第一个节点的large成员连接起来,如下图2中对大内存块管理的描述。

nginx源码分析之内存池实现原理_第2张图片

图2 内存池结构体

1、  内存池结构体

内存池相关的结构体主要有:ngx_pool_cleanup_s、ngx_pool_large_t(ngx_pool_large_s)、ngx_pool_data_t和ngx_pool_t(即ngx_pool_s)、 ngx_pool_cleanup_file_t,如下所示:

(1)ngx_pool_cleanup_s

 

struct ngx_pool_cleanup_s {
   ngx_pool_cleanup_pt   handler;
   void                 *data;
   ngx_pool_cleanup_t   *next;
};

 

 

 

 

 

结构体ngx_pool_cleanup_s用于描述一个从内存池中分配出去的、需要特殊回收的内存块,成员data指向这个需要特殊回收的内存块,Handler在回收data所指向内存块时使用,next指向下一个需特殊回收内存块的管理结构体,这样所有需要特殊回收内存块的管理结构体都被组织成一个链表结构。

(2)ngx_pool_large_s或ngx_pool_large_t

 

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_s或ngx_pool_large_t表示大内存块结构体,在nginx中大内存块的管理也是采用链表方式,其中成员next指向下一个大内存块,alloc指向当前结构体所管理的大内存块。

(3)ngx_pool_data_t

 

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表示下一个内存池节点结构体,failed表示从该节点分配内存失败的次数,详细见上图2中对该数据结构的描述。

(4)ngx_pool_s或者ngx_pool_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;
};

 

 

 

 

 

结构体ngx_pool_s或者ngx_pool_t用于描述一个内存池节点,内存池节点的组织方式如下图所示(初始化时的形态,该节点中还未分为出任何内存空间,因此其未分配区域为刚申请时的可用大小):

nginx源码分析之内存池实现原理_第3张图片

图3

一个内存池节点是一个连续的内存块,在其前sizeof(ngx_pool_t)部分存储了该节点的描述与管理信息,即结构体ngx_pool_s,该结构体之后的部分就是可用实际使用的存储空间。成员max表示当前内存池的可供分配内存块大小,如图3中未分配的区域大小,即该节点的全部大小减去ngx_pool_t结构体占据的部分之后,所剩下的能被用户所使用的空间大小,其大小不大于“内存页大小-1”,如果大于则修改为“内存页大小-1”;成员current指向当前内存池链表中,具备分配能力的内存节点,见图2所示;large指向当前内存池的大内存块列表;log成员为日志输出所用,可忽略它而不影响对内存池的理解;成员cleanup指向分配出去的需要单独回收的内存链表。

(5)ngx_pool_cleanup_file_t

 

typedefstruct {
    ngx_fd_t              fd;
    u_char               *name;
    ngx_log_t            *log;
} ngx_pool_cleanup_file_t;

 

 

 

 

 

Nginx内存池对打开的文件进行了特殊的管理和操作,结构体ngx_pool_cleanup_file_t就表示对打开文件的特殊操作,其成员fd表示打开的文件句柄,name表示打开的文件名。

1.2.4  内存的内部管理

1)  内存池链表扩展

【可参考】宏:#definengx_align(d, a)     (((d) + (a - 1))& ~(a - 1))

用于将d向上取整为a的倍数,例如:ngx_align(7,3)即:

((7) +(3-1))&~(3-1)

=(7+2)&~2

=9&~2

= 1001&~ 0010转换为2进制

=1001& 1101

= 1001

= 9

在用户在内存池链表中申请内存时,如果内存池链表中的可用内存空间不够分配,则内存池自动调用函数相关函数进行内存扩展。

函数ngx_palloc_block主要用于扩展内存池容量,其声明为:

static void*ngx_palloc_block(ngx_pool_t *pool, size_t size)

对内存池pool的存储节点链表新扩充一个节点,该函数的扩充算法为:

(1)      计算当前内存池的内存池链表的节点大小(在内存池链表中,每个节点的大小都是一样的,而且节点的管理数据结构和可分配的内存空间是连在一起的)psize;

(2)      调用ngx_memalign从操作系统的内存中申请psize大小内存块,作为内存池链表的新增节点;

(3)      对新申请存储节点的管理结构体ngx_pool_t的各成员进行初始化,可参考图2中对该结构体的描述;

(4)      从新申请存储节点的可分配内存空间中分配出用户申请的内存;

(5)      将新申请的存储节点插入到内存池的“内存池链表”的队列尾部,如果当前节点的分配失败次数小于4,则调整内存池的当期可用节点的位置移动到下一个节点;

【注意】

(1)      当用户申请内存失败时,内存池内部会自动扩充新节点并在新增节点中为用户分配所申请的内存;

(2)      当用户申请内存失败(即内存池中新增了存储节点)时,内存池链表汇中,从current节点到链表的最后一个节点的failed值全部+1;

(3)      从current遍历到内存池队尾,遇到failed值大于4时,则current指针移动到下一个内存池的存储节点,知道将current指向一个failed值小于等于4的节点,如下图4所示,当然,如果从current到队尾的所有节点的failed值都小于等于4,则在新节点假如到内存池时current不向后移动,如下图5所示;

nginx源码分析之内存池实现原理_第4张图片

图4 新增节点时移动current指针到下一个节点

nginx源码分析之内存池实现原理_第5张图片

图 5 新增节点current不移动

2)  大块内存链表扩展

如果用户从内存池中申请大于内存池最大存储能力的内存时,nginx的内存池将直接从操作系统内存中申请用户所需的大块内存,并将新分配的内存放入到内存池的大块内存链表中,该过程主要通过下面的函数完成:

staticvoid * ngx_palloc_large(ngx_pool_t *pool, size_t size)

在该函数中,首先通过ngx_alloc从操作系统中申请一块用户申请大小(size参数指定)的内存块,这块内存将被直接返回给申请用户使用,如有必要则在内存池中为该大内存块申请一个小块内存用于存储管理用户所申请大内存块的数据结构ngx_pool_large_t;如下图,新申请大块内存的管理结构体ngx_pool_large_t是在内存池中存储,用户实际申请的大块内存则是直接从操作系统中申请的。

nginx源码分析之内存池实现原理_第6张图片
图6 内存池扩展大块内存

在上图中,需要说明的是大块内存的管理结构体ngx_pool_large_t是在当前内存池中所分配,而不一定是在内存池的第一个存储节点中分配,这里只是为了节省空间才把这两个管理结构体ngx_pool_large_t画在了同一个内存池存储节点中。

大块内存链表的管理方式有以下要点:

(1)      在内存池的大块内存链表中,通过结构体ngx_pool_large_t管理每一个大块内存,多个ngx_pool_large_t节点链接起来形成一个大块内存链表;

(2)      在大块内存管理中,如果用户释放了大块内存,则把该大块内存的管理结构体ngx_pool_large_t中的alloc变量设为null,并不会释放该大块内存的管理结构体ngx_pool_large_t,而是留着等待产生新大块内存时复用;

(3)      在申请一个新的大块内存时,首先从头开始遍历由ngx_pool_large_t组成的大块链表,找到某个节点的大块内存已经被释放,则把这个空隙管理节点利用起来,如果从头开始连续找3个节点都没有发现空闲的ngx_pool_large_t节点,就不再找了,而是从当前内存池中新申请一个ngx_pool_large_t,并用它管理为用户新申请的大块内存,然后将这个新申请的ngx_pool_large_t节点插入到大块内存链表的首部!

1.2.5  对外提供的接口函数

1.2.5.1     内存申请

下面四个函数用于从内存池中分配一个内存块,并且所回收的内存块无需特殊处理:

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区别是:从nginx的内存池申请内存池时,ngx_palloc会对新申请的内存地址进行对齐操作;ngx_pcalloc内部调用ngx_palloc从内存池中申请需要的内存,并将申请的内存空间全部置零,因此ngx_pcalloc实际上也是采用地址对齐方式申请内存,如下图所示:

nginx源码分析之内存池实现原理_第7张图片

图7 内存地址对齐

ngx_palloc、ngx_pcalloc与ngx_pnalloc这三个函数内部处理方式类似:

(1)            如果申请的内存大小size小于等于内存池默认的最大可用内存空间大小(由结构体ngx_pool_t的成员max保存),则从内存池中进行分配,否则通过操作系统直接分配,并通过“大块内存管理链表”进行新分配内存的管理;

(2)            如果“内存池链表”中没有足够的内存可供分配,则调用前面介绍的函数ngx_create_pool对内存池进行扩充。

(3)            如果“大块内存管理链表”中,则直接调用前面介绍的ngx_palloc_large函数进行大块内存分配。

函数ngx_pmemalign主要用于通过内存池从操作系统中直接申请一大块内存,但是申请的内存块进行了地址对齐,并且新申请的内存块交由内存池来管理,实质上就是将该内存块交由大块内存链表的节点结构体(ngx_pool_large_t)来管理。通过该函数申请的大内存块直接新分配一个ngx_pool_large_t结构体来管理,并将该结构体插入到大块内存管理链表的首部。

如果遇到对回收的内存块做特殊处理时,申请函数为:

ngx_pool_cleanup_t*ngx_pool_cleanup_add(ngx_pool_t *p, size_t size);

该函数的内部处理方式为:

(1)      从内存池中申请一个特殊回收内存块的管理结构体ngx_pool_cleanup_t;

(2)      从内存池中申请用户需要大小的内存块;

(3)      根据所申请的内存块初始化其管理结构体,主要是将成员data指向分配给用户“需特殊处理”的内存块,将该管理结构体插入到特殊内存管理结构体链表的首部;将Handler设置为null;

用户申请到这个回收时需特殊处理的内存块时,就需要自己设置特殊处理函数Handler,这样内存池在回收这块内存时就调用用户设置的回收函数进行处理。

1.2.5.2     内存池的操作

1)                  创建内存池

内存池创建通过函数ngx_create_pool完成,该函数声明如下:

ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log);

它完成创建一个内存池的动作,在该函数中指定了内存池节点的大小为Min(size - sizeof(ngx_pool_t), (ngx_pagesize - 1)),当然该大小不能大于nginx内部默认的一页大小(ngx_pagesize- 1),否则内存池的存储节点大小自动调整为一页大小,存储该值的变量为结构体ngx_pool_s的max成员。

由于内存池实质上是由一个个的存储节点组成的链表,但是其第一个节点比较特殊,它的max成员、current成员、large成员都将被经常使用,但是第一个之后的存储节点的这些成员基本上不会使用,在该函数内部,实际上是创建内存池的第一个存储节点,其内部主要完成以下业务:

(1)          根据从操作系统内存中申请参数size指定大小的内存块作为第一个存储节点;

(2)          该内存块的前sizeof(ngx_pool_t)空间主要用于保存管理此存储节点的结构体ngx_pool_t;

(3)          对结构体ngx_pool_t进行初始化,主要成员为d(ngx_pool_data_t类型),max、current等,其中:d.last为可供分配的内存地址,设置为未分配存储空间的起始位置;d.end指向当前未分配空间的末尾;d.next用于指向下一个节点,这里设置为null,failed用于标识分配内存失败的次数,这里设置为0;max设置为Min(size - sizeof(ngx_pool_t), (ngx_pagesize - 1));current设置为当前节点的起始位置,large用于指向当前内存池的大块内存分配链表,这里设置为null,如下图所示:

nginx源码分析之内存池实现原理_第8张图片

图8 第一个内存池存储节点的初始化

2)                  销毁内存池

连接销毁的接口函数声明为:

voidngx_destroy_pool(ngx_pool_t *pool);

3)                  重置内存池的函数接口为:

voidngx_reset_pool(ngx_pool_t *pool);

重置内存池主要完成两个功能:

l  对于大块内存链表,依次遍历并释放每一个链表节点所管理的大块内存,注意这里并没有释放这些大块内存的管理节点;

l  对于内存池的每个存储节点,则将全部可分配内存节点设置为未分配状态,只需要将last指针指向存储节点的ngx_pool_t后的第一个字节即可,注意这一步就释放了上一步中大块内存管理链表的每个节点。

4)                  释放大块内存

通过内存池释放大块内存的接口函数为:

ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);

在该函数中,将遍历内存池的大块内存列表,依次比较每个节点所管理的内存地址,如果为传入的地址p,则将大块内存直接释放到操作系统,注意该函数并未释放大内存块的管理结构体ngx_pool_large_t。

1.2.5.3     对文件的特殊操作

Nginx的内存池对描述打开文件的结构体内存进行了特殊管理,该动作主要通过结构体ngx_pool_cleanup_file_t来完成,这样在回收内存池时就会自动调用相应函数对打开的文件进行关闭,当然,这样的内存回收时需要特殊处理的(调用相关函数关闭待回收内存中所保存的打开文件,关闭文件也是特殊处理的动作!),因此,针对文件的所有操作也都针对前面介绍的“需特殊回收的已分配内存链表”;相关的操作函数主要有以下三个:

voidngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd);

voidngx_pool_cleanup_file(void *data);

void ngx_pool_delete_file(void*data);

函数ngx_pool_run_cleanup_file的功能为关闭连接池中保存的已打文件fd,其过程如下:从连接池的第一个存储节点中的cleanup成员中拿到“需特殊回收的已分配内存链表”的首地址,然后依次遍历每个已分配出去的“需特殊回收内存”,由于特殊回收内存块的管理结构体为ngx_pool_cleanup_t,我们可以通过该结构体的Handler成员变量来判断它的处理函数是不是ngx_pool_cleanup_file(注意这是个函数,在下面有解释其作用)如果是再取出ngx_pool_cleanup_t的data成员,此时data的类型一定是ngx_pool_cleanup_file_t(注意这是个struct),其fd成员就保存了一个打开文件的具备,如果该句柄与用户传入的fd一致,则将其关闭。

函数ngx_pool_cleanup_file的功能是关闭一个文件句柄,函数ngx_pool_delete_file的功能也是删除一个文件或者解除一个文件的链接。

 

 

你可能感兴趣的:(linux,c/c++,架构设计)