剖析Nginx的内存池源码

1.Nginx内存池概述

Nginx内存池是Nginx为了优化内存管理而引入的一种机制。以下是对Nginx内存池工作原理和底层实现机制的详解:

一、工作原理

  1. 减少频繁的malloc和free操作:通过内存池,Nginx避免了频繁的动态内存申请和释放,从而降低了内存管理的开销。
  2. 防止内存泄漏:内存池可以有效地避免因为申请未释放、二次释放或异常流程未释放而导致的内存泄漏问题。
  3. 提高内存使用效率:内存池可以优化内存的分配和回收,避免内存碎片的产生,从而提高内存的利用率。
  4. 提高系统稳定性:通过内存池,系统能够更好地管理内存资源,减少因为内存问题引发的异常,从而增强系统的稳定性和健壮性。

二、底层实现机制

  1. 内存池结构

    Nginx内存池主要涉及以下几个结构体:

    • ngx_pool_s:作为整个内存池的头信息,只有第一个内存块才有,维护小块内存入口地址、大块内存入口地址、清理函数的入口指针等内存池全局信息。
    • ngx_pool_data_t:是每个内存块都有的,记录本小块内存的信息,包括本块内存的起始可分配位置、末尾可分配位置、下一块的地址链接等。
    • ngx_pool_large_s:保存大块内存的头信息,其保存大块内存的真正起始地址和下一个大块内存的头信息地址,这个头信息自身由于数据量小,也存放在小块内存中。
    • ngx_pool_cleanup_s:保存清理相关操作,在内存池销毁的时候才成链的去调用。
  2. 内存分配与回收

    • 小块内存分配:当外部向内存池申请小块内存分配时,内存池从连续内存空间中划分一部分出去。用户申请后并不需要释放小块内存,而是等待释放内存池时再释放。
    • 大块内存分配:对于大块内存的分配请求,内存池不会直接在内存池上分配内存来满足请求,而是直接向系统申请一块内存(就像直接使用malloc分配内存一样),然后将这块内存挂到内存池头部的large字段下。用户可以调用相关接口进行释放,也可以等内存池释放时再释放。
    • 内存回收:当内存池的生命周期结束时,整个内存池会被销毁,将分配的内存一次性归还给操作系统。
  3. 链式管理

    Nginx内存池通过链式管理大小内存块,实现内存管理。小块内存通过链表进行管理,内存分配过程涉及结点上空闲内存匹配,是链表的遍历。为了提高效率,增加了failed分配内存失败次数统计。大块内存由单向链表管理,没有复杂的空闲内存管理逻辑。

  4. 回调函数

    Nginx内存池支持增加回调函数,当内存池释放时,自动调用回调函数释放用户申请的资源。这方便开发者进行部分具体的业务处理。

  5. 内存对齐

    Nginx内存池在分配内存时,会进行内存对齐操作,这有利于提高CPU读数据效率,是高性能系统不可缺少的一环。

三、重要函数接口

  • ngx_create_pool:创建内存池。
  • ngx_destroy_pool:销毁内存池,释放所有内存,包括pool、large、cleanup链表。
  • ngx_reset_pool:重置内存池。
  • ngx_palloc:内存分配函数,支持内存对齐。
  • ngx_pnalloc:内存分配函数,不支持内存对齐。
  • ngx_pcalloc:内存分配函数,支持内存初始化0。
  • ngx_pfree:内存释放函数(大块内存)。
  • ngx_pool_cleanup_add:添加清理回调函数。

综上所述,Nginx内存池通过一系列精妙的设计和优化措施,为Nginx提供了高效、稳定的内存管理机制,为项目开发和服务器运行提供了更加可靠的基础。

2.内存池重要类型定义

宏定义

