Nginx 内存池源码分析

内存池介绍

  在C语言中,程序员需要手动管理动态分配的内存,这带来了不少麻烦的事情。譬如说,当某块内存使用完毕之后,程序员需要记得释放这块内存,否则就会发生内存泄漏。另一方面,如果程序中需要频繁地分配和释放小块的内存,倘若每次都是直接通过malloc()去分配内存,这将带来额外的调用开销,同时也会产生内存碎片。
  Nginx 使用内存池ngx_pool_t来管理内存,这带来了不少的好处:

  • 当程序需要内存时,只管向内存池申请,申请而来的内存也无需手动释放,因为内存池销毁时会负责释放所有的内存。
  • 对于那些需要频繁分配和释放小块内存的程序来说,使用内存池可以减少malloc()的次数,除了降低了调用开销之外,还减少了内存碎片的产生。

数据结构

  下面是内存池的数据结构,ngx_pool_t代表内存池,ngx_pool_data_t代表小块内存,而ngx_pool_large_t则代表大块内存。

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
// 大块内存
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;
// 内存池
typedef struct ngx_pool_s ngx_pool_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;
};

内存池构造

  内存池处理大块内存和小块内存的策略是不同的,譬如说,当用户向内存池申请内存时,内存池会判断申请的是大块内存还是小块内存,如果是小块内存,那么直接在内存池里面分配,但如果是大块内存,就直接向 OS 申请。ngx_pool_tmax成员则作为判断的标准,如果用户申请的内存大于max,则认为是大块内存,否则就是小块内存。下面的ngx_create_pool()函数用于创建和初始化内存池:

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
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
#define NGX_POOL_ALIGNMENT 16
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); // 返回的地址按 16 个字节对齐
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_create_pool()会分配一块比较大的内存,这块内存由两部分组成,一部分用来容纳ngx_pool_t结构体,另一部分内存则是用来等待用户申请。last指针和end指针之间的内存是空闲的,当用户申请小块内存时,如果空闲的内存大小满足用户的需求,则可以分配给用户。

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
---------------|
| | last ->--|--------|
| | end ->--|--------|------|
| d | next | | |
| | failed | | |
|--------------| | |
| max | | |
---------------| | |
| current | | |
---------------| | |
| chain | | |
---------------| | |
| large | | |
---------------| | |
| cleanup | | |
---------------| | |
| log | | |
---------------| | |
| | | |
| 已使用的内存 | | |
| | last | |
---------------| <------| |
| | |
| 未使用的内存 | |
| | end |
---------------| <-------------|

分配小块内存

  用户可以调用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
void *ngx_palloc(ngx_pool_t *pool, size_t size)
{
u_char *m;
ngx_pool_t *p;
if (size <= pool->max) {
// 用户申请的是小块内存
// 遍历小块内存链表,pool->current 指向第一个要遍历的小块内存
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);
}

  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
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_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;
// 所有小块内存的 failed 都加 1,表示不满足用户的需求 +1 次
current = pool->current;
for (p = current; p->d.next; p = p->d.next) {
// 某个小块内存若连续 5 次都不满足用户需求,则跳过这个小块内存,下次不再遍历它
if (p->d.failed++ > 4) {
current = p->d.next;
}
}
p->d.next = new;
pool->current = current ? current : new;
return m;
}

  下图是创建一个新的小块内存之后的效果图:

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
---------------| ---------------|
| | last ->--|--------| | | last ->--|------|
| | end ->--|--------|------| | | end ->--|------|------|
| d | next ->--|--------|------|------->| d | next ->---------|------|------>
| | failed | | | | | failed | | |
|--------------| | | |--------------| | |
| max | | | | | | |
---------------| | | | 已使用的内存 | | |
| current | | | | | last| |
---------------| | | |--------------| <----| |
| chain | | | | | |
---------------| | | | 未使用的内存 | |
| large | | | | | end |
---------------| | | |--------------| <-----------|
| cleanup | | |
---------------| | |
| log | | |
---------------| | |
| | | |
| 已使用的内存 | | |
| | last | |
---------------| <------| |
| | |
| 未使用的内存 | |
| | end |
---------------| <-------------|

大块内存

  内存池ngx_pool_t的成员large指向大块内存链表,如果用户申请的是大块内存,那么内存池就会调用ngx_palloc_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
29
30
31
32
33
34
35
36
37
38
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); // 相当于调用 malloc
if (p == NULL) {
return NULL;
}
n = 0;
// 遍历大块内存链表,找到可以挂载 p 的位置
for (large = pool->large; large; large = large->next) {
if (large->alloc == NULL) {
large->alloc = p;
return p;
}
// 若连续 4 次都没找到,就不再寻找了
if (n++ > 3) {
break;
}
}
// 创建一个 ngx_pool_large_t 结构,用来挂载 p
large = ngx_palloc(pool, sizeof(ngx_pool_large_t));
if (large == NULL) {
ngx_free(p);
return NULL;
}
// 将新分配的 ngx_pool_large_t 结构体插入到链表开头
large->alloc = p;
large->next = pool->large;
pool->large = large;
return p;
}

参考资料