使用C语言编程时,一般使用malloc和free进行动态内存申请和释放。如果一不小心忘记了调用free进行释放,很容易造成内存泄露。另一方面,频繁地进行malloc和free操作,很容易造成内存碎片。与此同时,因为malloc支持多线程同时操作,所以,使用同步锁是不可避免的。当然,根据malloc的实现原理,线程在进行malloc操作的时候,如果不能获得同步锁,就会另外在进程的heap区域开辟一段子区域进行内存申请,这样有效地避免长时间等待。但是频繁尝试去获得锁也需要一定的时间开销。(建议阅读这一篇文章)
NGINX是一个对性能要求很高的系统。大到架构设计,小到细节实现都对性能提升做了充分的考虑。所以,对应内存管理,除了封装了libc库中的malloc和free操作以外。NGINX也实现了自己的内存管理系统,有效地减少了内存碎片的产生,降低了内存泄露发生的概率,减少了同步操作(尝试)的次数。这些都对NGINX本身性能的提升有一定的帮助。
NGINX自身的内存管理系统最重要的特点就是加强中央集权管理。把内存的申请,释放牢牢地掌握在自己手里。具体来说就是:
NGINX本身维护自己的内存池。当进程申请内存时,先在自身内存池中里去查找,如果找到直接返回。如果所有的内存池都找不到合适的内存,NGINX本身再去向系统去申请一片大内存进行分割管理。这样,有效地减少了系统调用malloc的次数。每次都是相对大片的内存申请,也有效地减少了内存碎片的发生几率。
内存释放进行统一管理。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 {
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: };
结构定义:
创建内存池 | 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); |
内存申请(对齐) | void * ngx_palloc(ngx_pool_t *pool, size_t size); |
内存申请(不对齐) | void * ngx_pnalloc(ngx_pool_t *pool, size_t size); |
内存清除 | ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p); |
内存对齐:
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
封装函数
ngx_alloc:(只是对malloc进行了简单的封装)
void *ngx_alloc(size_t size, ngx_log_t *log)
2: {
3: void *p;
4:
5: p = malloc(size);
6: if (p == NULL) {
7: ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
8: "malloc(%uz) failed", size);
9: }
10:
11: ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, log, 0, "malloc: %p:%uz", p, size);
12:
13: return p;
14: }
注一:
log是记录时间
ngx_calloc:(调用malloc并初始化为0)
1: void *ngx_calloc(size_t size, ngx_log_t *log)
2: {
3: void *p;
4:
5: p = ngx_alloc(size, log);
6:
7: if (p) {
8: ngx_memzero(p, size);
9: }
10:
11: return p;
12: }
ngx_memzero:
#define ngx_memzero(buf, n) (void) memset(buf, 0, n)
ngx_free :
1: #define ngx_free free
ngx_memalign:
#define ngx_memalign(alignment, size, log) ngx_alloc(size, log)
这里alignment主要是针对部分unix平台需要动态的对齐,对POSIX 1003.1d提供的posix_memalign( )进行封装,在大多数情况下,编译器和C库透明地帮你处理对齐问题。nginx中通过宏NGX_HAVE_POSIX_MEMALIGN来控制;
遵循RALL
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);
//#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
//内存池最大不超过4095,x86中页的大小为4K
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;
}
1.nginx对内存的管理分为大内存与小内存,当某一个申请的内存大于某一个值时,就需要从大内存中分配空间否则从小内存中分配空间。
2.nginx中的内存池是在创建的时候就设定好了大小,在以后分配小块内存的时候,如果内存不够,则是重新创建一块内存串到内存池中,而不是将原有的内存池进行扩张。当要分配大块内存是,则是在内存池外面再分配空间进行管理的,称为大块内存池。
ngx_palloc
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_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);
}
ngx_align_ptr,这是一个用来内存地址取整的宏,非常精巧,一句话就搞定了。作用不言而喻,取整可以降低CPU读取内存的次数,提高性能。因为这里并没有真正意义调用malloc等函数申请内存,而是移动指针标记而已,所以内存对齐的活,C编译器帮不了你了,得自己动手
1: #define ngx_align_ptr(p, a) \
2: (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
2.ngx_palloc_block(ngx_pool_t *pool, size_t size)
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;//设置新内存块的end
new->d.next = NULL;
new->d.failed = 0;
m += sizeof(ngx_pool_data_t);//将指针m移动到d后面的一个位置,作为起始位置
m = ngx_align_ptr(m, NGX_ALIGNMENT);
new->d.last = m + size;//设置新内存块的last,即申请使用size大小的内存
//这里的循环用来找最后一个链表节点,这里failed用来控制循环的长度,如果分配失败次数达到5次,
//就忽略,不需要每次都从头找起
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;
}
3.ngx_palloc_large(ngx_pool_t *pool, size_t size)
在ngx_palloc中首先会判断申请的内存大小是否超过内存块的最大限值,如果超过,则直接调用ngx_palloc_large,进入大内存块的分配流程;
1: static void *
2: ngx_palloc_large(ngx_pool_t *pool, size_t size)
3: {
4: void *p;
5: ngx_uint_t n;
6: ngx_pool_large_t *large;
7: // 直接在系统堆中分配一块空间
8: p = ngx_alloc(size, pool->log);
9: if (p == NULL) {
10: return NULL;
11: }
12:
13: n = 0;
14: // 查找到一个空的large区,如果有,则将刚才分配的空间交由它管理
15: for (large = pool->large; large; large = large->next) {
16: if (large->alloc == NULL) {
17: large->alloc = p;
18: return p;
19: }
20:
21: if (n++ > 3) {
22: break;
23: }
24: }
25: //为了提高效率, 如果在三次内没有找到空的large结构体,则创建一个
26: large = ngx_palloc(pool, sizeof(ngx_pool_large_t));
27: if (large == NULL) {
28: ngx_free(p);
29: return NULL;
30: }
31:
32: large->alloc = p;
33: large->next = pool->large;
34: pool->large = large;
35:
36: return p;
37: }
void
2: ngx_reset_pool(ngx_pool_t *pool)
3: {
4: ngx_pool_t *p;
5: ngx_pool_large_t *l;
6: //释放所有大块内存
7: for (l = pool->large; l; l = l->next) {
8: if (l->alloc) {
9: ngx_free(l->alloc);
10: }
11: }
12:
13: pool->large = NULL;
14: // 重置所有小块内存区
15: for (p = pool; p; p = p->d.next) {
16: p->d.last = (u_char *) p + sizeof(ngx_pool_t);
17: }
18: }
ngx_pfree
ngx_pfree(ngx_pool_t *pool, void *p)
3: {
4: ngx_pool_large_t *l;
5: //只检查是否是大内存块,如果是大内存块则释放
6: for (l = pool->large; l; l = l->next) {
7: if (p == l->alloc) {
8: ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
9: "free: %p", l->alloc);
10: ngx_free(l->alloc);
11: l->alloc = NULL;
12:
13: return NGX_OK;
14: }
15: }
16:
17: return NGX_DECLINED;
18: }
所以说Nginx内存池中大内存块和小内存块的分配与释放是不一样的。我们在使用内存池时,可以使用ngx_palloc进行分配,使用ngx_pfree释放。而对于大内存,这样做是没有问题的,而对于小内存就不一样了,分配的小内存,不会进行释放。因为大内存块的分配只对前3个内存块进行检查,否则就直接分配内存,所以大内存块的释放必须及时。
ngx_pool_cleanup_s
Nginx内存池支持通过回调函数,对外部资源的清理。ngx_pool_cleanup_t是回调函数结构体,它在内存池中以链表形式保存,在内存池进行销毁时,循环调用这些回调函数对数据进行清理。
1: struct ngx_pool_cleanup_s {
2: ngx_pool_cleanup_pt handler;
3: void *data;
4: ngx_pool_cleanup_t *next;
5: };
handler:是回调函数指针;
data:回调时,将此数据传入回调函数;
next:指向下一个回调函数结构体;
如果我们需要添加自己的回调函数,则需要调用ngx_pool_cleanup_add来得到一个ngx_pool_cleanup_t,然后设置handler为我们的清理函数,并设置data为我们要清理的数据。这样在ngx_destroy_pool中会循环调用handler清理数据;
ngx_destroy_pool
ngx_destroy_pool这个函数用于销毁一个内存池:
void
2: ngx_destroy_pool(ngx_pool_t *pool)
3: {
4: ngx_pool_t *p, *n;
5: ngx_pool_large_t *l;
6: ngx_pool_cleanup_t *c;
7:
8: //首先调用所有的数据清理函数
9: for (c = pool->cleanup; c; c = c->next) {
10: if (c->handler) {
11: ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
12: "run cleanup: %p", c);
13: c->handler(c->data);
14: }
15: }
16:
17: //释放所有的大块内存
18: for (l = pool->large; l; l = l->next) {
19:
20: ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
21:
22: if (l->alloc) {
23: ngx_free(l->alloc);
24: }
25: }
26:
27: //最后释放所有内存池中的内存块
28: for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
29: ngx_free(p);
30:
31: if (n == NULL) {
32: break;
33: }
34: }
NGINX通过ngx_create_pool创建内存池。参数size用来指定从内存池的小块内存区域总共可以获取的内存的最大,同时也是可以申请的单个小块内存的最大size。在函数执行过程中size会转化成16的整数倍。
NGINX提供了几个从内存池中申请内存的API。他们大体流程是一样的。如果申请的内存的size不大于max,如就从小内存区域试着去切分,并且移动内存池的last指针指向新申请内存地址的末端。反之,就通过系统的malloc申请一片内存并且连接到pool的large链表中。
void *ngx_palloc(ngx_pool_t*pool,size_t size)把size大小对齐然后再申请内存,若size大于max,则改用向系统申请。
void *ngx_pnalloc(ngx_pool_t*pool,size_t size)和上面函数唯一的区别是size不用对齐.
void*ngx_pmemalign(ngx_pool_t *pool,size_tsize)无论size大小实际内存都向系统申请,并且加入到内存池的large链表中。其中对应的管理结构ngx_large_t是从内存池中申请。
void *ngx_pcalloc(ngx_pool_t*pool,size_t size)申请并初始化为零。
从内存池中申请的小块内存不能单独释放,只能在内存池释放时被整体被释放。大内存块因为是通过系统调用ngx_alloc申请的,所以,这些内存块可以调用ngx_pfree被单独释放。这两个函数本质上是对系统调用malloc和free的封装。
通过ngx_destroy_pool可以释放创建的内存池。函数首先会调用cleanup链表中的所有函数,然后通过调用free释放large链表中所有通过系统申请的大块内存。最后释放所有的内存池结构这其中就包括所有的小块内存。
通过使用内存池,NGINX有效地降低了内存分片,减少了内存泄露的可能。在使用小内存时只是进行了简单粗暴地分割来分配内存。这一方面简化了操作提高了效率。但是,另一方面这些大小不一小块内存因为没有管理信息的维护而不能及时释放和重用。它们只能在整个内存池释放时才能作为一个整体能得以释放。不过因为NGINX本身运行具有的阶段化的特征,特定内存池都只在特定阶段存在,使得内存不能及时释放的影响不是很大。
或许NGINX的内存池也能结合kernel的slab内存池的某些特性。这些slab内存池的内存块也是从一个大的内存区域切分出来,它们被有效地管理起来,可以很方便地进行释放和重用。而且在内存池释放时可以方便地释放掉所有的内存,也可以有效地杜绝内存泄露的发生。但是有些额外的管理开销所以会浪费一些内存,而且每一个内存池只能支持一个size的内存块的申请。需要把这些不同size的内存池有效地组织和管理起来。