1
2
3
4
5
6
7
8
9
10
11
12
/*能从内存池分配到的最大内存 4096-1=4095(4k) 就是32位操作系统一个页面的大小
这是nginx内存池区分大小块内存的一个分界线,比SGI STL内存池的128字节要大得多,在SGI STL中,只要比128字节大就去调用malloc而不是内存池
*/
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
//默认的内存池的大小最大为 16k
#define NGX_DEFAULT_POOL_SIZE (16 * 1024)
//内存池分配字节对齐的一个数字
#define NGX_POOL_ALIGNMENT 16
//内存池最小的大小 gnx_align是把数字调整到邻近的16的倍数上
#define NGX_MIN_POOL_SIZE \
ngx_align((sizeof(ngx_pool_t) + 2 * sizeof(ngx_pool_large_t)), \
NGX_POOL_ALIGNMENT)

内存池主结构体信息

1
2
3
4
5
6
7
8
9
10
11
12
13
// nginx内存池的主结构体类型
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; // 大块内存分配入口指针

/*清理函数handler的入口指针 用户可以设置回调函数,在内存池释放之前,可以对内存池上的数据进行一个释放操作。相当于析构函数的角色。
*/
ngx_pool_cleanup_t *cleanup;
ngx_log_t *log;//日志模块 研究内存池的时候暂不作考虑
};

小块内存数据头信息

1
2
3
4
5
6
7
8
typedef struct ngx_pool_s ngx_pool_t;
// 小块内存数据头信息
typedef struct {
u_char *last; // 可分配内存开始位置
u_char *end; // 可分配内存末尾位置
ngx_pool_t *next; // 保存下一个内存池的地址
ngx_uint_t failed; // 记录当前内存池分配失败的次数
} ngx_pool_data_t;

大块内存类型定义

1
2
3
4
5
6
typedef struct ngx_pool_large_s ngx_pool_large_t;
// 大块内存类型定义
struct ngx_pool_large_s {
ngx_pool_large_t *next; // 下一个大块内存
void *alloc; // 记录分配的大块内存的起始地址
};

回调函数cleanup结构体

1
2
3
4
5
6
7
8
typedef void (*ngx_pool_cleanup_pt)(void *data); // 清理回调函数的类型定义
typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t;
// 清理操作的类型定义,包括一个清理回调函数,传给回调函数的数据和下一个清理操作的地址
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler; // 清理回调函数
void *data; // 传递给回调函数的指针
ngx_pool_cleanup_t *next; // 指向下一个清理操作
};

3.内存池创建函数代码解读

主要做:

1.开辟内存

2.初始化记录内存池信息的两个结构体

1
2
3
//传入分配的内存大小,log日志模块,日志暂不做讨论 返回的是一个小块内存池入口指针
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
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
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
ngx_pool_t *p;
//根据用户指定的大小开辟内存 根据不同的平台调用不同的API 如果没有指定平台,那调用的就是malloc函数
p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
//申请失败 退出
if (p == NULL) {
return NULL;
}
//last指向的是用户可以使用的内存空间的首地址(除了内存池的头结构体部分的其他内容) end指向的是这块的末尾地址 size是传入的要申请的大小 在图中表示为log末尾到内存池末尾这一块
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 = size - sizeof(ngx_pool_t);
//如果比一个页面小,那就用size,否则就用一个页面的大小
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
2
3
4
5
6
7
8
9
10
11
//通过宏控制是否要字节对齐 两个都没有定义的话,就不会对齐
//alignment是控制字节对齐的,而第二个函数调用的ngx_alloc和alignment无关
#if (NGX_HAVE_POSIX_MEMALIGN || NGX_HAVE_MEMALIGN)

void *ngx_memalign(size_t alignment, size_t size, ngx_log_t *log);

#else

#define ngx_memalign(alignment, size, log) ngx_alloc(size, log)

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//ngx_alloc函数
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;
}

4.申请内存的函数

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
/*三个主要的申请内存函数
第一个考虑字节对齐
第二个不考虑字节对齐
第三个调用第一个,不同的是内存开辟完成以后会全部清零
*/
//1.
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);
}

//2.
void *
ngx_pnalloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)
if (size <= pool->max) {
return ngx_palloc_small(pool, size, 0);
}
#endif

