目录
重要类型与变量定义
/*
* NGX_MAX_ALLOC_FROM_POOL 应为 (ngx_pagesize-1),即 x86 上的 4095。
* 在 Windows NT 上,它会减少内核中锁定页面的数量。
*/
// 从内存池中能申请的最大小内存
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
// 内存池中一个内存块的默认大小
#define NGX_DEFAULT_POOL_SIZE (16 * 1024) // 16kb
// 内存对齐
#define NGX_POOL_ALIGNMENT 16
// 内存对齐函数,将参数向上对齐到最近的NGX_POOL_ALIGNMENT的整数倍
// 如15就会对齐到16,17会对齐到32
#define NGX_MIN_POOL_SIZE \
ngx_align((sizeof(ngx_pool_t) + 2 * sizeof(ngx_pool_large_t)), \
NGX_POOL_ALIGNMENT)
// 内存对齐函数的具体实现
#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))
如果你想了解内存对齐的大致原理,可以参考剖析SGI-STL二级空间配置器中的_S_round_up函数。
// 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; // 分配大块内存的入口指针
ngx_pool_cleanup_t *cleanup; // 清理外部资源的函数handler的入口指针
ngx_log_t *log;
};
// 重命名
typedef struct ngx_pool_s ngx_pool_t;
因为本文只分析nginx的内存池结构,所以不研究chain和log变量的含义和作用。
// 内存块数据头信息,用来管理分配小内存的内存块
typedef struct {
u_char *last; // 可分配内存的起始地址
u_char *end; // 可分配内存的末尾地址
ngx_pool_t *next; // 指向下一个内存块的指针
ngx_uint_t failed; // 记录当前内存块分配内存失败的次数
} ngx_pool_data_t;
typedef u_int uintptr_t;
typedef uintptr_t ngx_uint_t;
typedef struct ngx_pool_large_s ngx_pool_large_t;
// 大内存块头信息
struct ngx_pool_large_s {
ngx_pool_large_t *next; // 指向下一个大内存块头
void *alloc; // 指向大内存的起始位置
};
// 定义了名叫ngx_pool_cleanup_pt的函数指针类型
// 返回值为void,参数为void*
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; // 指向下一个清理操作
};
重要函数接口
创建内存池
// 创建内存池
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); // 开辟size大小的空间
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会根据传入的size大小来开辟内存池(中的第一个内存块),要注意的是这个内存池的管理信息会和这个内存块的头信息一起存储在这个内存块上,所以并不是所有的内存都会用来储存数据。
该函数会调用ngx_memalign来申请空间
/*
* Linux 有 memalign() 或 posix_memalign()。
* Solaris 有 memalign()
* FreeBSD 7.0 有 posix_memalign(),此外还有早期版本的 malloc()
* 在页面边界对齐大于页面大小的分配
*/
#if (NGX_HAVE_POSIX_MEMALIGN || NGX_HAVE_MEMALIGN)
// linux系统调用
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
#if (NGX_HAVE_POSIX_MEMALIGN)
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;
}
#elif (NGX_HAVE_MEMALIGN)
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;
}
#endif
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;
}
这里我们看ngnix在Linux平台上的实现。ngx_memalign会根据不同的环境有不同的实现版本,如果满足两个宏中的其中一个,就会调用系统函数memalign() 或 posix_memalign();如果都不满足就会调用自定义函数ngx_alloc,但它的底层也是调用了C++库函数malloc。
成功开辟空间后,会把last指针指向有效存储空间的起始位置,last指针指向末尾位置并初始化其他变量。
内存池的第一个内存块不仅要储存内存块的数据头信息,还要储存内存池的信息,所以第一个内存块的有效空间为第一个内存块的空间(即size)减去sizeof(ngx_pool_t),因为ngx_pool_t中不仅有内存池信息还有内存块信息。
因为成员max是当前内存池分配小内存块的大小上限,所以它的取值应该是内存池(第一个内存块)的有效空间与系统默认的的小块内存的最大字节数(NGX_MAX_ALLOC_FROM_POOL)的最小值,这样才能保证每一个小块内存都能从同一个内存块中分配出来。
随后就是初始化内存池的管理信息,因为当前内存池中只有一个内存块,所以current是指向自己,其他都初始化为空。

