C的内存管理难已经成为了共识。记得我那时候写ydfs,第一次接触全异步编程这种模式,没有考虑到内存的统一管理,导致溢出泄漏问题层出不穷。最终搞定没BUG了,那代码的阅读质量也是让人不敢恭维。那么来看看nginx是怎么做的吧。
nginx的内存池分为池(pool)和块(block)两个概念
这是nginx使用到的内存结构
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
| typedef struct ngx_pool_large_s ngx_pool_large_t;
struct ngx_pool_large_s { ngx_pool_large_t *next; void *alloc; };
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; };
typedef struct { ngx_fd_t fd; u_char *name; ngx_log_t *log; } ngx_pool_cleanup_file_t;
|
池中以链表的形式保存许多块以及large链表。这是从数据结构中能大致看出的东西。
然后通过实际的函数来看看池的具体使用方式吧。
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
| 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_destroy_pool函数很简单,先按链表调用所有的cleanup回调做一些上层模块所需操作,然后依次释放large链表的数据,最后依次释放块。
ngx_reset_pool函数将释放所有的large内存区,然后通过重置每个块的last指针来重置普通内存块(并不释放),另外cleanup回调也不会被调用或者释放。
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
| 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大于max,那么直接申请large内存,否则寻找能插入的块,如果找不到那么就通过ngx_palloc_block来新建块。
这里要提一下ngx_align_ptr
1 2 3
| #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))
|
可以这样理解d & ~(a-1) + (a-1)&~(a-1)
首先来理解~(a-1),a为2的幂,设右数第n位为非零位,则a-1为右数的n-1位均为1,
则有~(a-1)为最后的n-1位全为0
一个数和最后n-1位全为0的数相与,那么得到了他减去(他模a的余数)的值
然后加上(a-1)&~(a-1),就得到了对齐的数
计算宏ngx_align(1,
64)=64,只要输入d<64,则结果总是64,如果输入d=65,则结果为128,以此类推。
CPU访问不对齐的数据时,可能要进行多次内存访问。这样的对齐还是有必要的。
而NGX_ALIGNMENT的值在不同的平台上大小不一样,大致为一个字。
1
| #define NGX_ALIGNMENT sizeof(unsigned long)
|
另外可以看到ngx_align_ptr的其他地方ngx_crc32.c,ngx_hash.c也有用到。
是ngx_align_ptr(p, ngx_cacheline_size)这样使用的
ngx_cacheline_size是CPU二级cache的大小
这个变量的获取涉及到不同CPU的汇编调用了,都在core/ngx_cpuinfo.c中
可以看这篇文章的解释
Nginx源码完全注释(5)core/ngx_cpuinfo.c
下面看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 39 40 41 42 43
| 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_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;
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; }
|
从m +=
sizeof(ngx_pool_data_t)可以看到,后续的next指针使用的实际上都是块结构,尽管是ngx_pool_t指针。
另外pool->current指针只有在这里才会进行更新,一般只有在所有的节点错误次数都大于5以后,才会把新建的这个节点作为current记录。否则current总是存在值的,也就不会进行更新。
ngx_pnalloc是ngx_palloc的一个不对齐版本,没有ngx_memalign调用
ngx_palloc_large是简单的链表操作罢了,新的大内存总是插入链表尾
cleanup函数相关也是把回调简单的通过链表连接起来,每次把新加入的链表加入链表尾
基本上nginx的内存池就是那么多干货拉
可以看到这个内存池的核心思想有两个:
1.统一申请接口,统一进行数据销毁
2.一切内存处理全部进行内存对齐