return ngx_palloc_large(pool, size);
}

//3.
void *
ngx_pcalloc(ngx_pool_t *pool, size_t size)
{
void *p;

p = ngx_palloc(pool, size);
if (p) {
ngx_memzero(p, size);
}

return p;
}

主要剖析ngx_palloc函数

1
2
3
4
5
6
7
8
9
10
11
12
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);
}

5.小块内存分配(向内存池申请内存的过程)

1.小块内存分配函数ngx_palloc_small

小块内存分配函数ngx_palloc_small

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
//传入 内存池地址,申请的内存大小,是否考虑内存对齐(1考虑,0不考虑)
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指向传入内存池的起始地址
p = pool->current;

do {
//指向可分配内存的起始地址
m = p->d.last;
//考虑内存对齐
if (align) {
//根据平台调整字节对齐,调整到4或8字节的倍数上去
m = ngx_align_ptr(m, NGX_ALIGNMENT);
}
//目前可分配内存(空闲内存)比要申请的内存要大 就可以分配 然后更新last即空闲内存起始地址
if ((size_t) (p->d.end - m) >= size) {
p->d.last = m + size;

return m;//m是已经分配好的内存空间的首地址 返回这个指针给用户
}

p = p->d.next;//这个内存池大小不够就去下一个,没有下一个内存池的话就会退出循环

} while (p);
//没有分配成功就会进入这个函数
return ngx_palloc_block(pool, size);
}

2.ngx_palloc_block函数

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
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;
/*初始化内存池结构体
从第二块内存池开始 只需要ngx_pool_data_t里面的四个指针来管理,从max到log全都不需要了,存在第一块内存池里面就行了,剩下的只要这四个必要的指针就行
*/
new->d.end = m + psize;
new->d.next = NULL;//这是新创建的,就相当于链表最后一个结点,next域就是nullptr
new->d.failed = 0;

m += sizeof(ngx_pool_data_t);
//字节对齐 调整到4或8的倍数
m = ngx_align_ptr(m, NGX_ALIGNMENT);
new->d.last = m + size;//更新内存池的last指针,指向可用内存空间(空闲内存)的首地址

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;
}

重点解释一下最后这个for循环在做什么事情

1.我们要知道调用本函数传入的pool是内存分配失败以后传入进来,要让我们这个函数新开辟一个内存池来进行内存的分配的

2.所有创建的内存池只有第一个保留所有信息,以后的都只有四根指针

3.ngx_pool_t *current是小块内存池入口指针 每次分配都是从这个指针所指的内存池开始分配的,可以理解为是链表的头结点(先这样理解)

4.ngx_uint_t failed; 记录当前内存池分配失败的次数

5.从头遍历链表所有的节点,因为从current到当前新分配的结点,这一堆全都分配失败了,所以都要++。如果说当前内存池已经分配内存失败超过了4次,那说明当前这个内存池剩下的可用内存太小了

6.然后current指针往后移动,移动到next,以后就再也不用next之前的内存池了,因为太小了,分配也不会成功,下一次自然就从next这个结点开始

7.继续对下一个内存池重复这个动作直到最后一个

看到这里就明白了,一开始传入的current存放的也不一定是这个链表的头结点,可能是链表中间的某个结点

个人觉得施磊老师对小块函数讲解十分到位

6.大块内存分配与释放

1.分配

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
39
//传入起始地址以及想要申请的内存大小
static 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;
}

n = 0;
//先看下面的pfree函数再看这个,遍历大块内存池链表,找到alloc是空的,把新分配的这块给安置到这里
for (large = pool->large; large; large = large->next) {
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->alloc = p;
large->next = pool->large;
pool->large = large;

return p;
}

2.释放

大块内存释放函数ngx_pfree

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ngx_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;
}

遍历内存池链表,哪个和要释放的一样就把哪个释放掉,然后把alloc置为空

7.内存池重置函数和小块内存回收方案

