Nginx 内存池源码剖析

大纲

前言

本文将剖析 Nginx 内存池的源码,并介绍内存池的底层设计和工作原理,最后基于 C++ 移植 Nginx 内存池的核心源码。

C++ 常见的池

在 C++ 中有五大池,包括内存池、连接池、协程池、线程池、进程池。

内存管理概述

内存管理,是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效、快速地分配内存,并且在适当的时候释放和回收内存资源。

源码学习目标

在剖析 Nginx 内存管理的源码之前,先思考以下几个问题:

  • (1) Nginx 为什么要进行内存管理?
  • (2) Nginx 如何进行内存管理?
  • (3) Nginx 的内存管理解决了哪些问题?

Nginx 内存池

内存池的源码剖析

这里剖析的 Nginx 版本是 1.12.2,Nginx 内存池的核心源码主要位于 ngx_palloc.hngx_palloc.c 源文件中。

重要类型和变量的定义

  • Nginx 内存池的粒度信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// NGX_MAX_ALLOC_FROM_POOL should be (ngx_pagesize - 1), i.e. 4095 on x86. On Windows NT it decreases a number of locked pages in a kernel.
// 小块内存池单次分配的最大内存大小,不能超过 ngx_pagesize - 1,超过这个大小的内存不会从内存池中分配,而是通过 malloc() 之类的方式单独分配内存。
//ngx_pagesize 是系统页面大小(例如 x86 平台是 4096 字节),减 1 是为了确保对齐且不会跨页,最终小块内存池单次分配的最大内存大小为 4095 字节(x86 平台)。
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)

// 内存池的默认初始大小为 16KB
#define NGX_DEFAULT_POOL_SIZE (16 * 1024)

// 内存池分配时的对齐字节数为 16 字节
#define NGX_POOL_ALIGNMENT 16

// 小块内存的最小可接受大小
#define NGX_MIN_POOL_SIZE ngx_align ((sizeof (ngx_pool_t) + 2 * sizeof (ngx_pool_large_t)), NGX_POOL_ALIGNMENT)

// 小块内存分配时的对齐字节数,通常是 4 字节(32 位系统)或者 8 字节(64 位系统)
#define NGX_ALIGNMENT sizeof (unsigned long)
  • Nginx 内存池的主结构体类型
1
2
3
4
5
6
7
8
9
10
11
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_chain_t 结构
ngx_pool_large_t *large; // 大块内存分配入口指针,大块内存分配直接采用标准系统接口 malloc()
ngx_pool_cleanup_t *cleanup; // 清理函数 handler 的入口指针,用于指定内存释放时需要清理资源的一些必要操作
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; // 记录当前内存池分配内存失败的次数,当失败次数大于指定值时,current 指向下一内存池单元
} 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; // 记录分配的大块内存的起始地址
};
  • 清理操作的类型定义,包括一个清理回调函数、传给回调函数的数据和下一个清理操作的地址
1
2
3
4
5
6
7
8
9
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; // 指向下一个清理操作
};

重要的内存池函数接口

1
2
3
4
5
ngx_pool_t *ngx_create_pool (size_t size, ngx_log_t *log);   // 创建内存池

void ngx_destroy_pool (ngx_pool_t *pool); // 销毁内存池

void ngx_reset_pool (ngx_pool_t *pool); // 重置内存池

内存池管理核心函数

1
2
3
4
5
6
7
static ngx_inline void * ngx_palloc_small (ngx_pool_t *pool, size_t size, ngx_uint_t align)   // 内存池分配小块内存的入口函数

static void * ngx_palloc_block (ngx_pool_t *pool, size_t size) // 内存池分配新小块内存(在小块内存块空间不足时)

static void * ngx_palloc_large (ngx_pool_t *pool, size_t size) // 内存池分配大块内存的入口函数

ngx_int_t ngx_pfree (ngx_pool_t *pool, void *p) // 内存池释放大块内存的入口函数

内存池的底层设计

