文章目录
- Nignx内存池
- 源码刨析
- 1. 重要的类型定义
- 1.1 宏定义
- 1.2 类型定义
- 2. 主要的函数接口
- 2.1 内存池的创建
- 2.1.1 ngx_memalign函数
- 2.1.2 ngx_create_pool函数
- 2.2 内存申请函数
- 2.3 小块内存申请
- 2.4 ngx_palloc_block内存块分配函数
- 2.5 大块内存申请
- 2.6 内存释放函数(大块内存)
- 2.7 内存池重置函数
- 2.8 添加外部清理函数
- 2.9 内存池销毁函数
Nignx内存池
nginx内存池是一个设计很巧妙,效率也特别高的内存池,较SGI STL的内存池也有许多的不同,本篇文章将会简单刨析nginx内存池的源码,通过分析nginx内存池的源码,我们也可以明显的看到该内存池与SGI STL内存池的一些区别。
源码刨析
1. 重要的类型定义
1.1 宏定义
首先我们需要先认识一些宏定义,下面是一些主要的宏定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 可以从内存池中申请的最大的内存大小(一页,4k) #define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1) // 内存池的默认大小(16k) #define NGX_DEFAULT_POOL_SIZE (16 * 1024) // 内存池字节对齐 #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) |
对于前面三个定义都不难理解,最后一个宏定义调用了ngx_align函数,这个函数的作用和SGI STL二级空间配置器的_S_round_up函数如出一辙,即将d的大小调整到最邻近的a的倍数,其完整定义如下:
1 2 | // 把d调整为a的倍数 #define ngx_align(d, a) (((d) + (a - 1)) & ~(a - 1)) |
1.2 类型定义
接着我们需要了解内存池得基本结构,首先先看一下其中最基础得结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // 小块内存数据头信息 typedef struct { u_char *last; // 可分配内存开始位置 u_char *end; // 可分配内存末尾位置 ngx_pool_t *next; // 保存下一个内存池的地址 ngx_uint_t failed; // 记录当前内存池分配失败的次数 } ngx_pool_data_t; // 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; // 日志 }; |
内存池中最主要的结构是ngx_pool_s,其中的ngx_pool_data_t也为结构体,用来存储小块内存数据的头信息,各个成员的含义已经在代码中给出,通过下图,我们可以简单的认识一下上述的结构的关系
接着我们来看内存池中的其他结构,下面给出了内存池中大块内存的类型定义ngx_pool_large_s以及外部清理操作的类型定义ngx_pool_cleanup_s ,关于下面两个结构体的功能将会在下文介绍。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 大块内存类型定义 struct ngx_pool_large_s { ngx_pool_large_t *next; // 下一个大块内存 void *alloc; // 记录分配的大块内存的起始地址 }; typedef void (*ngx_pool_cleanup_pt)(void *data); // 清理回调函数的类型定义 // 清理操作的类型定义,包括一个清理回调函数,传给回调函数的数据和下一个清理操作的地址 struct ngx_pool_cleanup_s { ngx_pool_cleanup_pt handler; // 清理回调函数 void *data; // 传递给回调函数的指针 ngx_pool_cleanup_t *next; // 指向下一个清理操作 }; |
因为C语言的语法,每次在使用结构体类型的时候,前面的struct 是不可以省略的,所以为了提高代码的可读性,这里对上述的结构体的名字进行了重命名。
1 2 3 | typedef struct ngx_pool_s ngx_pool_t; typedef struct ngx_pool_large_s ngx_pool_large_t; typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t; |
2. 主要的函数接口
nginx内存池主要包括下列8个函数,下面将会对这8个函数进行一一解读
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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 |
2.1 内存池的创建
2.1.1 ngx_memalign函数
在看内存池创建函数之前,我们先要了解ngx_memalign函数,其定义如下:
1 2 3 4 5 | #if (NGX_HAVE_POSIX_MEMALIGN || NGX_HAVE_MEMALIGN) void *ngx_memalign(size_t alignment, size_t size, ngx_log_t *log); // #1 #else #define ngx_memalign(alignment, size, log) ngx_alloc(size, log) // #2 #endif |
当包含NGX_HAVE_POSIX_MEMALIGN或者NGX_HAVE_MEMALIGN时,调用 #1函数
当没有上述两个宏,则调用 #2函数,即ngx_alloc(size, log),这个函数就是对malloc的一个封装
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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; } |
2.1.2 ngx_create_pool函数
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 | ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log) { ngx_pool_t *p; // 申请size大小的空间 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); // 该内存池可分配的空间大小 // 比较该内存池一次可分配的最大空间 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; } |
nginx中的内存池是在创建的时候就设定好了大小,在以后分配小块内存的时候,如果内存不够,则是重新创建一块内存串到内存池中,而不是将原有的内存池进行扩张。当要分配大块内存是,则是在内存池外面再分配空间进行管理的,称为大块内存池。
创建内存池函数先申请size字节大小的内存,然后将last指针指向可分配内存的起始位置,end指针指向该内存池的最后位置。其中max指向的是该内存池一次可分配的最大的大小,若当前内存池可分配大小大于NGX_MAX_ALLOC_FROM_POOL(4k),则max为NGX_MAX_ALLOC_FROM_POOL;否则max的值为当前内存池可分配大小。
2.2 内存申请函数
nginx对内存的管理分为大内存与小内存,当某一个申请的内存大于某一个值时,就需要从大内存中分配空间,否则从小内存中分配空间。
内存申请函数有三个,实现也特别的简单,其中调用了小块内存申请和大块内存申请两个函数,后面我将会对那两个函数进行简单的讲解,首先我们先看一下内存申请的三个函数的实现:
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 | // 内存分配函数,支持内存对齐 void *ngx_palloc(ngx_pool_t *pool, size_t size) { if (size <= pool->max) { // 申请小块内存 return ngx_palloc_small(pool, size, 1); } // 申请大块内存 return ngx_palloc_large(pool, size); } // 内存分配函数,不支持内存对齐 void *ngx_pnalloc(ngx_pool_t *pool, size_t size) { if (size <= pool->max) { // 申请小块内存 return ngx_palloc_small(pool, size, 0); } // 申请大块内存 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_small的第三个参数决定的,为0则不支持对齐,为1则支持对齐;第三个可初始化为0的函数首先调用了ngx_palloc申请空间,申请成功后则调用ngx_memzero来进行初始化,ngx_memzero是对memset函数的封装,其定义如下:
1 2 | // 将buf初始化为0 #define ngx_memzero(buf, n) (void) memset(buf, 0, n) |
2.3 小块内存申请
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 | 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; 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(pool, size); } |
ngx_palloc_small函数首先先将指针指向内存池的入口,紧接着判断是否需要内存对齐,内存对齐是一个定义的宏,其定义如下:
1 2 3 4 5 6 | // 小块内存分配考虑字节对齐时的单位 #define NGX_ALIGNMENT sizeof(unsigned long) // 将p指针按a对齐(将p调整为a的临近的倍数) #define ngx_align_ptr(p, a) \ (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1)) |
这是一个用来内存地址取整的宏,非常精巧,作用不言而喻,取整可以降低CPU读取内存的次数,提高性能。因为这里并没有真正意义调用malloc等函数申请内存,而是移动指针标记而已,所以需要自己实现该代码。
在本函数中,将会依据不同的应用场景(32位4字节,64位 8字节)把m指针的地址 调整为与平台相关的NGX_ALIGNMENT(4 或者 8)的整数倍的地址上去。
指针对齐操作完成后,会依此判断当前内存池是否可以分配空间,若可以顺利分配,就返回分配内存的首地址;若均分配失败,就需要申请新的内存块。
上图就是对申请小块内存的一个简单描述。
2.4 ngx_palloc_block内存块分配函数
当申请小块内存失败时,就需要重新申请一块内存块用来给用户分配内存,ngx_palloc_block函数就是解决这样一个问题,其代码如下:
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 | 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 += sizeof(ngx_pool_data_t); // m指向可分配空间的起始位置 m = ngx_align_ptr(m, NGX_ALIGNMENT);// 使m指针对齐 new->d.last = m + size; // 偏移last指针,给用户分配内存 // 因为小块内存分配失败,对每个failed++,若failed>4,则更新内存池的入口指针 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; } |
该函数的实现较为简单,主要功能就是申请一块和内存池中内存块大小相同的一个空间,再将数据块信息进行初始化,紧接着给用户分配空间。分配完成后,需要遍历整个内存池,将所有内存块的failed+1,当一个内存块四次内存分配都失败,说明该内存块的可用空间已经不多,就需要调整内存池的入口指针。
通过上图我们就可以更好的理解该函数所进行的操作:申请好空间new后,先将m指向可分配内存的起始地址,再偏移last指针,为用户分配size大小的空间,将end指向该内存块的最后,然后将这个新的内存块连接到内存池中去。
2.5 大块内存申请
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 | 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); // ngx_free就是free函数 return NULL; } large->alloc = p; // 将alloc指向大块内存 large->next = pool->large; // 将该大块内存头插到内存池large处 pool->large = large; return p; } |
如上图所示,函数首先先通过ngx_alloc申请一块内存,然后通过循环查看大块内存链表的前三个内存头下有没有数据:如果前三个大块内存头信息下都有大块内存,则继续执行程序,在小块内存中申请一个大块内存头节点,并将该头节点以头插的方式加入链表中;若里面有一个为空,则将该内存头的alloc指向大块内存。
2.6 内存释放函数(大块内存)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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) { // 如果alloc指向的是需要释放的内存,将该块内存释放掉,将指针置空 ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc); ngx_free(l->alloc); // free(l->alloc); l->alloc = NULL; return NGX_OK; } } return NGX_DECLINED; } |
我们可以看到,内存池只提供了大块内存的释放而没有提供小块内存的释放。实际上从内存池小块内存的分配方式来看,他也无法提供小块内存的释放,因为小块内存是通过last指针偏移来实现的,所以内存池小块内存的分配效率特别的高。正所谓鱼和熊掌不可兼得,小块内存仅靠两个指针就实现了内存的分配,那么对于回收就无法进行控制。
就nginx而言,小块内存也是不需要进行回收的,这与nginx的应用场景有关:nginx的本质是http服务器,http服务器是一个短链接的服务器,客户端(浏览器)发起一个request请求,到达nginx服务器以后,处理完成后nginx给客户端返回一个response响应,http服务器就主动断开tcp连接,这时就可以调用重置函数重置内存池。
http1.1以后支持keep-alive(60s)即http服务器(nginx)返回响应以后,需要等待60s,60s之内客户端又发来请求,重置这个时间,否则60s之内没有客户端发来的响应, nginx就主动断开连接,此时 nginx可以调用ngx reset_poo1重置内存池,等待下一次该客户端的请求。
2.7 内存池重置函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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指针,将其指向可分配的起始位置 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指针重置即可。因为内存池中只有第一个内存块的结构为ngx_pool_t,而剩余的都为ngx_pool_data_t,所以若按源码来看,将会造成部分的空间浪费,对于第二个for循环,我认为可做如下改进:
1 2 3 4 5 6 7 8 9 10 | // 处理第一块内存池 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.last = (u_char *) p + sizeof(ngx_pool_data_t); p->d.failed = 0; } |
2.8 添加外部清理函数
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 | 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; } // 如果size指定大小,就为data开辟size大小的内存 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; } |
在说明该函数前,我们先看这样一个问题:
在该内存池的一块大块内存上,有一个指针指向一个外部资源,当我们释放内存池的时候就需要先将外部的资源释放掉,进而在释放内存池中的内存,这就需要提供一个外部清理函数,对于该函数我们可做如下运用:
将清理外部资源函数的赋予handler,并将指向资源的指针赋予data作为handler函数的参数。
2.9 内存池销毁函数
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 | 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); // 调用handler函数 } } // 清理大块内存 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; } } } |
销毁内存池主要分为三步:先释放外部资源,在释放大块内存,最后释放内存池。这三步的顺序不可乱,因为外部资源和大块内存的地址信息均存储在内存池中,所以内存池必须最后释放,最先释放的肯定是外部资源,接着就是大块内存,这样才能保证不会造成内存泄漏。