1.内存池重置函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void
ngx_reset_pool(ngx_pool_t *pool)
{
ngx_pool_t *p;
ngx_pool_large_t *l;
//遍历大块内存
for (l = pool->large; l; l = l->next) {
if (l->alloc) {
ngx_free(l->alloc);
}
}
/*遍历小块内存 这个处理的不太好,因为除了第一块结构体是完整的(从开始到log),
剩下的内存池结构体那块都是只有四个指针 这样释放内存相当于多释放了除开四个指针的多余部分。表现就是从第二块开始的内存池有一小部分不能使用,但是可以使用,不会报错。
*/
for (p = pool; p; p = p->d.next) {
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
p->d.failed = 0;
}

pool->current = pool;
pool->chain = NULL;
pool->large = NULL;
}
1
2
3
4
5
6
7
8
9
10
11
//修改后的版本

//处理第一个内存池
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
p->d.failed = 0;

//从第二个开始循环到最后一个 只需要释放那四个指针,即ngx_pool_data_t就行
for (p = pool; p; p = p->d.next) {
p->d.last = (u_char *) p + sizeof(ngx_pool_data_t);
p->d.failed = 0;
}

2.小块内存回收方案

没有提供任何的内存释放函数,实际上,从小块内存的分配方式来看(直接通过last指针偏移来分配内存),它也没办法提供回收函数。

nginx本质:http服务器

是一个短链接的服务器,客户端(浏览器)发起一个request请求,到达nginx服务器以后,处理完成,nginx给客户端返回一个response响应,http服务器就主动断开tcp连接(http 1.1 keep-avlie:60s)http服务器(nginx)返回响应以后,需要等待60s,60s之内客户端又发来请求,重置这个时间,否则60s之内没有客户端发来的响应,nginx就主动断开连接,此时nginx可以调用ngx_reset_pool重置内存池了,等待下一次连接。

总结:一个客户可以配一个内存池,重置就当回收了。只适合这种web http短连接。如果是TCP连接,那比较适合SGI STL空间配置器那种模式。

8.内存池外部资源释放和内存池销毁

1.外部资源释放

1.为什么要注意外部资源的释放

考虑下面这种情况

给结构体data分配内存是内存池里面的的,但是结构体里面的指针却又用malloc函数在堆上开辟了一块内存。这时候释放内存池只是把指针p释放掉了,而堆上的内存没有释放掉,造成了内存泄漏。所以就和类一样需要一个析构函数,先把堆上的内存释放掉。

外部资源不只是堆上的内存,还有文件描述符之类的,这里只是简单说明一下需要一个类似析构函数的东西。

所以我们需要预置一个资源释放的函数(通过回调函数的指针来实现)->充当析构函数的角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//使用nginx的内存池
ngx_create_pool(512, log); 512-sizeof(nginx_pool_s)
ngx palloc(512) ;= >大块内存分配
struct Data
{
char *p;
xxxxxxX
一共需要51字节
};
typedef struct Data stData;

stData *pData = ngx_alloc(512);
pData->p=(char*)malloc(12);
strcpy (pData->p, hello world"):
}:

2.ngx_pool_cleanup_t * cleanup 的作用

1
2
3
4
5
6
7
8
9
10
typedef void (*ngx_pool_cleanup_pt)(void *data);

typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t;

//把回调函数相关的内容用结构体存储起来 同样也存储在小块内存的内存池上面
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler;
void *data;
ngx_pool_cleanup_t *next;
};

3.cleanup_add函数

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
ngx_pool_cleanup_t *
ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
{
ngx_pool_cleanup_t *c;
//把结构体存储在小块内存内存池
c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
if (c == NULL) {
return NULL;
}
//如果预置的回调函数不需要参数,那就写0,如果有参数就写需要的内存大小,然后再开辟相应的内存,然后data指向它,如果不需要参数那就是nullptr
if (size) {
c->data = ngx_palloc(p, size);
if (c->data == NULL) {
return NULL;
}

} else {
c->data = NULL;
}
//暂时置为空,还没有具体的值
c->handler = NULL;
//链表的连接动作,把新创建的cleanup连接到链表上(next指向的是下一个清理操作)
c->next = p->cleanup;

p->cleanup = c;

ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);