Nginx 中所有请求都单独对应一个内存池,在这个请求的过程中,所有涉及到内存分配的地方,都到该请求相关的内存池中处理,而中间不会去释放内存,内存池的生命周期与请求一样,请求完毕则直接回收内存。这样的好处在于:统一分配和统一释放,降低了内存泄露问题的出现。Nginx 的内存池设计分为两个部分:

  • 大块内存:超过 max 大小(默认 4095 字节)的内存分配,走大块内存分配,这部分内存管理由 ngx_pool_large_t 结构体负责。
  • 小块内存:在 ngx_pool_t 链表中遍历符合要求的 ngx_pool_t 结构体,找到符合要求大小的 pool 直接返回,否则就申请新的小块内存 pool

特别注意

  • 小块内存在分配后不会被单独释放,而是在整个内存池销毁(ngx_destroy_pool())时,通过 free() 一次性释放。
  • 大块内存会在整个内存池重置(ngx_reset_pool())或者整个内存池销毁(ngx_destroy_pool())时被 free() 释放,但还可以手动调用 ngx_pfree() 单独释放。
  • 小块内存的重用机制:Nginx 也有机制尝试在链表中查找可用内存块(并非永远只分配不回收),但它不做碎片整理或回收,只是简单地继续分配新的小块内存。

内存池结构设计

Nginx 采用内存池的结构设计来管理内存,而内存池是由若干固定大小的内存块组成的单向链表(如下图所示)。从图中可以看出来,内存池的头结点维护着内存池的总体信息,从头结点开始,可以访问内存池的小块内存(单向链表,由 ngx_pool_data_t 结构特维护),大块内存(单向链表,由 ngx_pool_large_t 结构特维护),以及抽象内存数据(单向链表,由 ngx_pool_chain_t 结构体维护)。

ngx_pool_data_t

ngx_pool_data_t 结构体负责存储每个 ngx_pool_t 结构体的元数据:

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; // 记录当前内存池分配内存失败的次数,当失败次数大于指定值时,current 指向下一内存池单元
} ngx_pool_data_t;

提示

failed 成员的引入是为了避免某个 pool 虽然还有可用的内存空间,但是由于内存空间很小了,导致经常性的分配内存空间失败,当累计失败的次数达到某个阈值时,下一次再次查找内存就直接跳过这个 pool,直接去寻找内存池链表中的下一个 pool。在 ngx_pool_s 结构体中,current 指针会随着 failed 的增加而发生改变,如果 current 指向的内存池的 failed 达到了 4 的话,current 就会指向下一个内存池。

ngx_pool_large_t

ngx_pool_large_t 结构体用于存储大内存块,这一块就比较简单粗暴了,直接调用 malloc() 分配一块大内存来使用,多个大内存块之间也是以链表形式来组织数据。正常情况下,这些大块内存会在整个内存池销毁时统一释放,比如在请求处理完毕后、连接断开后、Worker 进程(子进程)退出后会释放大块内存。

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; // 记录分配的大块内存的起始地址
};

提示

大块内存的分配请求不会直接在内存池上分配内存来满足,而是直接向操作系统申请一大块内存(底层直接调用 malloc() 分配内存),然后将这块内存挂到内存池头部的 large 指针下。内存池的作用在于解决小块内存池的频繁申请问题,对于这种大块内存,是可以忍受直接申请的。为什么大块内存分配后是挂在链表头部而不是尾部呢?根据程序局部性原理,最近分配的内存一般经常使用,挂在头部可以提高空闲内存块的查找效率。

ngx_pool_t

ngx_pool_t 结构体用于表示一个内存池,内存池的内部以链表形式来组织数据。如下图:

1
2
3
4
5
6
7
8
9
10
11
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_chain_t 结构
ngx_pool_large_t *large; // 大块内存分配入口指针,大块内存分配直接采用标准系统接口 malloc()
ngx_pool_cleanup_t *cleanup; // 清理函数 handler 的入口指针,用于指定内存释放时需要清理资源的一些必要操作
ngx_log_t *log; // 内存分配相关的日志记录
};

需要注意的是:

  • 内存池内部以链表形式组织起来的,完成这个工作的就是前面的 ngx_pool_data_t 结构体的 next 成员。
  • current 指针,用于表示当前该内存池在使用的 pool 指针。除了内存池链表的头结点之外,内存池链表其他节点的该指针无效。之所以需要这个指针,就是前面提到的,在某个内存池多次失效的情况下,下一次直接跳过该内存池查找内存空间,current 指针保存当前在内存池链表的哪一个内存池上面查找内存空间。
  • large 指针,指向 ngx_pool_large_t 结构体,用于管理大块内存。

