http://aiyooyoo.com/index.php/archives/491/
http://www.oschina.net/question/234345_42068
http://blog.chinaunix.net/uid-21878516-id-200376.html
http://blog.csdn.net/xt_xiaotian/article/details/6640565
http://www.colaghost.com/web-server/232
http://www.cnblogs.com/sld666666/archive/2010/06/27/1766255.html
http://www.tbdata.org/archives/1390
Nginx的内存池实现得很精巧,代码也很简洁。总的来说,所有的内存池基本都一个宗旨:申请大块内存,避免“细水长流”。
一、创建一个内存池
nginx内存池主要有下面两个结构来维护,他们分别维护了内存池的头部和数据部。此处数据部就是供用户分配小块内存的地方。
//该结构用来维护内存池的数据块,供用户分配之用。
typedef struct {
u_char *last; //当前内存分配结束位置,即下一段可分配内存的起始位置
u_char *end; //内存池结束位置
ngx_pool_t *next; //链接到下一个内存池
ngx_uint_t failed; //统计该内存池不能满足分配请求的次数
} ngx_pool_data_t;
//该结构维护整个内存池的头部信息。
struct ngx_pool_s {
ngx_pool_data_t d; //数据块
size_t max; //数据块的大小,即小块内存的最大值
ngx_pool_t *current; //保存当前内存池
ngx_chain_t *chain; //可以挂一个chain结构
ngx_pool_large_t *large; //分配大块内存用,即超过max的内存请求
ngx_pool_cleanup_t *cleanup; //挂载一些内存池释放的时候,同时释放的资源。
ngx_log_t *log;
};
有了上面的两个结构,就可以创建一个内存池了,nginx用来创建一个内存池的接口是:ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log)(位于src/core/ngx_palloc.c中);调用这个函数就可以创建一个大小为size的内存池了。这里,我用内存池的结构图来展示,就不做具体的代码分析了。
ngx_create_pool接口函数就是分配上图这样的一大块内存,然后初始化好各个头部字段(上图中的彩色部分)。红色表示的四个字段就是来自于上述的第一个结构,维护数据部分,由图可知:last是用户从内存池分配新内存的开始位置,end是这块内存池的结束位置,所有分配的内存都不能超过end。蓝色表示的max字段的值等于整个数据部分的长度,用户请求的内存大于max时,就认为用户请求的是一个大内存,此时需要在紫色表示的large字段下面单独分配;用户请求的内存不大于max的话,就是小内存申请,直接在数据部分分配,此时将会移动last指针。
二、分配小块内存(size <= max)
上面创建好了一个可用的内存池了,也提到了小块内存的分配问题。nginx提供给用户使用的内存分配接口有:
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都是从内存池里分配size大小内存,至于分得的是小块内存还是大块内存,将取决于size的大小;他们的不同之处在于,palloc取得的内存是对齐的,pnalloc则否。ngx_pcalloc是直接调用palloc分配好内存,然后进行一次0初始化操作。ngx_pmemalign将在分配size大小的内存并按alignment对齐,然后挂到large字段下,当做大块内存处理。下面用图形展示一下分配小块内存的模型:
上图这个内存池模型是由上3个小内存池构成的,由于第一个内存池上剩余的内存不够分配了,于是就创建了第二个新的内存池,第三个内存池是由于前面两个内存池的剩余部分都不够分配,所以创建了第三个内存池来满足用户的需求。由图可见:所有的小内存池是由一个单向链表维护在一起的。这里还有两个字段需要关注,failed和current字段。failed表示的是当前这个内存池的剩余可用内存不能满足用户分配请求的次数,即是说:一个分配请求到来后,在这个内存池上分配不到想要的内存,那么就failed就会增加1;这个分配请求将会递交给下一个内存池去处理,如果下一个内存池也不能满足,那么它的failed也会加1,然后将请求继续往下传递,直到满足请求为止(如果没有现成的内存池来满足,会再创建一个新的内存池)。current字段会随着failed的增加而发生改变,如果current指向的内存池的failed达到了4的话,current就指向下一个内存池了。猜测:4这个值应该是作者的经验值,或者是一个统计值。
三、大块内存的分配(size > max)
大块内存的分配请求不会直接在内存池上分配内存来满足,而是直接向操作系统申请这么一块内存(就像直接使用malloc分配内存一样),然后将这块内存挂到内存池头部的large字段下。内存池的作用在于解决小块内存池的频繁申请问题,对于这种大块内存,是可以忍受直接申请的。同样,用图形展示大块内存申请模型:
注意每块大内存都对应有一个头部结构(next&alloc),这个头部结构是用来将所有大内存串成一个链表用的。这个头部结构不是直接向操作系统申请的,而是当做小块内存(头部结构没几个字节)直接在内存池里申请的。这样的大块内存在使用完后,可能需要第一时间释放,节省内存空间,因此nginx提供了接口函数:ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);此函数专门用来释放某个内存池上的某个大块内存,p就是大内存的地址。ngx_pfree只会释放大内存,不会释放其对应的头部结构,毕竟头部结构是当做小内存在内存池里申请的;遗留下来的头部结构会作下一次申请大内存之用。
四、cleanup资源
可以看到所有挂载在内存池上的资源将形成一个循环链表,一路走来,发现链表这种看似简单的数据结构却被频繁使用。由图可知,每个需要清理的资源都对应有一个头部结构,这个结构中有一个关键的字段handler,handler是一个函数指针,在挂载一个资源到内存池上的时候,同时也会注册一个清理资源的函数到这个handler上。即是说,内存池在清理cleanup的时候,就是调用这个handler来清理对应的资源。比如:我们可以将一个开打的文件描述符作为资源挂载到内存池上,同时提供一个关闭文件描述的函数注册到handler上,那么内存池在释放的时候,就会调用我们提供的关闭文件函数来处理文件描述符资源了。
五、内存的释放
nginx只提供给了用户申请内存的接口,却没有释放内存的接口,那么nginx是如何完成内存释放的呢?总不能一直申请,用不释放啊。针对这个问题,nginx利用了web server应用的特殊场景来完成;一个web server总是不停的接受connection和request,所以nginx就将内存池分了不同的等级,有进程级的内存池、connection级的内存池、request级的内存池。也就是说,创建好一个worker进程的时候,同时为这个worker进程创建一个内存池,待有新的连接到来后,就在worker进程的内存池上为该连接创建起一个内存池;连接上到来一个request后,又在连接的内存池上为request创建起一个内存池。这样,在request被处理完后,就会释放request的整个内存池,连接断开后,就会释放连接的内存池。因而,就保证了内存有分配也有释放。
总结:通过内存的分配和释放可以看出,nginx只是将小块内存的申请聚集到一起申请,然后一起释放。避免了频繁申请小内存,降低内存碎片的产生等问题
基本上所有的高性能服务器都会涉及到内存池这一块,nginx也不例外。nginx的内存池实现相对比较简洁精巧,看起来比较容易理解。
以下是nginx内存池的示意图,这是根据自己的理解画的,有什么理解错误的地方欢迎大家拍砖。
nginx的内存池主要涉及它的创建、小内存分配、大内存分配和资源清理。
一、内存池的创建
这里主要涉及到两个数据结构,分别是ngx_pool_s和ngx_pool_data_t。ngx_pool_s维护了内存池的的头部信息,而ngx_pool_data_t维护了内存池的数据部分的信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| //该结构用来维护内存池的数据部分,供用户分配内存使用
typedef struct {
u_char *last;//当前内存分配结束的位置
u_char *end;//内存池结束位置
ngx_pool_t *next;//指向下一个内存池
ngx_uint_t failed;//统计内存池不能满足分配请求的次数
} ngx_pool_data_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;//分配大内存使用,即请求分配的内存大小超过max
ngx_pool_cleanup_t *cleanup;//用来指向内存池释放时同时释放的资源
ngx_log_t *log;
}; |
有了上面两个结构体,实际上已经可以分配一个内存池了,创建一个内存池的入口函数是ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log),切记size需要大于sizeof(ngx_pool_s),大于部分为此内存池供分配的内存。返回的ngx_pool_t指针指向整个内存池链表的头结点,头结点会维护整个内存池链接的头部信息,下次如果往内存池链表里面添加ngx_pool_s结点时只会维护顶部的ngx_pool_data_t部分的信息,即只维护此内存池结点的数据部分的信息。
关于整个内存池链接的结构可以参考下上图,pool指向内存池的头部结点,红色部分为可分配用户使用的内存块。由上图可以看出来,pool->d.last指向从内存池分配新内存的起始地址,pool->d.end指向这块内存池的结束位置,所有分配内存的地址都不能超过pool->d.end。
当进行内存分配操作时,首先判断要分配的size大小是否超过max,是的话就直接跟系统分配一块大内存,跟malloc一样,挂在pool->large下;否则就直接在pool->current指向的内存池里分配,并相应地移动pool->current->d.last的位置。
二、分配小内存(size<max)
小内存分配涉及到的接口函数主要有:
1
2
3
4
| 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都是从内存池中分配内存,区别就是ngx_palloc取得的内存是对齐的,而ngx_pnalloc则没有;ngx_pcalloc是调用ngx_palloc分配内存的,并将分配的内存都置0.ngx_pmemalign主要用来对指针进行对齐,但是nginx似乎没有使用这个,而是用到了另外定义的一个宏,下面会谈到。
这里由于篇幅问题只进行ngx_palloc函数的分析,其它两个大同小异。
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
| void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
u_char *m;
ngx_pool_t *p;
if (size <= pool->max) {
p = pool->current;
do {
m = ngx_align_ptr(p->d.last, 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);
}
return ngx_palloc_large(pool, size);
} |
这里分配的是小内存,进行的是size<=pool->max部分的逻辑。我们可以看到nginx会从pool->current指向的内存池结点开始遍历,首先用ngx_align_ptr这个宏对last指针进行内存对齐,再判断当前内存池结点的数据部分剩余的内存是否够分配,如果够的话则移动last指针size大小的值,然后返回分配的内存的起始地址;否则移动到下一个内存池结点再进行同样的判断。
这里有一种可能性,即内存池链表里可能所有节点的数据部分的剩余内存都不够分配了,这时候就需要在内存池链表里插入一个新的内存池(ngx_pool_t)结点,这是由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
35
36
37
38
| static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
u_char *m;
size_t psize;
ngx_pool_t *p, *new, *current;
psize = (size_t) (pool->d.end - (u_char *) pool);
m = ngx_alloc(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;
current = pool->current;
for (p = current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
current = p->d.next;
}
}
p->d.next = new;
pool->current = current ? current : new;
return m;
} |
ngx_palloc_block函数会往内存池链表里插入一个新的内存池结点,并返回请求分配的内存的起始地址。
注意在这时nginx会对p->current结点后的所有内存池结点的数据部分维护的failed的值加1,并当failed的值超过4时,则将p->current的值指向下一个内存池结点,这也就意味着内存池链表里每一个结点的可分配内存不一定会被分配完。
这里再提下ngx_align_ptr这个宏,NGX_ALIGNMENT取值32或者64(sizeof(unsigned long)),这个宏会在这两个平台里对指针进行对齐,下面看看这个宏的定义。
1
2
| #define ngx_align_ptr(p, a) \
(u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1)) |
uintptr_t可以将指针类型转换为整数,由于这段代码跨平台,所以用uintptr_t更安全,因为有可能为4或者8.
从二进制来看,为32位平台时,进行指针对齐需要保证指针的值的最低两位为0,即为4的倍数。当最低两位有任意一位不为零时,则需要加上((uintptr_t) a – 1)) (这时候a取值为4)产生进位,再通过与~((uintptr_t) a – 1)(最低四位是为1100)做且运算,抹平指针最低两位,这时候对齐完成。
这段代码并不复杂,但是涉及的方面比较多,效率也很高,比较有意思。
三、分配大块内存
大块内存的分配相对来说逻辑上比较简单,这时候并不是从内存池里分配,而是直接跟系统申请(相当于直接用malloc分配),再将这块内存挂到内存池头部的large字段里。由于内存池主要是用来解决小内存的频繁分配问题,这里大内存直接向系统申请是可以忍受的。大块内存的组织结构可以参考上图。
注意每块大内存都有一个数据结构进行维护的:
1
2
3
4
| struct ngx_pool_large_s {
ngx_pool_large_t *next;//指向下一块大内存
void *alloc;//指向分配的大块内存
}; |
这个数据结构是在内存池里进行分配的,因为这部分信息占用的字节比较少。由于大块内存很多时候可能需要及时被释放,所以nginx提供了ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p)函数进行释放。其中p就是指向大块内存的地址。ngx_pfree只会释放分配的大块内存,但数据结构部分并不会被释放,会留下来供下次分配大内存使用。
四、资源回收
可以参考上图的cleanup部分,会发现所有需要被释放的资源会形成一个循环链表。每个需要释放的资源都会有一个头部结构:
1
2
3
4
5
| struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler;//释放资源的函数指针
void *data;//待释放的资源
ngx_pool_cleanup_t *next;//指向下一个待释放的资源头部
}; |
这里可以看到,当挂载一个待释放的资源时,需要注册一个释放函数。这就意味着这里不单单可以进行内存的释放,相应的文件标识符等也可以在这里进行释放,只要注册相应的释放资源函数调用即可。
五、内存回收
一路看来,貌似nginx并没有提供释放内存的接口(除了大内存块),难道nginx的内存都是只申请不释放的,这样不行啊,内存再多也只有吃完的时候啊。但显然我的担心是多余了,nginx并不会这样,它针对特定的场景,如每一个request都会建一个内存池,当request被完成时内存池分配的内存也会一次性被回收,这样就保证了内存有分配也有释放的时候了。当然具体还有很多的场景,由于理解深度还不够这里暂不涉及。
Nginx 内存池(pool)分析
Nginx 内存池管理的源码在src/core/ngx_palloc.h、src/core/ngx_palloc.c 两个文件中。
先将我整理的注释等内容贴上,方便下面分析:
ngx_create_pool:创建pool
ngx_destory_pool:销毁 pool
ngx_reset_pool:重置pool中的部分数据
ngx_palloc/ngx_pnalloc:从pool中分配一块内存
ngx_pool_cleanup_add:为pool添加cleanup数据
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler; // 当前 cleanup 数据的回调函数
void *data; // 内存的真正地址
ngx_pool_cleanup_t *next; // 指向下一块 cleanup 内存的指针
};
struct ngx_pool_large_s {
ngx_pool_large_t *next; // 指向下一块 large 内存的指针
void *alloc; // 内存的真正地址
};
typedef struct {
u_char *last; // 当前 pool 中用完的数据的结尾指针,即可用数据的开始指针
u_char *end; // 当前 pool 数据库的结尾指针
ngx_pool_t *next; // 指向下一个 pool 的指针
ngx_uint_t failed; // 当前 pool 内存不足以分配的次数
} ngx_pool_data_t;
struct ngx_pool_s {
ngx_pool_data_t d; // 包含 pool 的数据区指针的结构体
size_t max; // 当前 pool 最大可分配的内存大小(Bytes)
ngx_pool_t *current; // pool 当前正在使用的pool的指针
ngx_chain_t *chain; // pool 当前可用的 ngx_chain_t 数据,注意:由 ngx_free_chain 赋值
ngx_pool_large_t *large; // pool 中指向大数据快的指针(大数据快是指 size > max 的数据块)
ngx_pool_cleanup_t *cleanup; // pool 中指向 ngx_pool_cleanup_t 数据块的指针
ngx_log_t *log; // pool 中指向 ngx_log_t 的指针,用于写日志的
};
使用 ngx_create_pool、ngx_destory_pool、ngx_reset_pool三个函数来创建、销毁、重置 pool。使用ngx_palloc、ngx_pnalloc、ngx_pool_cleanup_add来使用pool。使用结构体 ngx_pool_t 管理整个 pool。下面将详细分析其工作方式。
我们以 nginx 接受并处理 http 请求的方式,来分析pool的工作流程。
在 ngx_http_request.c 中,ngx_http_init_request 函数便是 http 请求处理的开始,在其中调用了ngx_create_pool 来创建对应于 http 请求的 pool。同一个c文件中,ngx_http_free_request 函数便是 http 请求处理的结束,在其中调用了 ngx_destory_pool。
我们一步步来看具体工作流程。首先,调用ngx_create_pool来创建一个pool,源码如下:
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
ngx_pool_t *p;
// 分配一块 size 大小的内存
p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
if (p == NULL) {
return NULL;
}
// 对pool中的数据项赋初始值
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; // pool 中最大可用大小
// 继续赋初始值
p->current = p;
p->chain = NULL;
p->large = NULL;
p->cleanup = NULL;
p->log = log;
return p;
}
创建完pool后,pool示例如为:
最左边的便是创建的pool内存池,其中首sizeof(ngx_pool_t)便是pool的header信息,header信息中的各个字段用于管理整个pool。由于此时刚创建,pool中除了header之外,没有任何数据。
注意:current 永远指向此pool的开始地址。current的意思是当前的pool地址,而非pool中的地址。
从代码的角度来说,pool->d.last ~ pool->d.end 中的内存区便是可用数据区。
接下来,我们使用ngx_palloc从内存池中获取一块内存,源码如下:
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
u_char *m;
ngx_pool_t *p;
// 判断 size 是否大于 pool 最大可使用内存大小
if (size <= pool->max) {
p = pool->current;
do {
// 将 m 对其到内存对齐地址
m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);
// 判断 pool 中剩余内存是否够用
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);
}
return ngx_palloc_large(pool, size);
此处需要分3步进行讨论。当需要的内存大于pool最大可分配内存大小时;否则,当需要的内存大于pool目前可用内存大小时;否则,当需要的内存可以在此pool中分配时。
我们先从最简单的情况开始,即,当需要的内存可以在此pool中分配时。此时从代码流程可以看到,判断内存够用后,直接移动 p->d.last 指针,令其向下偏移到指定的值即可,使用此种方式可以避免新分配内存的系统调用,效率大大提高。此时的 pool 示例图为:
我们继续讨论第二种情况,当需要的内存大于pool目前可用内存大小时。从代码流程可以看到,此时首先寻找pool数据区中的下一个节点,看是否有够用的内存,如不够,则调用ngx_palloc_block 重新分配,我们将问题简单化,由于刚创建pool,pool->d.next指针为NULL,所以肯定会重新分配一块内存。源码如下:
static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
u_char *m;
size_t psize;
ngx_pool_t *p, *new, *current;
// 先前的整个 pool 的大小
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;
current = pool->current;
// 判断在当前 pool 分配内存的失败次数,即:不能复用当前 pool 的次数,
// 如果大于 4 次,这放弃在此 pool 上再次尝试分配内存,以提高效率
for (p = current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
current = p->d.next;
}
}
// 让旧指针数据区的 next 指向新分配的 pool
p->d.next = new;
// 更新 current 指针
pool->current = current ? current : new;
return m;
通过上面可以看到,nginx 重新分配了一个新pool,新pool大小跟之前的大小一样,然后对 pool 赋初始值,最终将新pool串到老pool的后面。注意,此处新pool的current指针目前没有起用,为NULL。另外,在此处会判断一个pool尝试分配内存失败的次数,如果失败次数大于4(不等于4),则更新current指针,放弃对老pool的内存进行再使用。此时的pool示例图为:
我们讨论最后一种情况,当需要的内存大于pool最大可分配内存大小时,此时首先判断size已经大于pool->max的大小了,所以直接调用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;
// 重新申请一块大小为 size 的新内存
// 注意:此处不使用 ngx_memalign 的原因是,新分配的内存较大,对其也没太大必要
// 而且后面提供了 ngx_pmemalign 函数,专门用户分配对齐了的内存
p = ngx_alloc(size, pool->log);
if (p == NULL) {
return NULL;
}
n = 0;
// 查找可复用的 large 指针
for (large = pool->large; large; large = large->next) {
// 判断当前 large 指针是否指向真正的内存,否则直接拿来用
// ngx_free 可使此指针为 NULL
if (large->alloc == NULL) {
large->alloc = p;
return p;
}
// 如果当前 large 后串的 large 内存块数目大于 3 (不等于3),
// 则直接去下一步分配新内存,不再查找了
if (n++ > 3) {
break;
}
}
// 为 ngx_pool_large_t 分配一块内存
large = ngx_palloc(pool, sizeof(ngx_pool_large_t));
if (large == NULL) {
ngx_free(p);
return NULL;
}
// 将新分配的 large 串到链表后面
large->alloc = p;
large->next = pool->large;
pool->large = large;
return p;
由如上代码可知,函数首先申请一块大小为size的内存,然后判断当前 large 链表中是否有存在复用的可能性,有的话,当然直接赋值返回;如果没有,则新分配一块大小为sizeof(ngx_pool_large_t)的内存,串到large链表的后面。我们继续上面的例子,由于之前没有分配过large内存,所以此时直接将新内存块串起来。此时pool示例图为:
至此,在pool中分配普通内存的情况我们就讨论完了。如果有新内存需要分配,无非也就是在pool中直接移动last指针,next、large next指针后面串接新的内存块而已。
我们接下来看看函数ngx_pool_cleanup_add,在pool中分配带有handler的内存,先上源码:
ngx_pool_cleanup_t *
ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
{
ngx_pool_cleanup_t *c;
// 首先申请 sizeof(ngx_pool_cleanup_t) 大小的内存作为header信息
c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
if (c == NULL) {
return NULL;
}
if (size) {
// cleanup 中有内存大小的话,分配 size 大小的内存空间
c->data = ngx_palloc(p, size);
if (c->data == NULL) {
return NULL;
}
} else {
c->data = NULL;
}
// 对 cleanup 数据结构其他项进行赋值
c->handler = NULL;
c->next = p->cleanup;
// 将 cleanup 数据串进去
p->cleanup = c;
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);
return c;
我们看到源码首先分配 header 大小的头信息内存,然后判断是否要真正分配内存,如果要的话,分配内存,最后将新的数据块串起来。我们继续上面的示例图,将分配一个 cleanup 之后的示例图画出。此时 pool 示例图为:
在此顺带提一点,pool 中的 chain 指向一个 ngx_chain_t 数据,其值是由宏 ngx_free_chain 进行赋予的,指向之前用完了的,可以释放的ngx_chain_t数据。由函数ngx_alloc_chain_link进行使用。
接下来我们通过上面的图讨论一下ngx_reset_pool函数,源码:
void
ngx_reset_pool(ngx_pool_t *pool)
{
ngx_pool_t *p;
ngx_pool_large_t *l;
// 释放 large 数据块的内存
for (l = pool->large; l; l = l->next) {
if (l->alloc) {
ngx_free(l->alloc);
}
}
// 将 pool 直接下属 large 设为 NULL 即可,无需再上面的 for 循环中每次都进行设置
pool->large = NULL;
// 重置指针位置,让 pool 中的内存可用
for (p = pool; p; p = p->d.next) {
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
}
可以看到,代码相当简单,将large、pool 中原有内存还原到初始状态而已。
最后我们讨论一下ngx_destory_pool函数,销毁创建的pool,源码:
void
ngx_destroy_pool(ngx_pool_t *pool)
{
ngx_pool_t *p, *n;
ngx_pool_large_t *l;
ngx_pool_cleanup_t *c;
// 调用 cleanup 中的 handler 函数,清理特定资源
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);
}
}
// 释放 large 数据块的内存
for (l = pool->large; l; l = l->next) {
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
if (l->alloc) {
ngx_free(l->alloc);
}
}
// 释放整个 pool
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
ngx_free(p);
if (n == NULL) {
break;
}
}
代码也相当简单,首先调用 cleanup 中的handler函数来清理特定资源,然后释放large内存,最终释放整个pool。
最终整个pool就销毁的无影无踪了。细心的朋友可能会发现,销毁时似乎忘了释放 cleanup 内存块分配的内存了,真的是这样吗?呃,这个还是留给大家自己想吧。
看了nginx 代码,也在网上看了些源码分析,结合源代码和大家的分析,自己重新整理一下,下面是可以直接编译通过测试的例子。--------感谢rainx 的这个图,画得很清晰了。也很想知道这个图是用什么画的。
我几乎很少网上写博客,现在有时间也打算写点,先从nginx 源码开始吧,打算把它的一些好的思想摘出来,测下然后整理出来。
<div>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
#include <stdint.h>
#define ngx_free free
#define ngx_align(d, a) (((d) + (a - 1)) & ~(a - 1))
#define ngx_align_ptr(p, a) \
(u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
#define NGX_ALIGNMENT sizeof(unsigned long) /* platform word */
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
#define NGX_DEFAULT_POOL_SIZE (16 * 1024)
#define NGX_POOL_ALIGNMENT 16
#define NGX_MIN_POOL_SIZE \
ngx_align((sizeof(ngx_pool_t) + 2 * sizeof(ngx_pool_large_t)), \
NGX_POOL_ALIGNMENT)
typedef intptr_t ngx_int_t;
typedef uintptr_t ngx_uint_t;
typedef intptr_t ngx_flag_t;
typedef void (*ngx_pool_cleanup_pt)(void *data);
typedef struct ngx_pool_s ngx_pool_t;
typedef struct ngx_pool_large_s ngx_pool_large_t;
typedef struct ngx_chain_s ngx_chain_t;
typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t;
typedef int* ngx_log_t ; //???
struct ngx_chain_s {
/*ngx_buf_t *buf;*/
ngx_chain_t *next;
};
typedef struct {
u_char *last;
u_char *end;
//指向下一块内存池
ngx_pool_t *next;
///失败标记
ngx_uint_t failed;
} ngx_pool_data_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;
};
struct ngx_pool_large_s {
ngx_pool_large_t *next;
void *alloc;
};
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler;
void *data;
ngx_pool_cleanup_t *next;
};
ngx_uint_t ngx_pagesize;
static void * ngx_palloc_block(ngx_pool_t *pool, size_t size);
static void * ngx_palloc_large(ngx_pool_t *pool, size_t size);
void *
ngx_alloc(size_t size, ngx_log_t *log)
{
void *p;
p = malloc(size);
if (p == NULL) {
printf("malloc error \r\n");
/*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;
}
//获得pagesize
void
ngx_os_init()
{
ngx_pagesize = getpagesize();
printf("pagesize = %d .... \r\n",ngx_pagesize);
getchar();
}
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
ngx_pool_t *p;
///可以看到直接分配size大小,也就是说我们只能使用size-sizeof(ngx_poll_t)大小
p = ngx_alloc(size, log);
if (p == NULL) {
return NULL;
}
///开始初始化数据区。
///由于一开始数据区为空,因此last指向数据区的开始。
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
///end也就是数据区的结束位置
p->d.end = (u_char *) p + size;
p->d.next = NULL;
p->d.failed = 0;
///这里才是我们真正能使用的大小。
size = size - sizeof(ngx_pool_t);
///然后设置max。内存池的最大值也就是size和最大容量之间的最小值。
p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
///current表示当前的内存池。
p->current = p;
///其他的域置NULL。
p->chain = NULL;
p->large = NULL;
p->cleanup = NULL;
/*p->log = log; */
///返回指针。
return p;
}
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
u_char *m;
ngx_pool_t *p;
///首先判断当前申请的大小是否超过max,如果超过则说明是大块,此时进入large
if (size <= pool->max) {
///得到当前的内存池指针。
p = pool->current;
///开始遍历内存池,
do {
///首先对齐last指针。
m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);
///然后得到当前内存池中的可用大小。如果大于请求大小,则直接返回当前的last,也就是数据的指针。
if ((size_t) (p->d.end - m) >= size) {
///更新last,然后返回前面保存的last。
p->d.last = m + size;
return m;
}
///否则继续遍历
p = p->d.next;
} while (p);
///到达这里说明内存池已经满掉,因此我们需要重新分配一个内存池然后链接到当前的data的next上。(紧接着我们会分析这个函数)
return ngx_palloc_block(pool, size);
}
///申请大块。
return ngx_palloc_large(pool, size);
}
static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
u_char *m;
size_t psize;
ngx_pool_t *p, *new, *current;
///计算当前的内存池的大小。
psize = (size_t) (pool->d.end - (u_char *) pool);
printf("size=%d\r\n",psize);
///再分配一个同样大小的内存池
m = ngx_alloc(psize, pool->log);
if (m == NULL) {
return NULL;
}
new = (ngx_pool_t *) m;
///接下来和我们create一个内存池做的操作一样。就是更新一些指针
new->d.end = m + psize;
new->d.next = NULL;
new->d.failed = 0;
///这里要注意了,可以看到last指针是指向ngx_pool_data_t的大小再加上要分配的size大小,也就是现在的内存池只包含了ngx_pool_data_t和数据。
m += sizeof(ngx_pool_data_t);
m = ngx_align_ptr(m, NGX_ALIGNMENT);
new->d.last = m + size;
///设置current。
current = pool->current;
///这里遍历所有的子内存池,这里主要是通过failed来标记重新分配子内存池的次数,然后找出最后一个大于4的,标记它的下一个子内存池为current。
for (p = current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
current = p->d.next;
}
}
///链接到最后一个内存池后面
p->d.next = new;
///如果current为空,则current就为new。
pool->current = current ? current : new;
return m;
}
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;
///开始遍历large链表,如果有alloc(也就是内存区指针)为空,则直接指针赋值然后返回。一般第一次请求大块内存都会直接进入这里。并且大块内存是可以被我们手动释放的。
for (large = pool->large; large; large = large->next) {
if (large->alloc == NULL) {
large->alloc = p;
return p;
}
if (n++ > 3) {
break;
}
}
///malloc一块ngx_pool_large_t。
large = ngx_palloc(pool, sizeof(ngx_pool_large_t));
if (large == NULL) {
ngx_free(p);
return NULL;
}
///然后链接数据区指针p到large。这里可以看到直接插入到large链表的头的。
large->alloc = p;
large->next = pool->large;
pool->large = large;
return p;
}
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);
}
}
pool->large = NULL;
for (p = pool; p; p = p->d.next) {
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
}
}
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);
}
}
///free大块内存
for (l = pool->large; l; l = l->next) {
/*ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);*/
if (l->alloc) {
ngx_free(l->alloc);
}
}
///遍历小块内存池。
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
///直接free掉。
ngx_free(p);
if (n == NULL) {
break;
}
}
}
int main(int argc,char *argv[])
{
char *p;
int i=0;
int c=10000;
ngx_os_init();
ngx_pool_t* pool =ngx_create_pool(12,NULL);
while(c-->0)
{
for(i=0;i<100;i++)
{
p=(char*)ngx_palloc(pool,11);
printf("get %p \r\n",p);
}
if(c%10 == 0){ngx_reset_pool(pool);printf("Reset pool..................\r\n");}
}
getchar();
return 0;
} </div>
下面解释一下主要的几个操作:
// 创建内存池 ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log);
大致的过程是创建使用 ngx_alloc 分配一个size大小的空间, 然后将 ngx_pool_t* 指向这个空间, 并且初始化里面的成员, 其中
p->d.last = (u_char *) p + sizeof(ngx_pool_t); // 初始指向 ngx_pool_t 结构体后面 p->d.end = (u_char *) p +size; // 整个结构的结尾后面 p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL; // 最大不超过 NGX_MAX_ALLOC_FROM_POOL,也就是getpagesize()-1 大小
其他大都设置为null或者0
// 销毁内存池 void ngx_destroy_pool(ngx_pool_t *pool);
遍历链表,所有释放内存,其中如果注册了clenup(也是一个链表结构), 会一次调用clenup 的 handler 进行清理。
// 重置内存池 void ngx_reset_pool(ngx_pool_t *pool);
释放所有large段内存, 并且将d->last指针重新指向 ngx_pool_t 结构之后(和创建时一样)
// 从内存池里分配内存 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的过程一般为,首先判断待分配的内存是否大于 pool->max的大小,如果大于则使用 ngx_palloc_large 在 large 链表里分配一段内存并返回, 如果小于测尝试从链表的 pool->current 开始遍历链表,尝试找出一个可以分配的内存,当链表里的任何一个节点都无法分配内存的时候,就调用 ngx_palloc_block 生成链表里一个新的节点, 并在新的节点里分配内存并返回, 同时, 还会将pool->current 指针指向新的位置(从链表里面pool->d.failed小于等于4的节点里找出) ,其他几个函数也基本上为 ngx_palloc 的变种,实现方式大同小异
// 释放指定的内存 ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);
这个操作只有在内存在large链表里注册的内存在会被真正释放,如果分配的是普通的内存,则会在destory_pool的时候统一释放.
// 注册cleanup回叫函数(结构体) ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size);
这个过程和我们之前经常使用的有些区别, 他首先在传入的内存池中分配这个结构的空间(包括data段), 然后将为结构体分配的空间返回, 通过操作返回的ngx_pool_cleanup_t结构来添加回叫的实现。 (这个过程在nginx里面出现的比较多,也就是 xxxx_add 操作通常不是实际的添加操作,而是分配空间并返回一个指针,后续我们还要通过操作指针指向的空间来实现所谓的add)
我的总结:这个思路非常的好,且充分的高效的利用了已申请的内存,current 指针实现了这一点,比如说,一次需要1024个字节,但内存池中空间不足(只有128个字节),会申请下一块内存放到后面,并返回地址供使用,但current 可能还在此节点上,下一次可以需要100个字节,current就可以从128里面取出100字节返回,以此类推,所以说充分利用已申请空间。