return c;
}

4.cleanup举例

1
2
3
4
5
6
7
8
9
10
11
//回调函数
void release(void *p)
{
//释放外部资源操作举例
free(p);
}

//设置回调函数,传入参数大小为一个指针大小 这个参数在小块内存池存放着
ngx_pool_cleanup_t *pclean=ngx_pool_cleanup_add(poo1,sizeof(char*));
pclean->handler = &release://绑定函数
pclean->data =pData->p;

2.内存池销毁

1.释放外部资源->handler回调函数

2.释放大块内存池

3.释放小块内存池

因为外部资源清理相关的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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
void
ngx_destroy_pool(ngx_pool_t *pool)
{
ngx_pool_t *p, *n;
ngx_pool_large_t *l;
ngx_pool_cleanup_t *c;
//1.回调函数不为空先调回调函数 这样就把外部资源释放掉了
for (c = pool->cleanup; c; c = c->next) {
if (c->handler) {
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
"run cleanup: %p", c);
c->handler(c->data);
}
}

#if (NGX_DEBUG)

/*
* we could allocate the pool->log from this pool
* so we cannot use this log while free()ing the pool
*/

for (l = pool->large; l; l = l->next) {
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
}

for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
"free: %p, unused: %uz", p, p->d.end - p->d.last);

if (n == NULL) {
break;
}
}

#endif
//2.大块
for (l = pool->large; l; l = l->next) {
if (l->alloc) {
ngx_free(l->alloc);
}
}
//3.小块
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
ngx_free(p);

if (n == NULL) {
break;
}
}
}

9.nginx源码编译测试内存池接口函数功能

在Ubuntu环境下测试

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
#include <ngx_config.h>
#include <nginx.h>
#include <ngx_core.h>
#include <ngx_palloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//日志文件
void 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;
};
//两个回调函数 堆内存资源和文件资源
void func1(char *p)
{
printf("free ptr mem!");
free(p);
}
void func2(FILE *pf)
{
printf("close file!");
fclose(pf);
}
void main()
{
// 最大值设置为512
ngx_pool_t *pool = ngx_create_pool(512, NULL);
if(pool == NULL)
{
printf("ngx_create_pool fail...");
return;
}

void *p1 = ngx_palloc(pool, 128); // 从小块内存池分配的
if(p1 == NULL)
{
printf("ngx_palloc 128 bytes fail...");
return;
}

stData *p2 = ngx_palloc(pool, 512); // 从大块内存池分配的
if(p2 == NULL)
{
printf("ngx_palloc 512 bytes fail...");
return;
}
p2->ptr = malloc(12);
strcpy(p2->ptr, "hello world");
p2->pfile = fopen("data.txt", "w");

ngx_pool_cleanup_t *c1 = ngx_pool_cleanup_add(pool, sizeof(char*));
c1->handler = func1;
c1->data = p2->ptr;

ngx_pool_cleanup_t *c2 = ngx_pool_cleanup_add(pool, sizeof(FILE*));
c2->handler = func2;
c2->data = p2->pfile;

ngx_destroy_pool(pool); // 1.调用所有的预置的清理函数 2.释放大块内存 3.释放小块内存池所有内存

return;
}

编译链接操作

1
2
3
4
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


gcc -o ngx_testpool ngx_testpool.o objs/src/core/ngx_palloc.o objs/src/os/unix/ngx_alloc.o

10.Nginx内存池总结

1.指针相关重点

current指针确定分配的内存池是哪个

last指针确定分配的内存在内存池的起始地址

next把内存池连接成为链表

2.小块的分配与回收

小块内存池依靠last偏移分配内存

小块内存池没有专门的回收函数

3.销毁内存池时的顺序

外部资源-大块内存池-小块内存池

11.Nginx和SGI STL空间配置器对比

