内存池是在真正使用内存之前,预先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够用时,再继续申请新的内存。
内存池的好处有减少向系统申请和释放内存的时间开销,解决内存频繁分配产生的碎片,提示程序性能,减少程序员在编写代码中对内存的关注等
目前一些常见的内存池实现方案有STL中的内存分配区,boost中的object_pool
,nginx中的ngx_pool_t
,google的开源项目TCMalloc等。
为了自身使用的方便,Nginx封装了很多有用的数据结构,比如ngx_str_t ,ngx_array_t, ngx_pool_t 等等,对于内存池,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))
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; // 内存池块的类型 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; // 日志 };
// 根据size进行内存开辟 ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log){ ngx_pool_t *p; // 根据系统平台定义的宏以及用户执行的size,调用不同平台的API开辟内存池 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, 4096),max指的是除开头信息以外的内存块的大小 p->max = (sizecurrent = p; // 指向可用于分配空间的内存块的起始地址 p->chain = NULL; p->large = NULL; // 小块内存直接在内存块开辟,大块内存在large指向的内存开辟 p->cleanup = NULL; p->log = log; return p; }
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; }
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; // 从第一个内存块的current指针指向的内存池进行分配 p = pool->current; do { m = p->d.last; // m指向可分配内存的起始地址 if (align) { // 把m调整为NGX_ALIGNMENT整数倍 m = ngx_align_ptr(m, NGX_ALIGNMENT); } // 内存池分配内存的核心代码 if ((size_t) (p->d.end - m) >= size) { // 若可分配空间 >= 申请的空间 // 偏移d.last指针,记录空闲空间的首地址 p->d.last = m + size; return m; } // 当前内存块的空闲空间不够分配,若有下一个内存块则转向下一个内存块 // 若没有,p会被置空,退出while p = p->d.next; } while (p); return ngx_palloc_block(pool, size); }
当前内存池的块足够分配:
当前内存池的块不够分配: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开始分配空间,再次分配的时候可以不用遍历前面的内存块 if (p->d.failed++ > 4) { pool->current = p->d.next; } } p->d.next = new; // 连接可分配空间的首个内存块 和 新开辟的内存块 return m; }
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次后,若还没有找到被释放过的大块内存对应的信息 // 为了提高效率,直接在小块内存中申请空间保存大块内存的信息 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; }
大块内存的释放
// 释放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; }
就用了last和end两个指着标识空闲的空间,是无法将已经使用的空间合理归还到内存池的,只是会重置内存池。同时还存储了指向大内存块large和清理函数cleanup的头信息
考虑到nginx的效率,小块内存分配高效,同时也不回收内存
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); } } // 遍历小块内存的链表,重置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; } // current指向可用于分配内存的内存块 pool->current = pool; pool->chain = NULL; pool->large = NULL; }
nginx本质是http服务器,通常处理的是短链接,间接性提供服务,需要的内存不大,所以不回收内存,重置即可。
客户端发起一个requests请求后,nginx服务器收到请求会返回response响应,若在keep-alive时间内没有收到客户的再次请求,nginx服务器会主动断开连接,此时会reset内存池。下一次客户端请求再到来时,可以复用内存池。
如果是处理长链接,只要客户端还在线,服务器的资源就无法释放,直到系统资源耗尽。长链接一般使用SGI STL内存池的方式进行内存的开辟和释放,而这种方式分配和回收空间的效率就比nginx低
假设如下情况:
// 假设内存对齐为4B typedef struct{ char* p; char data[508]; }stData; ngx_pool_t *pool = ngx_create_pool(512, log); // 创建一个总空间为512B的nginx内存块 stData* data_ptr = ngx_alloc(512); // 因为可用的实际内存大小为:512-sizeof(ngx_pool_t),所以属于大内存开辟 data_ptr->p = malloc(10); // p指向外界堆内存,类似于C++对象中对用占用了外部资源
当回收大块内存的时候,调用ngx_free,就会导致内存泄漏
以上内存泄漏的问题,可以通过回调函数进行内存释放(通过函数指针实现)typedef void (*ngx_pool_cleanup_pt)(void *data); typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t; // 以下结构体由ngx_pool_s.cleanup指向,也是存放在内存池的小块内存 struct ngx_pool_cleanup_s { ngx_pool_cleanup_pt handler; void *data; // 指向需要释放的资源 ngx_pool_cleanup_t *next; // 释放资源的函数都放在一个链表,用next指向这个链表 };
nginx提供的函数接口:
// p表示内存池的入口地址,size表示p->cleanup->data指针的大小 // p->cleanup指向含有清理函数信息的结构体 // ngx_pool_cleanup_add返回 含有清理函数信息的结构体 的指针 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申请size的空间 c->data = ngx_palloc(p, size); if (c->data == NULL) { return NULL; } } else { c->data = NULL; } c->handler = NULL; // 采用头插法,将当前结构体串在pool->cleanup后 c->next = p->cleanup; p->cleanup = c; ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c); return c; }
使用方式:
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; } } }
执行configure
生成Makefile文件(若报错则表示需要apt安装软件)
Makefile如下:
执行make命令使用Makefile编译源码,在相应目录下生成 .o
文件
#include由于#include #include #include #include #include #include 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!\n"); free(p); } void func2(FILE *pf){ printf("close file!\n"); fclose(pf); } 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 = 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; }
ngx_pool_cleanup_add
中用头插法将创建的清理块链入pool->cleanup
,所以ngx_destroy_pool
的时候先清理文件后清理堆内存。
相关测试代码推送到:https://github.com/BugMaker-shen/nginx_sgistl_pool
到此这篇关于nginx内存池源码解析的文章就介绍到这了,更多相关nginx内存池内容请搜索