nginx内存池源码解析

目录

  • 内存池概述
  • 一、nginx数据结构
  • 二、nginx向OS申请空间ngx_create_pool
  • 三、nginx向内存池申请空间
  • 四、大块内存的分配与释放
  • 五、关于小块内存不释放
  • 六、销毁和清空内存池
  • 七、编译测试内存池接口功能

内存池概述内存池是在真正使用内存之前,预先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用 。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够用时,再继续申请新的内存 。
内存池的好处有减少向系统申请和释放内存的时间开销,解决内存频繁分配产生的碎片,提示程序性能,减少程序员在编写代码中对内存的关注等
目前一些常见的内存池实现方案有STL中的内存分配区,boost中的object_pool,nginx中的ngx_pool_t,google的开源项目TCMalloc等 。
为了自身使用的方便,Nginx封装了很多有用的数据结构,比如ngx_str_t ,ngx_array_t, ngx_pool_t 等等,对于内存池,nginx设计的十分精炼,值得我们学习,本文重点给大家介绍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_ALIGNMENT16#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_tfailed; // 当前内存块分配空间失败的次数} ngx_pool_data_t;// 内存池块的类型struct ngx_pool_s {ngx_pool_data_td;// 内存池块头信息size_tmax;ngx_pool_t*current;// 指向可用于分配空间的内存块(failed < 4)的起始地址ngx_chain_t*chain;// 连接所有的内存池块ngx_pool_large_t*large;// 大块内存的入口指针ngx_pool_cleanup_t*cleanup;// 内存池块的清理操作,用户可设置回调函数,在内存池块释放之前执行清理操作ngx_log_t*log; // 日志};
nginx内存池源码解析

文章插图

二、nginx向OS申请空间ngx_create_pool// 根据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 = (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;}
nginx内存池源码解析

文章插图

三、nginx向内存池申请空间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);// 考虑内存对齐}#endifreturn 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);// 不考虑内存对齐}#endifreturn 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);}当前内存池的块足够分配:
nginx内存池源码解析

文章插图
当前内存池的块不够分配:
  1. 开辟新的内存块,修改新内存块头信息的last、end、next、failed
  2. 前面所有内存块的failed++
  3. 连接新的内存块以及前面的内存块
static void * ngx_palloc_block(ngx_pool_t *pool, size_t size){u_char*m;size_tpsize;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;// 下一块内存的地址为NULLnew->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;}
nginx内存池源码解析

文章插图

四、大块内存的分配与释放typedef struct ngx_pool_large_sngx_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_tn;ngx_pool_large_t*large;// 调用的就是mallocp = 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的内存地址写入alloclarge->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;}
nginx内存池源码解析

文章插图
大块内存的释放
// 释放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);// freel->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低
六、销毁和清空内存池假设如下情况:
// 假设内存对齐为4Btypedef 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,就会导致内存泄漏
nginx内存池源码解析

文章插图
以上内存泄漏的问题,可以通过回调函数进行内存释放(通过函数指针实现)
typedef void (*ngx_pool_cleanup_pt)(void *data);typedef struct ngx_pool_cleanup_sngx_pool_cleanup_t;// 以下结构体由ngx_pool_s.cleanup指向,也是存放在内存池的小块内存struct ngx_pool_cleanup_s {ngx_pool_cleanup_pthandler;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 = https://tazarkount.com/read/ngx_palloc(p, size); if (c->data =https://tazarkount.com/read/= NULL) {return NULL; }} else { c->data = https://tazarkount.com/read/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 = https://tazarkount.com/read/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; }}}
nginx内存池源码解析

文章插图
执行configure生成Makefile文件(若报错则表示需要apt安装软件)
nginx内存池源码解析

文章插图
Makefile如下:
nginx内存池源码解析

文章插图
执行make命令使用Makefile编译源码,在相应目录下生成 .o文件
nginx内存池源码解析

文章插图
#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 = https://tazarkount.com/read/p2->ptr;// 设置资源地址ngx_pool_cleanup_t *c2 = ngx_pool_cleanup_add(pool, sizeof(FILE*));c2->handler = func2;c2->data = https://tazarkount.com/read/p2->pfile;// 1.调用所有的预置的清理函数 2.释放大块内存 3.释放小块内存池所有内存ngx_destroy_pool(pool);return;}
nginx内存池源码解析

文章插图
由于ngx_pool_cleanup_add中用头插法将创建的清理块链入pool->cleanup,所以ngx_destroy_pool的时候先清理文件后清理堆内存 。
相关测试代码推送到:https://github.com/BugMaker-shen/nginx_sgistl_pool
【nginx内存池源码解析】到此这篇关于nginx内存池源码解析的文章就介绍到这了,更多相关nginx内存池内容请搜索考高分网以前的文章或继续浏览下面的相关文章希望大家以后多多支持考高分网!