内存管理
内存分配
ngnix内存池中有三个内存分配函数,分别是ngx_palloc、ngx_pnalloc和ngx_pcalloc。
// 内存分配函数,支持内存对齐
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);
}
// 内存分配函数,不支持内存对齐
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);
}
// 内存分配函数,支持内存对齐且初始化为0
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来了解一下流程。
参数size是用户需要申请的空间大小,如果size小于等于内存池一次性能分配的最大小内存max,就会调用ngx_palloc_small在内存块中申请小内存,否则就会调用ngx_palloc_large单独申请一块大内存并通过内存池来管理。
申请小内存
ngnix通过ngx_palloc_small申请小内存
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 = pool->current; // 小内存分配的起始内存块
do {
m = p->d.last; // 让m指向空闲空间的起始位置
if (align) { // 如果需要内存对齐
m = ngx_align_ptr(m, NGX_ALIGNMENT); // 把m上调到NGX_ALIGNMENT(16)的整数倍
}
if ((size_t) (p->d.end - m) >= size) { // 如果剩余空间足够分配size大小的内存
p->d.last = m + size; // 调整剩余空间的起始位置
return m;
}
p = p->d.next; // 去下一个内存块找剩余空间
} while (p);
return ngx_palloc_block(pool, size); // 如果所有内存块都没有足够的剩余空间,就开辟一个新的内存块,并把所需空间返回回去
}
参数align是一个表示布尔类型的整数,0表示不需要内存对齐,非0表示需要内存对齐。
变量p会指向小块内存分配的起始位置,之后会根据是否需要内存对齐来把m(即空闲内存的起始位置)上调至16的整数倍。(size_t) (p->d.end - m)计算得到的结果是当前内存块剩余空间的大小,如果当前内存块能够分配出足够的内存,就在当前内存块分配出去,通过调整表示空闲内存起始位置的指针来起到把内存从内存池中划分出去的效果。如果当前内存块没有足够的空间,就会去内存池中下一个用于分配小内存的内存块中去尝试分配目标内存。最后如果所有现存用于小块内存分配的内存块都不满足条件的话,就会向系统申请一块新的内存块用于分配空间。
申请用于分配小块内存的内存块
当现存的所有内存块都无法满足用户的需求时,就会向系统申请一个新的内存块用来分配小内存
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; // 指向开辟的内存块
// 内存池中之后开辟的内存块与第一块内存块不同
// 因为第一块要存储内存池的信息,之后开辟的只需要存储内存块的数据头信息
new->d.end = m + psize;
new->d.next = NULL;
new->d.failed = 0;
// m指向有效内存空间的起始位置
m += sizeof(ngx_pool_data_t);
m = ngx_align_ptr(m, NGX_ALIGNMENT);
new->d.last = m + size;
// 更新内存池中每一个内存块无法分配内存的错误次数
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;
}
内存池中后续开辟的小内存块的大小都和第一次开辟的内存块的大小一致,但是有一个不同的地方是第一块要存储内存池的信息,而之后开辟的只需要存储内存块的数据头信息,所以后续开辟的内存块的有效空间会更大一些。
我们看到在成功开辟内存之后,会通过迭代的方式来增加除最后一个内存块外每一个内存块的失误次数,如果小块内存分配的入口失误次数达到了4次就会将入口将转移到下一个内存块。
注:因为是在更新错误次数后才连接内存块链表,所以每一个内存块能容忍的失误次数其实是5次。

申请大内存
当用户申请的内存大于内存池的max值时,就会调用ngx_palloc_large单独开辟一个大空间提供给用户,并将这个大空间的头信息交付给内存池管理
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;
// 寻找空闲的存储大内存头信息的结构体
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;
}
因为大内存被释放时只会释放其通过ngx_alloc开辟的空间,而它的头信息空间不会被释放且依旧会连接在链表中,所以大内存的头信息空间是可以重复利用的。但为了效率考虑,ngnix在实现时并不会检查所有的头信息空间,而是只检查头三个,如果没有空闲空间就会向内存池申请一个头信息大小的小空间来储存信息。

空间释放
大内存块释放
因为nginx小块内存分配的原理是指针的移动,所以nginx无法对小空间进行释放。因此nginx内存池的设计方案只适用于http服务器这种短连接,间歇性给客户端提供资源的应用场景。