Nginx 内存池和 SGI STL 内存池的设计和使用目标有所不同,各自具备特定的内存管理特点。以下是它们的异同点。

一、Nginx 内存池与 SGI STL 内存池的相同点

  1. 减少内存分配和释放的开销:两者都通过内存池来减少频繁的 mallocfree 操作,提升内存管理效率。
  2. 提供固定大小的内存块分配:它们在分配小内存块时都能够提升分配效率并降低碎片化。
  3. 适合小对象的分配:两个内存池都更适合频繁分配、释放的小对象,这些对象的生命周期通常较短。

二、Nginx 内存池与 SGI STL 内存池的不同点

  1. 设计目的
    • Nginx 内存池:主要用于高并发服务器中的短生命周期对象管理。Nginx 内存池更适合在 HTTP 请求处理过程中管理频繁生成和销毁的短生命周期对象。
    • SGI STL 内存池:设计用于 C++ STL 容器的内存管理,为容器对象提供更高效的内存分配,支持小对象和灵活的内存回收,适合长生命周期的容器操作。
  2. 实现方式
    • Nginx 内存池:分配一个大块内存后,按需划分小块,并不支持回收,整个内存池在使用完后一次性释放。
    • SGI STL 内存池:使用自由链表管理小块内存,动态复用并支持按大小分组,回收时更高效。
  3. 内存碎片管理
    • Nginx 内存池:通过单一大块内存的连续分配来减少碎片,管理更简单。
    • SGI STL 内存池:使用分组管理,减少小块分配带来的碎片,但碎片化比 Nginx 内存池相对严重。
  4. 灵活性
    • Nginx 内存池:固定分配模式,灵活性较低,不适合频繁变化的内存需求。
    • SGI STL 内存池:分配灵活,适用于各种对象大小的动态分配和回收管理。

三、各自的优势与劣势

1. Nginx 内存池

  • 优势
    • 高效管理短生命周期内存:不需要逐个释放对象,适合短生命周期数据块管理,减少了频繁的 malloc/free 调用。
    • 减少碎片:通过集中管理内存分配,降低了内存碎片产生的概率。
    • 实现简单:内存池结构简单,分配和释放速度较快。
  • 劣势
    • 灵活性低:固定大小的内存池管理方式对动态内存需求变化适应性较差。
    • 内存浪费:池内内存块大小固定,可能导致某些情况下的内存浪费。
    • 不适合长生命周期对象:适合短生命周期对象,不适合需要长期驻留的对象管理。

2. SGI STL 内存池

  • 优势
    • 灵活:适用于不同大小的内存块管理,具有更强的灵活性。
    • 高效内存复用:对小块内存使用自由链表进行回收,降低了 malloc 的调用次数。
    • 适用于容器的内存分配:尤其对频繁插入、删除的小对象有较好表现。
  • 劣势
    • 实现复杂:相比 Nginx 内存池实现更为复杂。
    • 内存碎片问题:虽然通过分组策略减少了碎片,但不如 Nginx 内存池集中管理彻底。
    • 适用性较窄:更适用于 STL 容器内部的内存管理,对大块内存管理不够高效。

四、适用场景

1. Nginx 内存池

Nginx 内存池非常适合在高并发场景中使用,特别是对于具有短生命周期的大量对象。这类场景包括:

  • Web 服务器:如 Nginx,频繁处理短时间内生成和销毁的 HTTP 请求数据。
  • 数据缓存:当数据缓存是短生命周期且访问频繁时,可有效减少内存分配开销。
  • 其他高并发短生命周期场景:如负载均衡器、反向代理等。

2. SGI STL 内存池

SGI STL 内存池通常用于 C++ 标准容器(如 vectorlist 等)的内存管理。适用的场景包括:

  • 通用 C++ 应用:任何需要使用 STL 容器且频繁创建销毁小对象的场景。
  • 图算法、树形结构:这种算法中对象较多且较小,适合 STL 内存池的管理方式。
  • 嵌入式系统:在资源受限的环境中使用容器管理小对象,可以降低内存开销和碎片。