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

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

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

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

图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中对大内存块管理的描写叙述。

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

图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用于描写叙述一个内存池节点。内存池节点的组织方式例如以下图所看到的(初始化时的形态。该节点中还未分为出不论什么内存空间,因此其未分配区域为刚申请时的可用大小):

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

图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所看到的;

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

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

图 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是在内存池中存储,用户实际申请的大块内存则是直接从操作系统中申请的。


图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实际上也是採用地址对齐方式申请内存。例如以下图所看到的:

图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,例如以下图所看到的:

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

 

时间: 2024-10-10 22:54:26

nginx源代码分析之内存池实现原理的相关文章

STL源码分析之内存池

前言 上一节只分析了第二级配置器是由多个链表来存放相同内存大小, 当没有空间的时候就向内存池索取就行了, 却没有具体分析内存池是怎么保存空间的, 是不是内存池真的有用不完的内存, 本节我们就具体来分析一下 内存池 static data template的初始化 template <bool threads, int inst> char *__default_alloc_template<threads, inst>::start_free = 0; // 内存池的首地址 tem

nginx源码学习----内存池

最近在进行监控平台的设计,之前一直觉得C/C++中最棘手的部分是内存的管理上,远不止new/delete.malloc/free这么简单.随着代码量的递增,程序结构复杂度的提高.各种内存方面的问题悄然滋生.而且作为平台,后期的插件扩展在所难免.长时间运行的采集平台的特性更是提出了对稳定性的高要求.不是c#.java,没有虚拟机为你管理内存,一切都要靠自己.于是想看看nginx.python.lua这些C的经典之作在内存管理这块“要地”又是如何处理的. 先来看看nginx吧,因为网上都说nginx

cocos2d-x 源代码分析 : Ref (CCObject) 源代码分析 cocos2d-x内存管理策略

从源代码版本号3.x.转载请注明 cocos2d-x 总的文件夹的源代码分析: http://blog.csdn.net/u011225840/article/details/31743129 1.Ref,AutoreleasePool.PoolManager Ref中包括了一个叫referenceCount的引用计数,当一个Ref类的变量被new的时候,其referenceCount的引用计数被置为1. 当中有三个重要的操作,retain.release,autorelease,以下源代码分析

nginx源代码分析--从源代码看nginx框架总结

nginx源代码总结: 1)代码中没有特别绕特别别扭的编码实现.从变量的定义调用函数的实现封装,都非常恰当.比方从函数命名或者变量命名就能够看出来定义的大体意义,函数的基本功能,再好的架构实如今编码习惯差的人实现也会黯然失色,假设透彻理解代码的实现,领悟架构的设计初衷,认为每块代码就想经过耐心雕琢一样,不只实现了主要的功能给你,为其它人阅读也会提供非常好的支持.仔细恰当的命名规则就能够看出作者的功力. 2)更好更高的软件性能体如今架构设计上,好的架构会让软件更加稳定.easy维护.便于扩展.从核

redis 源代码分析(一) 内存管理

一,redis内存管理介绍 redis是一个基于内存的key-value的数据库,其内存管理是很重要的,为了屏蔽不同平台之间的差异,以及统计内存占用量等,redis对内存分配函数进行了一层封装,程序中统一使用zmalloc,zfree一系列函数,其相应的源代码在src/zmalloc.h和src/zmalloc.c两个文件里,源代码点这里. 二,redis内存管理源代码分析 redis封装是为了屏蔽底层平台的差异,同一时候方便自己实现相关的函数,我们能够通过src/zmalloc.h 文件里的相

MySQL系列:innodb源代码分析之内存管理

在innodb中实现了自己的内存池系统和内存堆分配系统,在innodb的内存管理系统中,大致分为三个部分:基础的内存块分配管理.内存伙伴分配器和内存堆分配器.innodb定义和实现内存池的主要目的是提供内存的使用率和效率,防止内存碎片和内存分配跟踪和调试.我们先来看看他们的关系和结构. 下面是它的关系结构图: 上图中的: ut_mem_block块是基础内存管理 Buddy allocator是内存伙伴分配器 mem_heap是内存堆分配器 1.基础内存管理 innodb中的内存分配和内存释放是

leveldb源码分析之内存池Arena

转自:http://luodw.cc/2015/10/15/leveldb-04/ 这篇博客主要讲解下leveldb内存池,内存池很多地方都有用到,像linux内核也有个内存池.内存池的存在主要就是减少malloc或者new调用的次数,较少内存分配所带来的系统开销. Arena类采用vector来存储每次分配内存的指针,每一次分配的内存,我们称为一个块block.block默认大小为4096kb.我们可以先看下Arena的模型: 我们来看看源码: 首先看下这个类的几个成员变量: 1 2 3 4

nginx源代码分析--读请求主体(1)

首先,读取请求体已进入HTTP要求11相,我们需要做的请求正文部分处理一些模块,所以这个模块需要注册功能在这个阶段,在阅读功能要求的身体ngx_http_read_client_request_body()是存在的.仅仅只是不同的模块可能对请求体做不同的处理.读取请全体的函数是在某个模块的conent_handler函数中包括的.比方比方proxy模块,fastcgi模块,uwsgi模块等这些模块对请求体感兴趣,那么读取请求体的函数在这些模块的content_handler中注冊. 上节说到ng

nginx源代码分析--event事件驱动初始化

1.在nginx.c中设置每一个核心模块的index ngx_max_module = 0; for (i = 0; ngx_modules[i]; i++) { ngx_modules[i]->index = ngx_max_module++; } 2.进入函数ngx_init_cycle,调用每一个核心模块的create_conf for (i = 0; ngx_modules[i]; i++) { if (ngx_modules[i]->type != NGX_CORE_MODULE)