目录:linux
1.Nginx内存管理介绍nginx
2.Nginx内存池的逻辑结构程序员
3.Nginx内存池的基本数据结构web
4.内存池基本操做介绍服务器
5.内存池管理源码详解数据结构
6.内存池使用源码详解tcp
7.小结ide
在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版本。
前面提到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指针后移)。值得注意的是:咱们的第二、3块小块内存池中只包含了ngx_pool_t结构体和数据区,并不包含max、current、...、log。这是因为后续第1块小内存池已经包含了这些信息,后续的小块内存池没必要在浪费空间存储这些信息。咱们在第6小节:内存池的使用中将会有所介绍。图中共分配了3个大块内存,其中第二块的alloc为NULL(提早调用了ngx_pfree())。图中还挂在了两个资源清理方法。提醒一下的是:若是在这里没有弄清楚,没有关系,看完了后面的部分再回过头来理解这个示意图就可以很好的理解了。这里只是先给出一个归纳性的Nginx内存池逻辑结构的介绍,先给你们留下一个大概的印象。
本部分主要介绍内存池中重要的数据结构,主要是ngx_pool_t,而后介绍ngx_pool_t中三个重要数据结构:ngx_pool_data_t,ngx_pool_large_t和ngx_pool_cleanup_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中:
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:内存池中执行时输出日志的地方。
咱们能够在Nginx的源码的src/core/目录下的nax_palloc.h头文件中看到:
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成员就会移向下一个小块内存,在之后分配内存时,将从下一个小块内存开始遍历。
咱们能够在Nginx的源码的src/core/nax_palloc.h头文件中看到:
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。
咱们能够在Nginx的源码的src/core/nax_palloc.h头文件中看到:
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,须要设置的清理函数。
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链表中。
这一部分主要简单讲解与内存池管理有关的基本操做(共15个)。主要包括四个部分:(a).内存池操做 (b).基于内存池的分配、释放操做 (3).随着内存池释放同步释放资源的操做 (4).与内存池无关的分配、释放操做。在第5和第6节中,咱们会对部分经常使用内存池的操做进行代码上的详细介绍。
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
重置内存池,即将在内存池中原有的内存释放后继续使用。后面咱们会看到,这个方法是把大块的内存释放给操做系统,而小块的内存则在不释放的状况下复用。
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移出链表并删除。效率不高。
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成员。
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函数,释放内存到操做系统。
本部分的源码能够在src/core/ngx_palloc.h、src/core/ngx_palloc.c、src/os/unix/ngx_alloc.h和src/os/unix/ngx_alloc.c中找到。内存池的管理主要包括内存池的建立、销毁以及重置操做。咱们经过对源码的分析来研究和学习Nginx的内存管理技术。
建立内存池的操做主要由ngx_create_pool()函数完成,代码以下:
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结构体中的成员进行初始化。在进行初始化以前,让咱们先讨论如下什么是小块内存?
/*
* 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中看到其源码,前面部分是声明,后面是定义。以下所示:
/*
* 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
#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字节的内存池做为一个分配模块:
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字节)以下所示:
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内存池建立的主要原理和具体实现。
销毁内存池的工做主要由ngx_destroy_pool()函数完成。代码以下:
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上的单链表了,由于咱们清理了其单链表的第一个节点。
重置内存池,就是将内存池分配到初始分配的状态。这是由ngx_reset_pool()函数完成的。代码以下:
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指针重置到刚分配时的位置。小块内存中存储的数据并无被释放,其在之后的内存池使用的过程当中将会被覆盖更新。这能够减小内存分配的次数,提高内存重用率。但会浪费一些内存空间。
内存池建立好以后,如何进行使用呢?这些内存使用完了以后是如何进行回收利用的呢?下面的部分将会详细的介绍内存池的使用。
在Nginx中,基于内存池的申请方法主要有ngx_palloc、ngx_pnalloc、ngx_pcalloc和ngx_pmemalign共4种方法。而不基于内存池,直接从操做系统中申请内存的主要有ngx_alloc和ngx_calloc共两种方法。在这一小节中,咱们只讲述从内存池中申请内存相关的4中方法。而其余的部分将会在后面的小节进行讲解。
基于内存池的4中内存申请方法的区别在第4章:内存池API介绍中已经详细阐述了。此处再也不赘述。
下面给出源码:
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()函数的源码,以下所示:
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中,以下所示:
#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()函数。其源码以下所示:
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()的源码:
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,若是插在尾部,那么遍历链表前面的三个节点就没有意义了,由于每次均可能会遍历不到后面的空闲节点,而致使每次都须要从新创建新节点。而且插入头部,从头部开始遍历也会使得效率比较高。由于这样遍历到空闲的大块内存节点的几率会高不少。
先给出其源码:
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);
}
#endif
return ngx_palloc_large(pool, size);
}
咱们能够看到,ngx_pnalloc()和ngx_palloc()很是类似,惟一的区别就是ngx_pnalloc()中调用的是ngx_palloc_small(pool, size, 0),而ngx_palloc()中调用的是ngx_palloc_small(pool, size, 1)。那么实际上的含义有什么区别呢?ngx_pnalloc()分配内存时不考虑内存数据对齐,而ngx_palloc()分配内存时考虑内存数据对齐。
咱们先给出其源码,以下所示:
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。
咱们给出其源码,以下所示:
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()函数用于提早释放大块内存。
此处咱们将介绍基于内存池的内存释放操做函数ngx_pfree(),与内存池无关的内存释放操做ngx_free()将在后面被讲解。
在Nginx中,小块内存并不存在提早释放这么一说,由于其占用的内存较少,不太须要被提早释放。可是对于很是大的内存,若是它的生命周期远远短于所属的内存池,那么在内存池销毁以前提早释放它就变得有意义了。下面先给出其源码:
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。
在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种。下面咱们将分别讲解这些操做。
这个方法的目的是为了添加一个须要在内存池释放时同步释放的资源。咱们依照惯例仍是先给出其源码,而后对源码进行分析和学习。其源码以下所示:
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成员指定相应的函数。
在内存池释放前,若是须要提早关闭文件,则调用该方法。下面给出其源码,以下所示:
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头文件中),以下所示:
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),进行文件的关闭操做。
该方法以关闭文件的方式来释放资源,能够被设置为ngx_pool_cleanup_t的handler成员(函数指针)。咱们给出其源码实现,以下所示:
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()函数实现的。
以删除文件来释放资源的方法,能够设置到ngx_pool_cleanup_t的handler成员。咱们先给出其源码,以下所示:
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中看到其具体实现,以下所示:
#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中操做方法。下面咱们将继续讲解它们的具体实现。
ngx_alloc()函数直接从操做系统中申请内存,其实现是对malloc()函数的一个简单封装。咱们能够在src/os/unix/ngx_alloc.c中找到其源码。以下所示:
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()函数,并作了一些日志和调试方面的处理。
ngx_calloc()和ngx_alloc()很是类似,惟一的区别是在调用malloc()函数申请完内存以后,会调用ngx_memzero()函数将内存所有初始化为0。ngx_memzero()就是memset()函数。
咱们能够在src/os/unix/ngx_alloc.h中看到其源码,以下所示:
#define ngx_free free
能够看到Nginx程序释放内存的函数很是简单,和销毁内存池中用的是同一个(free)。这里须要再次说明的是:对于在不一样场合下从内存池中申请的内存空间的释放时机是不同的。通常只有大数据块才直接调用ngx_free()函数进行释放,其余数据空间的释放都是在内存池销毁的时机完成的,不须要提早完成。
至此,Nginx与内存相关的操做的源码实现已基本讲完了。你们若是想进一步研究和学习Nginx内存管理机制,能够从官方下载Nginx源码,从源码中去发现Nginx下降系统内存开销的方法。
全部的讲解都讲述完了,咱们来进行总结一下。在第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。