内存池的工作原理

内存池的创建

  • 经过测试,Nginx 会为每个 Http 连接(Connection)创建大小为 256 字节的 pool,为每个请求(Request)创建大小为 4096pool
  • 当客户端使用长连接向服务器请求资源时,Nginx 处理完 Request 后会释放 Request 对应的 pool,但不会立即释放 Connection 对应的 pool,而是等连接超时后再释放。
  • 在一次简单的会话中(比如请求首页),从连接建立到连接关闭,至少有约 30 次的内存分配(调用 ngx_palloc())。

内存分配机制

  • 小块内存的分配

    • 对于小块内存的分配,Nginx 会首先尝试在当前小块内存池中查找是否存在足够的空闲内存空间。
    • 如果当前小块内存池的可用内存不足以满足需求,Nginx 会尝试遍历该小块内存池的链表(即多个小块内存池),寻找可用的内存空间。
    • 如果仍然找不到可用的内存空间,则会创建一个新的小块内存块,并将其添加到该小块内存池链表的尾部。
    • 小块内存的分配是按顺序进行的,不会进行回收,因此分配效率很高,适合频繁分配和释放内存的场景(比如 Web 服务器处理大量短连接)。
  • 大块内存的分配

    • 对于大块内存的分配,当分配的内存大小超过小块内存分配的阈值(一般是 4095 字节,根据平台决定),Nginx 会直接调用 malloc() 申请一块大内存,并将该大块内存封装成一个 ngx_pool_large_t 节点,挂载到内存池的大块内存链表的头部。
    • 这类内存块比较大、分配成本比较高,通常用于缓存大数据或模块中临时需要的较大资源。
    • 由于小块内存使用的是顺序分配模型,而大块内存是从系统申请分配的,因此两者的管理方式不同,但最终都会在内存池销毁时(比如处理完 HTTP 请求、HTTP 连接关闭等)统一释放,确保资源不会泄漏。

内存分配流程

Nginx 从内存池分配内存的流程如下图所示:

内存池的销毁

  • 一个 Web Server 通常会持续不断地接收 Connection 和 Request,因此 Nginx 将内存池划分为不同的层级,包括进程级内存池、Connection 级内存池以及 Request 级内存池。具体来说,当创建一个 Worker 进程时,系统会为该 Worker 分配一个独立的内存池;当有新的 Connection 到来时,又会在该 Worker 的内存池基础上为该 Connection 分配一个新的内存池;当该 Connection 上接收到一个新的 Request 时,再在 Connection 的内存池中为该 Request 创建一个新的内存池。

  • 这种分层次的内存池管理方式,使得在处理完一个 Request 后,可以一次性释放该 Request 的整个内存池;当 Connection 关闭时,可以释放该 Connection 对应的内存池;而当 Worker 进程退出时,其对应的内存池也会整体释放。通过这种机制,确保了内存既有分配,也能及时回收,避免了内存泄漏的问题。

  • 从内存的分配与释放策略可以看出,Nginx 内存池的核心作用在于将多个小块内存的分配操作集中处理,并在适当的时机统一释放,从而避免频繁的小内存申请,降低内存碎片的产生,提升了系统的内存管理效率和性能。

内存池的源码测试

这里将介绍如何编写代码测试 Nginx 内存池的 ngx_destroy_pool() 接口,并编译运行自定义的测代码。

编译 Nginx 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 下载源码
wget https://nginx.org/download/nginx-1.12.2.tar.gz

# 解压源码
tar -xvf nginx-1.12.2.tar.gz

# 进入源码目录
cd nginx-1.12.2

# 生成构建文件
./configure

# 编译源码
make -j4

编写测试代码

在 Nginx 的源码目录下(比如 nginx-1.12.2),创建 C 源文件 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
#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 cleanFunc1 (char *p) {
printf ("free ptr mem!\n");
free (p);
}

// 自定义资源清理操作
void cleanFunc2 (FILE *pf) {
printf ("close file!\n");
fclose (pf);
}