而大内存的释放也十分简单,就只是把大内存块头信息指向的内存块释放,再将指针置空就行了。
// 参数pool是需要释放大内存的内存池
// 参数p是大内存的起始地址
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); // 底层调用的free函数
l->alloc = NULL;
return NGX_OK;
}
}
return NGX_DECLINED;
}
重置内存池
虽然没有单独清理小块内存的接口,但是可以通过清理内存池来实现该功能。
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);
}
}
// 将小块内存块的数据头信息全部初始化达到清理内存的效果
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;
}
不知道你看完源代码之后有没有一个疑问,那就是明明从第二个小内存块开始就只需要存储内存块头信息,但为什么重置小内存块的时候,所有内存块的last指针都要跳过内存池头信息的大小呢?这不是浪费了空间吗?

我个人倾向于是代码实现上出了问题,小内存块的重置应该这样改
// 第一块进行特殊处理
p = pool;
p -> d.last = (u_char*)p + sizeof(ngx_pool_t);
p -> d.failed = 0;
// 从第二块开始
for (p = p -> d.next; p; p = p -> d.next) {
p -> d.next = (u_char*)p + sizeof(ngx_pool_data_t);
p -> d.failed = 0;
}
这样的话就没有空间浪费了。
ngnix的本质是一个http服务器,它是一个短连接的服务器,客户端(浏览器)发起一个request请求,到达ngnix服务区处理完成后,ngnix就会给客户端返回一个response响应,http服务器就主动断开tcp连接(http 1.1 keep-alive 60s)。http服务器(nginx)返回响应之后,需要等待60s,60s之内客户端发来响应的话,就重置这个时间,否则服务器就会主动断开连接,此时ngnix就可以重置内存池了,等待下一次客户端的请求。
销毁内存池
void
ngx_destroy_pool(ngx_pool_t *pool)
{
ngx_pool_t *p, *n;
ngx_pool_large_t *l;
ngx_pool_cleanup_t *c;
// 销毁内存池中的外部资源,如堆内存、文件描述符等
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)
/*
* 我们可以从这个池中分配 pool->log
* 因此在释放池时我们无法使用该日志
*/
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
// 释放大内存块
for (l = pool->large; l; l = l->next) {
if (l->alloc) {
ngx_free(l->alloc);
}
}
// 释放小内存块
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
ngx_free(p);
if (n == NULL) {
break;
}
}
}
释放外部资源
struct Data
{
FILE* file_; // = fopen();
int* array_; // = new int[n];
};
如果我们将Data对象存储在内存池中,那么像file_这种系统资源或array_这种在堆上开辟的内存都算外部资源。因为不管是大内存块的释放还是小内存块的置空都没有对外部资源进行释放,所以如果直接将在小内存上的Data对象进行覆盖,或把Data对象所在的大内存释放,都会导致外部资源泄漏。为了避免这种情况,可以把外部资源托管给内存池,当内存池销毁的时候就是释放外部资源。
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; // 指向下一个清理操作
};
// 把清理外部资源的回调函数交给内存池
// size为回调函数参数的大小
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;
}
// 如果处理函数有参数就给参数分配空间
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; // 返回清理操作结构体让用户自行填写
}
如果你不明白该函数的调用方法可以参考一下以下代码
struct Data {
FILE* file_;
int* array_;
};
void fileHandler(void* fp) {
fclose((FILE*)fp);
fp = nullptr;
}
void arrayHandler(void* p) {
delete[] p;
p = nullptr;
}
int main()
{
// ......
ngnix_pool_t* pool = ngnix_create_pool(1024, log);
Data* p_data = (Data*)ngnix_palloc(pool, sizeof(Data));
p_data->file_ = fopen(".\\readme.txt", "r");
p_data->array_ = new int[20];
ngx_pool_cleanup_t* pclean1 = ngx_pool_cleanup_add(pool, sizeof(FILE*));
pclean->handler = &fileHandler;
pclean->data = p_data->file_;
ngx_pool_cleanup_t* pclean2 = ngx_pool_cleanup_add(pool, sizeof(int*));
pclean->handler = &arrayHandler;
pclean->data = p_data->array_;
// ......
return 0;
}

8万+

被折叠的 条评论
为什么被折叠?



