nginx内存池原理
轮子-nginx内存池
- 内存池中分为大内存block和小内存block
- 大内存block的头信息在小内存块中。大内存块头信息通过链表连接起来。
内存池结构
整体
- 内存池
1
2
3
4
5
6
7
8
9// 内存池
struct ngx_pool_s {
ngx_pool_data_t d; // 小内存Block的头信息
size_t max; // p->max:一个Block块内最多能分配多大的小内存。其大小受制于程序本身ngx_memalign开辟的大小,也受制于小内存定义的上限4095
ngx_pool_t* current; // 指向当前内存块
ngx_pool_large_t* large; // 大内存Block头信息的链表入口
ngx_pool_cleanup_t* cleanup; // 外部资源的头信息链表
};
小内存Block 头信息
1 | // 小内存池头信息 |
大内存Block 头信息
1 | // 大内存池头信息 |
外部资源 头信息
1 | struct ngx_pool_cleanup_s { |
一些宏
1 |
创建内存池 ngx_create_pool
ngx_create_pool
- 创造的是一个内存Block,从操作系统malloc而来。是打头的那个。有完整的ngx_pool_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
27
28
29
30
31
32
33
34
35// size:内存池大小(实际可以使用的比size小)
// log:日志
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
ngx_pool_t *p;
// 开辟内存池。依据平台调用相应函数
// ngx_pool_t + 自由内存
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; // 下一个小内存block
p->d.failed = 0; // 当前内存块分配内存失败次数
size = size - sizeof(ngx_pool_t); // 内存Block实际能使用的大小。整个内存池大小size - 内存池头信息
// p->max:一个Block块内最多能分配多大的小内存。
// 其大小受制于程序本身ngx_memalign开辟的大小,也受制于小内存定义的上限4095
// p->max = min(size,4095)
// max不超过一个页面的大小
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_memalign
- 向操作系统malloc内存
ngx_memalign(size_t alignment, size_t size, ngx_log_t *log)
:- 开辟内存池(size和log参数起作用),并根据平台选择是否内存对齐(alignment参数起作用)
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66/*
* Linux has memalign() or posix_memalign()
* Solaris has memalign()
* FreeBSD 7.0 has posix_memalign(), besides, early version's malloc()
* aligns allocations bigger than page size at the page boundary
*/
// 根据平台选择是否内存对齐
void *ngx_memalign(size_t alignment, size_t size, ngx_log_t *log); // 进行内存对齐
----------------------------------------------------------------------------
// ngx_alloc:就是malloc
void *
ngx_alloc(size_t size, ngx_log_t *log)
{
void *p;
p = malloc(size);
if (p == NULL) {
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;
}
--------------------------------------------------
// void *ngx_memalign(size_t alignment, size_t size, ngx_log_t *log); // 内存对齐的malloc
void *
ngx_memalign(size_t alignment, size_t size, ngx_log_t *log)
{
void *p;
int err;
err = posix_memalign(&p, alignment, size);
if (err) {
ngx_log_error(NGX_LOG_EMERG, log, err,
"posix_memalign(%uz, %uz) failed", alignment, size);
p = NULL;
}
ngx_log_debug3(NGX_LOG_DEBUG_ALLOC, log, 0,
"posix_memalign: %p:%uz @%uz", p, size, alignment);
return p;
}
void *
ngx_memalign(size_t alignment, size_t size, ngx_log_t *log)
{
void *p;
p = memalign(alignment, size);
if (p == NULL) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
"memalign(%uz, %uz) failed", alignment, size);
}
ngx_log_debug3(NGX_LOG_DEBUG_ALLOC, log, 0,
"memalign: %p:%uz @%uz", p, size, alignment);
return p;
}
- 开辟内存池(size和log参数起作用),并根据平台选择是否内存对齐(alignment参数起作用)
内存Block分配 ngx_palloc
- 小块内存分配、大块内存分配
- ngx_palloc 考虑了内存对齐
1
2
3
4
5
6
7
8
9
10
11
12
13
14void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
// 分配小块内存(优先从内存池分配,如果没有,再新开辟ngx_memalign)
// size:上级要请求的
// pool->max block块内空闲内存的上限
if (size <= pool->max) {
return ngx_palloc_small(pool, size, 1); // 指针 ,大小 ,1:对齐
}
// 分配大块内存
return ngx_palloc_large(pool, size);
}
ngx_palloc_small 小块内存分配
从操作系统开辟新的小块内存池。ngx_palloc_small调用ngx_palloc_block。ngx_palloc_block底层调用memalign。
效率极高:如何分配内存:通过移动last指针分配内存。将移出的size内存返回给上级
ngx_palloc_small
- 先尝试用已有的内存池中拿出size大小的内存块,当内存池里没有可分配的满足size大小的内存块时,再ngx_palloc_block开辟新block块。从block里拿出size大小的内存返回给上级。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25static 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); // 内存对齐:把指针调整到平台相关的4/8倍数。
}
// 内存池的空闲内存大于要申请的内存
if ((size_t) (p->d.end - m) >= size) {
// m指针偏移size字节,即内存池给应用程序分配内存
p->d.last = m + size;
return m;
}
// 如果本block块剩余的不够size,那么顺着p->d.next向下走到第二个内存块block。
p = p->d.next;
} while (p);
// 从pool->current开始 遍历完了 所有的block,也没找到够用的空闲内存
// 那么就只能新开辟block
return ngx_palloc_block(pool, size);
}
- 先尝试用已有的内存池中拿出size大小的内存块,当内存池里没有可分配的满足size大小的内存块时,再ngx_palloc_block开辟新block块。从block里拿出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
36
37
38static 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); // Block大小
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;
// 由于进入到这个函数必然意味着之前的block都分配内存失败
// 所以要把前几个block的fail次数都++;
// 当一个block了=块分配内存失败次数
// 当一个block块失败次数>4之后,就认为这块Block的剩余内存已经很少,之后请求小内存时就不从这块Block开始请求
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;
}
- 原有内存池内存不够,分配新的内存块加入内存池
ngx_free 释放大块内存
小块内存不释放
ngx_palloc_large 大内存块分配
从操统malloc大块内存。ngx_palloc_large调用ngx_alloc。ngx_alloc调用malloc
malloc大块内存用于存储数据;从小内存池申请一块内存用作大内存池头信息;维护大内存池链表;将本次malloc的大块内存全部返回给上级使用。
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
43static void *
ngx_palloc_large(ngx_pool_t *pool, size_t size)
{
void *p;
ngx_uint_t n;
ngx_pool_large_t *large;
// malloc大块内存;不进行字节对齐
p = ngx_alloc(size, pool->log);
if (p == NULL) {
return NULL;
}
// 以pool->large为起始 遍历大内存池头信息节点
n = 0;
for (large = pool->large; large; large = large->next) {
// 如果遍历到的这块大内存池节点的头信息的alloc==nullptr
// 意味着并这个ngx_pool_large_t没有管理一块大内存。
// 所以直接由他管理
if (large->alloc == NULL) {
large->alloc = p;
return p;
}
// 为了效率
if (n++ > 3) {
break;
}
}
// 向小内存池申请一段内存用作大内存池头信息
large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
if (large == NULL) { // 没申请来就放弃本次操作
ngx_free(p);
return NULL;
}
// 将大内存池头插法插入large起始的链表中
large->alloc = p;
large->next = pool->large;
pool->large = large;
return p;
}ngx_alloc() 分配大块内存,不进行内存对齐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void *
ngx_alloc(size_t size, ngx_log_t *log)
{
void *p;
p = malloc(size);
if (p == NULL) {
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;
}ngx_free()
1
大小内存Block的重置 ngx_reset_pool
nginx
大块内存block通过malloc
分配- 大块内存:通过
free
释放
- 大块内存:通过
nginx
小内存通过在内存池(的一个小内存块)中移动last指针来实现内存分配- 小块内存:无
free
释放,只是通过移动last指针来重置
- 小块内存:无
1 | void |
释放指定大内存块 ngx_pfree
- 释放指定大内存块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16ngx_int_t
ngx_pfree(ngx_pool_t *pool, void *p)
{
ngx_pool_large_t *l;
for (l = pool->large; l; l = l->next) {
if (p == l->alloc) {
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
"free: %p", l->alloc);
ngx_free(l->alloc);
l->alloc = NULL;
return NGX_OK;
}
}
return NGX_DECLINED;
}
为什么reset_pool中nginx内存池的小内存块不free释放??
- 因为没有free小块内存的接口。
- 所以只能移动last指针
1. 为什么没free接口
- 首先nginx内存池的没办法回收针对某一特定小内存块进行free回收。
- 因为从实现方式上来看,就释放不了。我们通过移动last指针来分配内存。可能我们要释放2号内存,但是我们只有last和end的两个指针,且last指向3号下的free内存。那么如果移动last到2那里然后free,那么就会把3也free掉
- 因此nginx没有提供free回收小块内存的接口函数
- 所以重置时我们只能通过移动last指针来表示哪些内存是空闲内存
2. 为什么移动last可以
- 那么在释放内存时为什么移动last就可以。这样能表明装有没用数据的内存是空闲的,不害怕有用的内存被覆盖吗?
- 当
reset_pool
时,不会有有用的内存块。因为我们是短连接。 nginx
本质:http
服务器。短连接、间歇性。- client向nginx server发送请求后,nginx处理完请求,并发送完response响应后;即一次服务处理完后,就主动和客户端断开tcp连接,该次连接的资源就都可以释放了。(server就像和client端没见过面一样。之前的东西都不必再保留,一干二净。)
- 所以,当nginx server 处理完一次请求后,就可以调用
ngx_reset_pool
,free大块内存;将小块内存的last指针回退,等待下次覆盖。
- 而长连接不论client是否发来请求,server和client是不能断开的。也即处理完请求后,连接也不能断开,资源也不允许释放,直到系统资源耗尽。长连接的这种server应该用SGI STL的二级空间配置器中的内存池。??????
- 长连接举例:
http1.1 keep-alive = 60s
,即http服务器返回响应后,需等待60s
。如果这段时间之内客户端又发来请求,那么重置等待时间。如果没发来请求,那么就断开连接。
- 长连接举例:
- 以上关于nginx用于短连接的场景我可以理解。不过他为什么不能用于长连接?我只写过短连接的webserver,还没写过长连接的。等写了长连接的server应该就能理解了吧。
- 长连接短连接
- nginx服务器入门
nginx内存池和sgi stl二级空间配置器
- nginx对于小块内存的分配效率绝对比sgi stl二级空间配置器高,因为nginx只需要移动指针。
- nginx无法释放小块内存。sgi stl 小块和大块都释放。
- 故nginx适用于短连接Web服务器(http服务器)。sgi stl适用于长连接服务器。
内存池外部资源释放
外部资源信息头
1 | struct ngx_pool_cleanup_s { |
内存池外部资源释放
对于cleanup_add的使用有两种方式
- 一种是给c->data开辟一块内存,然后把外部资源拷贝进去。之后destroy中c->handler(void*p)
ngx_pool_cleanup_t * c = ngx_pool_cleanup_add(pool,sizeof(x))
- 感觉适用于一个这块内存里很多指针管理很多外部资源,可以省去挨个设置外部资源信息头以及回调函数。
- 一种是不给c->data开辟内存,直接让c->data指向要释放的内存。
ngx_pool_cleanup_t * c = ngx_pool_cleanup_add(pool,0)
- 我觉得在一块申请的内存中由很多指针管理外部资源时,【第一种优于第二种】。
- 相较于【第二种不拷贝方法】单独为 每个指针管理的外部资源都设置一个外部资源信息头,以及一个释放的回调函数;不如【第一种方法】只设置一个回调函数和一个信息头。将这些外部资源拷贝到一块内存中,然后将这块内存传入即可。这样就可以用一个信息头,一个回调函数,即可释放多个资源。
源码论证
- 第【1】种
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// 结构体
typedef struct {
ngx_fd_t fd;
u_char *name;
ngx_log_t *log;
} ngx_pool_cleanup_file_t;
// 为c->data开辟内存
cln = ngx_pool_cleanup_add(pool, sizeof(ngx_pool_cleanup_file_t));
....
cln->handler = clean ? ngx_pool_delete_file : ngx_pool_cleanup_file;
// 将管理的资源地址拷贝到c->data指向的内存
clnf = cln->data;
clnf->fd = file->fd;
clnf->name = file->name.data;
clnf->log = pool->log;
// 预制的回调函数
void
ngx_pool_delete_file(void *data)
{
ngx_pool_cleanup_file_t *c = data;
ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d %s",
c->fd, c->name);
if (ngx_delete_file(c->name) == NGX_FILE_ERROR) {
...
}
if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
...
}
} - 第【2】种
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// 不申请内存
cln = ngx_pool_cleanup_add(r->pool, 0);
if (cln == NULL) {
return NGX_ERROR;
}
// 直接将地址给cln->data
cln->handler = ngx_http_file_cache_cleanup;
cln->data = c;
// 预制回调函数
static void
ngx_http_file_cache_cleanup(void *data)
{
ngx_http_cache_t *c = data;
if (c->updated) {return;}
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->file.log, 0,
"http file cache cleanup");
if (c->updating && !c->background) {
...
}
ngx_http_file_cache_free(c, NULL);
}
- cleanup_add功能概述:通过预置回调函数,实现c++中对象析构的功能。也即释放内存。
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
32ngx_pool_cleanup_t *
ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
{
ngx_pool_cleanup_t *c;
// 从小内存block中 分配一块内存用作ngx_pool_cleanup_t
c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
if (c == NULL) {
return NULL;
}
// 看用户是怎么调用的
// size>0 从内存池中申请一块内存。用作之后用户将外部资源拷贝到这里
// size=0 不申请内存。用户直接将c->data = 外部资源地址
if (size) {
c->data = ngx_palloc(p, size);
if (c->data == NULL) {
return NULL;
}
} else {
c->data = NULL;
}
c->handler = NULL;
c->next = p->cleanup;
p->cleanup = c;
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);
return c;
}
内存池销毁
- 先遍历链表执行外部资源的释放操作;再遍历链表执行释放大块内存的操作;再遍历链表重置小块内存Block
1 | void |
测试代码
- 环境:ubuntu 18.04 ; nginx-1.12.2 ;工具:Source Insigh4.0
- 指令
1
2gcc -c -g -I src/core -I src/event -I src/event/modules -I src/os/unix -I objs -I src/http -I src/http/modules -o ngx_testpool.o ngx_testpool.c
gcc -o ngx_testpool ngx_testpool.o objs/src/core/ngx_palloc.o objs/src/os/unix/ngx_alloc.o void *memcpy(void *des, const void *src, size_t n)
- 将src指向的内存中的内容逐字节的拷贝到des指向的内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using namespace std;
int main(int argc, char* argv[])
{
int a = 9;
int b = 10;
int* p = &a;
int* q = &b; // q不能=null!因为是要把p指向的内存的内容拷贝到q的内存里。q如果是null则error
memcpy(q, p, sizeof(int));
cout << q << endl;
cout << *q << endl;
cout << p << endl;
cout << *p << endl;
}
00000021ACF5F904
9
00000021ACF5F8E4
9
- 将src指向的内存中的内容逐字节的拷贝到des指向的内存
- ngx_testpool.c
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err,
const char *fmt, ...)
{
}
typedef struct Data stData;
struct Data
{
// 多个指针
char *ptr;
FILE *pfile;
char *ptr2;
};
typedef struct yData yData;
struct yData
{
char *ptr;
int x;
int y;
};
void self_handler(void *p)
{
printf("self_handler\n");
stData *q = (stData*) p;
printf("free ptr mem!\n");
free(q->ptr);
printf("free ptr mem!\n");
free(q->ptr2);
printf("close file!\n");
fclose(q->pfile);
}
void self_handler_02(void *p)
{
char *q = (char*)p;
printf("self_handler_02\n");
printf("free ptr mem!\n");
free(q);
}
void main()
{
// 1. ngx_create_pool 造内存池
// 第一块内存Block。里面有完整的ngx_pool_t
// ngx_pool_t::max = min(512 - sizeof(ngx_pool_t) , 4095)
ngx_pool_t *pool = ngx_create_pool(512, NULL);
if(pool == NULL)
{
printf("ngx_create_pool fail...");
return;
}
// 2. 小块内存以及外部资源
// 向内存池申请小块内存
stData *p1 = ngx_palloc(pool, sizeof(yData)); // 从小块内存池分配的
if(p1 == NULL)
{
printf("ngx_palloc 128 bytes fail...");
return;
}
// 小块内存保存的指针管理的外部资源
p1->ptr = malloc(12);
strcpy(p1->ptr, "hello world");
p1->pfile = fopen("data.txt", "w");
p1->ptr2 = malloc(15);
strcpy(p1->ptr2, "hhhh");
// 预置回调函数用于释放外部资源
ngx_pool_cleanup_t *c1 = ngx_pool_cleanup_add(pool,sizeof(yData)); // 开辟内存,用于handler传参
c1->handler = self_handler;
memcpy(c1->data,p1,sizeof(yData)); // 用户只负责拷贝!!将外部资源拷贝一下到c1->data。将p1指针指向的内容逐字节拷贝到c->data指向的内存
// 3. 大块内存以及外部资源
// 向内存池申请大块内存
yData *p2 = ngx_palloc(pool, 512); // 从大块内存池分配的
if(p2 == NULL)
{
printf("ngx_palloc 512 bytes fail...");
return;
}
// 外部资源
p2->ptr = malloc(12);
strcpy(p2->ptr, "hello world");
// 预置回调函数用于释放外部资源
ngx_pool_cleanup_t *c2 = ngx_pool_cleanup_add(pool,0); // 不开辟内存,直接让c->data指向要释放的内存
c2->handler = self_handler_02;
c2->data = p2->ptr;
// 释放内存池
ngx_destroy_pool(pool); // 1.调用所有的预置的清理函数 2.释放大块内存 3.释放小块内存池所有内存
return;
}
shc@shc-virtual-machine:~/code/nginx/nginx-1.12.2$ gcc -c -g -I src/core -I src/event -I src/event/modules -I src/os/unix -I objs -I src/http -I src/http/modules -o ngx_testpool.o ngx_testpool.c
shc@shc-virtual-machine:~/code/nginx/nginx-1.12.2$ gcc -o ngx_testpool.out ngx_testpool.o objs/src/core/ngx_palloc.o objs/src/os/unix/ngx_alloc.o
shc@shc-virtual-machine:~/code/nginx/nginx-1.12.2$ ./ngx_testpool.out
self_handler_02
free ptr mem!
self_handler
free ptr mem!
free ptr mem!
close file!
内存对齐好处
- 较少cpu的IO次数
- 内存对齐博客待完善
gdb调试
- gcc -g 生成的可执行文件可以调试
- gdb …out
- start 开始一行一行运行
- n 下一行
- s(step)进入函数
- finish 跳出函数
- quit 退出
- l 当前附近代码
轮子-nginx内存池不足(我认为)
- 链表管理
- 链表的查找遍历时间复杂度是 O(n)。ngx_pfree 效率不高
- 小内存块链表,current 问题:
- 当遇到密集地分配比较大的小内存场景时,导致已分配结点,分配失败,failed 次数增加。current 指向新的结点,由于是单向链表,前面的结点其实还有足够的空闲空间分配给其它小内存的,导致空闲空间利用率不高。
- ngx_reset_pool中没有释放外部资源。需要等到destroy时才可以释放。
- ngx_reset_pool中,在重置小块内村的last指针时,有一部分没有重置,造成内存碎片。
我?
- 移植之后解决了ngx_reset_pool中没有释放外部资源的问题
- 解决了reset中last指针的问题。