void main () {
// 创建内存池
ngx_pool_t *pool = ngx_create_pool (512, NULL);
if (pool == NULL) {
printf ("ngx_create_pool fail...\n");
return;
}

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

// 从大块内存池分配内存
stData *p2 = ngx_palloc (pool, 512);
if (p2 == NULL) {
printf ("ngx_palloc 512 bytes fail...\n");
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 = cleanFunc1;
c1->data = p2->ptr;

// 添加资源清理操作
ngx_pool_cleanup_t *c2 = ngx_pool_cleanup_add (pool, sizeof (FILE *));
c2->handler = cleanFunc2;
c2->data = p2->pfile;

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

return;
}

运行测试代码

  • 编译测试代码
1
2
3
4
5
6
7
8
9
10
11
# 进入源码目录
cd 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

# 链接测试代码(生成可执行文件)
gcc -o ngx_testpool ngx_testpool.o objs/src/core/ngx_palloc.o objs/src/os/unix/ngx_alloc.o

# 运行可执行文件
./ngx_testpool
  • 测试代码运行的输出结果
1
2
close file!
free ptr mem!

内存池的源码移植

这里将介绍如何基于 C++ 移植 Nginx 内存池的核心源码,其中移植的代码主要位于 Nginx 的 ngx_palloc.hngx_palloc.c 源文件中。

提示

这里移植的 Nginx 版本是 1.12.2,完整的案例代码可以从 这里 下载得到。

核心代码

  • ngx_mem_pool.h 头文件
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
113
114
115
116
117
118
119
120
121
#pragma once

#include <string.h>

// 将数值 d 调整为临近数值 a 的倍数
#define ngx_align (d, a) (((d) + (a - 1)) & ~(a - 1))

// 将数值 p 调整为临近数值 a 的倍数
#define ngx_align_ptr (p, a) (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))

// 将指定的内存全部初始化为 0
#define ngx_memzero (buf, n) (void) memset (buf, 0, n)

// 类型重定义
using u_char = unsigned char;
using ngx_uint_t = unsigned int;

// 类型前置声明
struct ngx_pool_s;

// 清理函数(回调函数)的类型
typedef void (*ngx_pool_cleanup_pt)(void *data);

// 清理操作的头部信息
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler; // 函数指针,指向内存清理操作的回调函数
void *data; // 传递给回调函数的参数
ngx_pool_cleanup_s *next; // 下一个清理函数,最终形成单向链表
};

// 大块内存的内存池的头部信息
struct ngx_pool_large_s {
ngx_pool_large_s *next; // 下一个大块内存,最终形成单向链表
void *alloc; // 记录分配的大块内存的起始地址
};

// 小块内存的内存池的头部信息
struct ngx_pool_data_t {
u_char *last; // 可分配内存的开始位置,即当前内存分配结束位置
u_char *end; // 可分配内存的末尾位置
ngx_pool_s *next; // 链接到下一个内存池,内存池的很多块内存就是通过该指针连接成链表的
ngx_uint_t failed; // 记录当前内存池分配内存失败的次数,当失败次数大于指定值时,current 指向下一内存池单元
};

// 内存池的主结构体
struct ngx_pool_s {
ngx_pool_data_t d; // 小块内存池的头部信息
size_t max; // 小块内存分配的最大空间
ngx_pool_s *current; // 指向内存池中的可用内存块
ngx_pool_large_s *large; // 大块内存分配入口指针,大块内存分配直接采用标准系统接口 malloc()
ngx_pool_cleanup_s *cleanup; // 清理函数 handler 的入口指针,用于指定内存释放时需要清理资源的一些必要操作
};

// 一个物理页面的默认大小(4KB)
const int NGX_PAGESIZE = 4096;
// 小块内存可分配的最大空间
const int NGX_MAX_ALLOC_FROM_POOL = NGX_PAGESIZE - 1;
// 内存池的默认初始大小(16KB)
const int NGX_DEFAULT_POOL_SIZE = 16 * 1024;
// 内存池分配时的对齐字节数(16 字节)
const int NGX_POOL_ALIGNMENT = 16;
// 小块内存的最小可接受大小
const int NGX_MIN_POOL_SIZE = ngx_align ((sizeof (ngx_pool_s) + 2 * sizeof (ngx_pool_large_s)), NGX_POOL_ALIGNMENT);
// 小块内存分配时的对齐字节数,通常是 4 字节(32 位系统)或者 8 字节(64 位系统)
const int NGX_ALIGNMENT = sizeof (unsigned long);

