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

时间:2022-04-28 12:06:21

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

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

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

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

图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源代码分析之内存池实现原理

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaG91aml4aW4=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />

图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源代码分析之内存池实现原理

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaG91aml4aW4=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />

图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源代码分析之内存池实现原理

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaG91aml4aW4=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />

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

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

图 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 内存池扩展大块内存

在上图中。须要说明的是大块内存的管理结构体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 内存地址对齐

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 第一个内存池存储节点的初始化

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的功能也是删除一个文件或者解除一个文件的链接。