(nginx源码系列四)--nginx内存池分析

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;//池中块支持的最大大小,大于该大小将分配在large结构中
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;

//设定large块的最小临界值,这个值最大不会超过ngx_pagesize大小
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)    /* platform word */

另外可以看到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);

//相当于malloc,但是由于malloc的内存对齐一般是以8字节为单位的,
//更大的粒度就需要memalign了
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;

//这里在寻找最后一个节点,并且把扫描到的分配失败的节点状态值都加1,
//同时如果>4那么忽略他,并且把更新当前的节点
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.一切内存处理全部进行内存对齐