class ngx_mem_pool {

public:
// 构造函数
ngx_mem_pool (int size = NGX_DEFAULT_POOL_SIZE);

// 析构函数
~ngx_mem_pool ();

// 禁止拷贝
ngx_mem_pool (const ngx_mem_pool &pool) = delete;

// 禁止赋值
ngx_mem_pool &operator=(const ngx_mem_pool &) = delete;

public:
// 从内存池中申请指定大小的内存,并考虑内存字节对齐
void *ngx_palloc (size_t size);

// 从内存池中申请指定大小的内存,不考虑内存字节对齐
void *ngx_pnalloc (size_t size);

// 调用 ngx_palloc 实现内存分配,并将内存初始化为 0
void *ngx_pcalloc (size_t size);

// 释放大块内存
void ngx_pfree (void *p);

// 重置内存池
void ngx_reset_pool ();

// 销毁内存池
void ngx_destroy_pool ();

// 添加资源清理操作(回调)函数
ngx_pool_cleanup_s *ngx_pool_cleanup_add (size_t size);

private:
// 创建指定大小的内存池
void *ngx_create_pool (size_t size);

// 分配小块内存
void *ngx_palloc_small (size_t size, ngx_uint_t align);

// 分配大块内存
void *ngx_palloc_large (size_t size);

// 分配新的小块内存
void *ngx_palloc_block (size_t size);

private:
// 指向内存池的入口指针
ngx_pool_s *_pool;

};
  • ngx_mem_pool.cpp 源文件
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
#include<iostream>
#include <stdexcept>
#include "ngx_mem_pool.h"

// 构造函数
ngx_mem_pool::ngx_mem_pool (int size) {
// 创建内存池
this->ngx_create_pool (size);
if (_pool == nullptr) {
throw std::runtime_error ("create memory pool fail...");
}
}

// 析构函数
ngx_mem_pool::~ngx_mem_pool () {
// 销毁内存池
if (_pool != nullptr) {
this->ngx_destroy_pool ();
}
}

// 创建指定大小的内存池
void *ngx_mem_pool::ngx_create_pool (size_t size) {
if (size < NGX_MIN_POOL_SIZE) {
std::cout << "create memory pool fail, pool size too small" << std::endl;
return nullptr;
}

// 分配内存空间
_pool = (ngx_pool_s *) malloc (size);
if (_pool == nullptr) {
return nullptr;
}

// 小块内存池的头部信息
_pool->d.last = (u_char *) _pool + sizeof (ngx_pool_s);
_pool->d.end = (u_char *) _pool + size;
_pool->d.next = nullptr;
_pool->d.failed = 0;

// 小块内存分配的最大空间
size = size - sizeof (ngx_pool_s);
_pool->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

_pool->current = _pool;
_pool->large = nullptr;
_pool->cleanup = nullptr;

return _pool;
}

// 从内存池中申请指定大小的内存,并考虑内存字节对齐
void *ngx_mem_pool::ngx_palloc (size_t size) {
if (size <= this->_pool->max) {
return ngx_palloc_small (size, 1);
}
return ngx_palloc_large (size);
}

// 从内存池中申请指定大小的内存,不考虑内存字节对齐
void *ngx_mem_pool::ngx_pnalloc (size_t size) {
if (size <= this->_pool->max) {
return ngx_palloc_small (size, 0);
}
return ngx_palloc_large (size);
}

// 调用 ngx_palloc 实现内存分配,并将内存初始化为 0
void *ngx_mem_pool::ngx_pcalloc (size_t size) {
void *p;

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

return p;
}

