nginx内存池的剖析
一、nginx数据结构
// SGI STL小块和大块内存的分界点:128B
// nginx(给HTTP服务器所有的模块分配内存)小块和大块内存的分界点:4096B
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)
// 内存池默认大小
#define NGX_DEFAULT_POOL_SIZE (16 * 1024)
// 内存池字节对齐,SGI STL对其是8B
#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)
// 将开辟的内存调整到16的整数倍
#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))
00000000 00000000 00000000 00001000 a
00000000 00000000 00000000 00000111 a-1
11111111 11111111 11111111 11111000 ~a-1
d的区间:
1-16 ==> 16
17-32 ==> 32
...
// 小块内存数据头信息
typedef struct {
u_char *last; // 可分配内存开始位置
u_char *end; // 可分配内存末尾位置
ngx_pool_t *next; // 指向下一个内存块
ngx_uint_t failed; // 当前内存块分配空间失败的次数
} ngx_pool_data_t;
// 内存池的头信息
struct ngx_pool_s {
ngx_pool_data_t d; // 内存池块头信息
size_t max;
ngx_pool_t *current; // 指向可用于分配空间的内存块(failed < 4)的起始地址
ngx_chain_t *chain; // 连接所有的内存池块
ngx_pool_large_t *large; // 大块内存的入口指针
ngx_pool_cleanup_t *cleanup; // 内存池块的清理操作,用户可设置回调函数,在内存池块释放之前执行清理操作
ngx_log_t *log; // 日志
};
typedef struct ngx_pool_s ngx_pool_t;
// 大块内存类型定义
struct ngx_pool_large_s {
ngx_pool_large_t *next; // 下一个大块内存
void *alloc; // 记录分配的大块内存的起始地址
};
二nginx内存池重要函数接口
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); // 重置内存池
void *ngx_palloc(ngx_pool_t *pool, size_t size); // 内存分配函数,支持内存对齐
void *ngx_pnalloc(ngx_pool_t *pool, size_t size); // 内存分配函数,不支持内存对齐
void *ngx_pcalloc(ngx_pool_t *pool, size_t size); // 内存分配函数,支持内存初始化0
ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p // 内存释放(大块内存)
ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size); // 添加清理handler
三、申请内存分配
#define ngx_memalign(alignment, size, log) ngx_alloc(size, log)
void* ngx_alloc(size_t size, ngx_log_t *log){
void *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;
}
// 根据size进行内存开辟
ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log){
ngx_pool_t *p;
// 根据系统平台定义的宏以及用户执行的size,调用不同平台的API开辟内存池,malloc
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; // 指向下一个内存块,当前刚申请内存块,所以置空
p->d.failed = 0; // 内存块是否开辟成功
size = size - sizeof(ngx_pool_t); // 能使用的空间 = 总空间 - 头信息
// 指定的大小若大于一个页面就用一个页面,否则用指定的大小
// 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; // 小块内存直接在内存块开辟,大块内存在large指向的内存开辟
p->cleanup = NULL;
p->log = log;
return p;
}
申请的内存不大于4095则调用ngx_palloc_small,分配小块内存。
否则size>4095调用ngx_palloc_large=>ngx_alloc=>malloc,申请大块内存。
void* ngx_palloc(ngx_pool_t *pool, size_t size){
#if !(NGX_DEBUG_PALLOC)
if (size <= pool->max) {
// 当前分配的空间小于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);
}
void* ngx_pcalloc(ngx_pool_t *pool, size_t size){
void *p;
p = ngx_palloc(pool, size); // 考虑内存对齐
if (p) {
ngx_memzero(p, size); // 可以初始化内存为0
}
return p;
}
当前内存池的块不够分配
- 开辟新的内存块,修改新内存块头信息的last、end、next、failed
- 前面所有内存块的failed++,超过四次就表示该内存块的剩余空间很小了,不能再分配空间了,后序遍历直接跳过这块内存
- 连接新的内存块以及前面的内存块
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);
// 将psize对齐为NGX_POOL_ALIGNMENT的整数倍后,向OS申请空间
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; // 下一块内存的地址为NULL
new->d.failed = 0; // 当前内存块分配空间失败的次数
// 指向头信息的尾部,而max,current、chain等只在第一个内存块有
m += sizeof(ngx_pool_data_t);
m = ngx_align_ptr(m, NGX_ALIGNMENT);
new->d.last = m + size; // last指向当前块空闲空间的起始地址
// 由于每次都是从pool->current开始分配空间
// 若执行到这里,除了new所属的这个内存块分配成功,前面的内存块全部分配失败
for (p = pool->current; p->d.next != NULL; p = p->d.next) {
// 对所有的内存块的failed都++,直到该内存块分配失败的次数大于4了,就表示该内存块的剩余空间很小了,不能再分配空间了
// 就修改current指针,下次从current开始分配空间,再次分配的时候可以不用遍历current前面的内存块
if (p->d.failed++ > 4) {
pool->current = p->d.next;
}
}
p->d.next = new; // 连接可分配空间的首个内存块 和 新开辟的内存块
return m;
}
当前内存池的块足够分配:
由上面可以看出ngx_palloc_small :带有内存对齐的(把分配出去的起始地址对齐)小块内存池的分配,分配效率高,只做了指针的偏移
#define ngx_align_ptr(p, a) (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
四、大块内存的分配与释放ngx_palloc_large
通过ngx_alloc,即malloc分配大块内存,通过ngx_palloc_small在小块内存上存放大块内存头信息,大块内存头信息包含* next和* alloc,分别表示下一个大块内存头信息和大块内存的起始地址
在小块内存上开辟大块内存头信息时,也不是直接就在小块内存上重新申请新的空间,而是查看前4个内存块,是否有已经被释放过的大块内存的头信息,进行复用。前4个无法复用,则直接使用ngx_palloc_small申请新的空间
并不是每次分配大块内存都重新在小块内存上分配头信息,会遍历存储大块内存头信息的链表,查找已经释放的大块内存
遍历4次后,若还没有找到被释放过的大块内存对应的信息(只查看前4个内存块,由于是头插法,前4个内存块是不断改变的 ,为了提高效率,直接在小块内存中申请空间保存大块内存的信息
typedef struct ngx_pool_large_s ngx_pool_large_t;
struct ngx_pool_large_s {
ngx_pool_large_t *next; // 下一个大块内存的起始地址
void *alloc; // 大块内存的起始地址
};
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;
// 并不是每次分配大块内存都重新在小块内存上分配头信息,会遍历存储大块内存头信息的链表,查找已经释放的大块内存
// for循环遍历存储大块内存信息的链表
for (large = pool->large; large; large = large->next) {
if (large->alloc == NULL) {
// 当大块内存被ngx_pfree时,alloc为NULL
// 遍历链表,若大块内存的首地址为空,则把当前malloc的内存地址写入alloc
large->alloc = p;
return p;
}
// 遍历4次后,若还没有找到被释放过的大块内存对应的信息(只查看前4个内存块,由于是头插法,前4个内存块是不断改变的)
// 为了提高效率,直接在小块内存中申请空间保存大块内存的信息
if (n++ > 3) {
break;
}
}
// 通过指针偏移在小块内存池上分配存放大块内存头信息*next和*alloc的空间
large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
if (large == NULL) {
// 如果在小块内存上分配存储*next和*alloc空间失败,则无法记录大块内存头信息
// 释放大块内存p
ngx_free(p);
return NULL;
}
large->alloc = p; // alloc指向大块内存的首地址
large->next = pool->large; // 这两句采用头插法,将新内存块的记录信息存放于以large为头结点的链表中
pool->large = large;
return p;
}
五、资源释放
小块内存的释放:
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释放内存池的大块内存
ngx_free(l->alloc);
}
}
// 遍历小块内存的链表,重置last、failed、current、chain、large等管理信息
for (p = pool; p; p = p->d.next) {
// 由于只有第一个内存块有除了ngx_pool_data_t以外的管理信息,别的内存块只有ngx_pool_data_t的信息
// 不会出错,但是会浪费空间
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
p->d.failed = 0;
}
/*
正确写法如下:
pool->d.last = (u_char *) pool + sizeof(ngx_pool_t); // 第一个内存块的last指针偏移ngx_pool_t
pool->d.failed = 0;
for (p = pool->d.next; p; p = p->d.next) {
// 由于只有第一个内存块有除了ngx_pool_data_t以外的管理信息,别的内存块只有ngx_pool_data_t的信息
// 不会出错,但是会浪费空间
p->d.last = (u_char *) p + sizeof(ngx_pool_data_t); // 后续所有内存块的last指针偏移ngx_pool_data_t
p->d.failed = 0;
}
*/
// current指向可用于分配内存的内存块
pool->current = pool;
pool->chain = NULL;
pool->large = NULL;
}
大块内存的释放
// 释放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) {
// 遍历存储大块内存信息的链表,找到p对应的大块内存
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; // alloc置空
return NGX_OK;
}
}
return NGX_DECLINED;
}
释放顺序
1.预制的回调函数,把data传给handler
2.遍历large,释放大块内存
3遍历小块内存池,并且清空
六、设置清理外部资源的回调函数:
void release(void* p){
free(p);
}
ngx_pool_cleanup_t* clean_ptr = ngx_clean_cleanup_add(pool, sizeof(char*));
clean_ptr->handler = &release; // 用户设置销毁内存池前需要调用的函数
clean_ptr->data = data_ptr->p; // 用户设置销毁内存池前需要释放的内存的地址
ngx_destroy_pool(pool); // 用户销毁内存池
void ngx_destroy_pool(ngx_pool_t *pool)
{
ngx_pool_t *p, *n;
ngx_pool_large_t *l;
ngx_pool_cleanup_t *c;
// 遍历cleanup链表(存放的时释放前需要调用的函数),可释放外部占用的资源
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);
}
}
// 释放大块内存
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;
}
}
}
七、测试内存池释放外部资源
#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){
free(p);
printf("free ptr mem!\n");
}
void func2(FILE *pf){
fclose(pf);
printf("close file!\n");
}
void main(){
// max = 512 - sizeof(ngx_pool_t)
// 创建总空间为512字节的nginx内存块
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 = (char*)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;
// 1.调用所有的预置的清理函数 2.释放大块内存 3.释放小块内存池所有内存
ngx_destroy_pool(pool);
return;
}