// 分配小块内存
void *ngx_mem_pool::ngx_palloc_small (size_t size, ngx_uint_t align) {
u_char *m;
ngx_pool_s *p;

p = this->_pool->current;

// 遍历小块内存池链表,查找有可用空间的小块内存
do {
m = p->d.last;

if (align) {
m = ngx_align_ptr (m, 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 (size);
}

// 分配大块内存
void *ngx_mem_pool::ngx_palloc_large (size_t size) {
void *p;
ngx_uint_t n;
ngx_pool_large_s *large;

// 分配新的大块内存
p = malloc (size);
if (p == nullptr) {
return nullptr;
}

n = 0;

// 遍历大块内存池链表,将新的大块内存添加到链表中
for (large = this->_pool->large; large; large = large->next) {
if (large->alloc == nullptr) {
large->alloc = p;
return p;
}

if (n++ > 3) {
break;
}
}

large = (ngx_pool_large_s *) ngx_palloc_small (sizeof (ngx_pool_large_s), 1);
if (large == nullptr) {
free (p);
return nullptr;
}

large->alloc = p;
large->next = this->_pool->large;
this->_pool->large = large;

return p;
}

// 分配新的小块内存
void *ngx_mem_pool::ngx_palloc_block (size_t size) {
u_char *m;
size_t psize;
ngx_pool_s *p, *_new;

// 新的小块内存的大小
psize = (size_t) (this->_pool->d.end - (u_char *) this->_pool);

// 分配新的小块内存
m = (u_char *) malloc (psize);
if (m == nullptr) {
return nullptr;
}

_new = (ngx_pool_s *) m;

// 新的小块内存的头部信息
_new->d.end = m + psize;
_new->d.next = nullptr;
_new->d.failed = 0;

m += sizeof (ngx_pool_data_t);
m = ngx_align_ptr (m, NGX_ALIGNMENT);
_new->d.last = m + size;

for (p = this->_pool->current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
this->_pool->current = p->d.next;
}
}

p->d.next = _new;

return m;
}

// 释放大块内存
void ngx_mem_pool::ngx_pfree (void *p) {
ngx_pool_large_s *l;
for (l = this->_pool->large; l; l = l->next) {
if (p == l->alloc) {
free (l->alloc);
l->alloc = nullptr;
return;
}
}
}

// 重置内存池
void ngx_mem_pool::ngx_reset_pool () {
ngx_pool_s *p;
ngx_pool_large_s *l;

// 释放大块内存池链表中的所有内存
for (l = this->_pool->large; l; l = l->next) {
if (l->alloc) {
free (l->alloc);
}
}

// 重置小块内存池链表中的第一块内存,实现后续小块内存的复用
p = this->_pool;
p->d.last = (u_char *) p + sizeof (ngx_pool_s);
p->d.failed = 0;


// 重置小块内存池链表中的第二块到最后一块内存,实现后续小块内存的复用
for (p = p->d.next; p; p = p->d.next) {
p->d.last = (u_char *) p + sizeof (ngx_pool_data_t);
p->d.failed = 0;
}

this->_pool->current = this->_pool;
this->_pool->large = nullptr;
}

// 销毁内存池
void ngx_mem_pool::ngx_destroy_pool () {
ngx_pool_s *p, *n;
ngx_pool_large_s *l;
ngx_pool_cleanup_s *c;

// 调用所有清理操作(回调)函数
for (c = this->_pool->cleanup; c; c = c->next) {
if (c->handler) {
c->handler (c->data);
}
}

// 释放大块内存池链表中的所有内存
for (l = this->_pool->large; l; l = l->next) {
if (l->alloc) {
free (l->alloc);
}
}

// 释放小块内存池链表中的所有内存
for (p = this->_pool, n = this->_pool->d.next; /* void */; p = n, n = n->d.next) {
free (p);
if (n == nullptr) {
break;
}
}
}

// 添加资源清理操作(回调)函数
ngx_pool_cleanup_s *ngx_mem_pool::ngx_pool_cleanup_add (size_t size) {
ngx_pool_cleanup_s *c;

c = (ngx_pool_cleanup_s *) ngx_palloc (sizeof (ngx_pool_cleanup_s));
if (c == nullptr) {
return nullptr;
}

if (size) {
c->data = ngx_palloc (size);
if (c->data == nullptr) {
return nullptr;
}

} else {
c->data = nullptr;
}

c->handler = nullptr;

// 添加到资源清理操作链表中(头插法)
c->next = this->_pool->cleanup;
this->_pool->cleanup = c;

return c;
}

测试代码

  • main.cpp 源文件
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
#include<iostream>
#include<memory>
#include <string.h>
#include "ngx_mem_pool.h"

using namespace std;

// 自定义数据类型
struct Data {
char *ptr;
FILE *pfile;
};

// 自定义资源清理操作
void cleanFunc1 (void *arg) {
char *p = (char *) arg;
cout << "free ptr memory!" << endl;
free (p);
}

// 自定义资源清理操作
void cleanFunc2 (void *arg) {
FILE *p = (FILE *) arg;
cout << "close file!" << endl;
fclose (p);
}

int main () {
// 创建内存池
unique_ptr<ngx_mem_pool> pool (new ngx_mem_pool (256));

// 从小块内存池分配内存
void *p1 = pool->ngx_palloc (128);
if (p1 == nullptr) {
cout << "ngx_palloc 128 bytes fail..." << endl;
return -1;
}

// 从大块内存池分配内存
Data *p2 = (Data *) pool->ngx_palloc (512);
if (p2 == nullptr) {
cout << "ngx_palloc 512 bytes fail..." << endl;
return -1;
}

p2->ptr = (char *) malloc (12);
if (p2->ptr == nullptr) {
cout << "malloc 12 bytes fail..." << endl;
return -1;
}

strcpy (p2->ptr, "hello world");
p2->pfile = fopen ("data.txt", "w");

// 添加资源清理操作
ngx_pool_cleanup_s *c1 = pool->ngx_pool_cleanup_add (sizeof (char *));
c1->handler = cleanFunc1;
c1->data = p2->ptr;

// 添加资源清理操作
ngx_pool_cleanup_s *c2 = pool->ngx_pool_cleanup_add (sizeof (FILE *));
c2->handler = cleanFunc2;
c2->data = p2->pfile;

// 内存池由智能指针管理,程序运行结束后会自动销毁内存池

return 0;
}
  • 程序运行的输出结果:
1
2
close file!
free ptr memory!

FAQ

Nginx 为什么要进行内存管理?

  • Nginx 作为高性能 Web 服务器,在各个环节都必须考虑如何提高性能的问题,比如使用 malloc() 申请内存是很耗时的,这时候就需要优化内存的分配。

  • 在使用系统的 malloc()free() 面临几个问题:Web 服务器对内存的需求有大有小,系统运行久了会产生大量的内存碎片,最终造成内存操作更加耗时直到无可用内存,导致服务器宕机。

Nginx 如何进行内存管理?

  • Nginx 首先将内存池进行分级管理:包括进程级、连接级和请求级三个层次;随后再根据内存使用情况的不同,将内存池细分为三类:小块内存、大块内存以及自定义资源内存。

  • 进程级内存池在通过 Fork() 创建 Worker 子进程时完成初始化。由于 Fork() 会复制父进程的数据段和堆栈段,因此每个子进程拥有独立的内存空间。根据 Web Server 的运行特点,当客户端建立连接时,会在函数 void ngx_event_accept (ngx_event_t *ev) 中创建连接级的内存池;当客户端发起请求时,会在函数 void ngx_http_init_connection (ngx_connection_t *c) 中创建请求级的内存池。

  • 在处理 HTTP 请求的过程中,所有与该请求相关的内存分配操作都在对应连接的内存池中完成。根据实际需要的内存大小及资源类型,Nginx 会采用不同的分配策略:对于较小的内存块,采用顺序分配以提高效率;对于较大的内存块或特殊资源,则使用独立分配方式以便于管理。

  • 当请求处理完成后,请求级的内存池会被整体释放;当连接超时或者断开时,连接级的内存池随之释放;当进程退出时,系统会释放进程占用的全部内存池资源。通过这种分级且分类型的内存管理机制,Nginx 有效提升了内存分配效率,降低了内存碎片率,同时确保了资源的可控释放和系统的高性能运行。

Nginx 的内存管理解决了哪些问题?

  • 简化了内存操作:程序员不必担心何时释放内存,当连接释放时,就回收该连接对应的内存池。
  • 避免了内存碎片:从外部内存碎片来看,采用一次性申请一个内存页,避免了外部内存碎片;从内部内存碎片来看,对大小内存申请分别管理,提高了内存利用率,避免了内部内存碎片。
  • 避免了内存泄露:在同一内存池上进行内存申请和回收,当连接关闭后,不存在没有被回收的内存,即可以避免内存泄漏问题。
  • 提高了内存访问效率:充分利用程序局部性原理,结合内存对齐和内存分页机制,有效提高了 CPU 访存的 Cache 命中率。

参考资料