剖析Nginx的内存池源码 1.Nginx内存池概述 Nginx内存池是Nginx为了优化内存管理而引入的一种机制。以下是对Nginx内存池工作原理和底层实现机制的详解:
一、工作原理
减少频繁的malloc和free操作 :通过内存池,Nginx避免了频繁的动态内存申请和释放,从而降低了内存管理的开销。
防止内存泄漏 :内存池可以有效地避免因为申请未释放、二次释放或异常流程未释放而导致的内存泄漏问题。
提高内存使用效率 :内存池可以优化内存的分配和回收,避免内存碎片的产生,从而提高内存的利用率。
提高系统稳定性 :通过内存池,系统能够更好地管理内存资源,减少因为内存问题引发的异常,从而增强系统的稳定性和健壮性。
二、底层实现机制
内存池结构
Nginx内存池主要涉及以下几个结构体:
ngx_pool_s :作为整个内存池的头信息,只有第一个内存块才有,维护小块内存入口地址、大块内存入口地址、清理函数的入口指针等内存池全局信息。
ngx_pool_data_t :是每个内存块都有的,记录本小块内存的信息,包括本块内存的起始可分配位置、末尾可分配位置、下一块的地址链接等。
ngx_pool_large_s :保存大块内存的头信息,其保存大块内存的真正起始地址和下一个大块内存的头信息地址,这个头信息自身由于数据量小,也存放在小块内存中。
ngx_pool_cleanup_s :保存清理相关操作,在内存池销毁的时候才成链的去调用。
内存分配与回收
小块内存分配 :当外部向内存池申请小块内存分配时,内存池从连续内存空间中划分一部分出去。用户申请后并不需要释放小块内存,而是等待释放内存池时再释放。
大块内存分配 :对于大块内存的分配请求,内存池不会直接在内存池上分配内存来满足请求,而是直接向系统申请一块内存(就像直接使用malloc分配内存一样),然后将这块内存挂到内存池头部的large字段下。用户可以调用相关接口进行释放,也可以等内存池释放时再释放。
内存回收 :当内存池的生命周期结束时,整个内存池会被销毁,将分配的内存一次性归还给操作系统。
链式管理
Nginx内存池通过链式管理大小内存块,实现内存管理。小块内存通过链表进行管理,内存分配过程涉及结点上空闲内存匹配,是链表的遍历。为了提高效率,增加了failed分配内存失败次数统计。大块内存由单向链表管理,没有复杂的空闲内存管理逻辑。
回调函数
Nginx内存池支持增加回调函数,当内存池释放时,自动调用回调函数释放用户申请的资源。这方便开发者进行部分具体的业务处理。
内存对齐
Nginx内存池在分配内存时,会进行内存对齐操作,这有利于提高CPU读数据效率,是高性能系统不可缺少的一环。
三、重要函数接口
ngx_create_pool :创建内存池。
ngx_destroy_pool :销毁内存池,释放所有内存,包括pool、large、cleanup链表。
ngx_reset_pool :重置内存池。
ngx_palloc :内存分配函数,支持内存对齐。
ngx_pnalloc :内存分配函数,不支持内存对齐。
ngx_pcalloc :内存分配函数,支持内存初始化0。
ngx_pfree :内存释放函数(大块内存)。
ngx_pool_cleanup_add :添加清理回调函数。
综上所述,Nginx内存池通过一系列精妙的设计和优化措施,为Nginx提供了高效、稳定的内存管理机制,为项目开发和服务器运行提供了更加可靠的基础。
2.内存池重要类型定义 宏定义 1 2 3 4 5 6 7 8 9 10 11 12 #define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1) #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)
内存池主结构体信息 1 2 3 4 5 6 7 8 9 10 11 12 13 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; ngx_log_t *log;};
小块内存数据头信息 1 2 3 4 5 6 7 8 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 ;
大块内存类型定义 1 2 3 4 5 6 typedef struct ngx_pool_large_s ngx_pool_large_t ;struct ngx_pool_large_s { ngx_pool_large_t *next; void *alloc; };
回调函数cleanup结构体 1 2 3 4 5 6 7 8 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; };
3.内存池创建函数代码解读 主要做:
1.开辟内存
2.初始化记录内存池信息的两个结构体
1 2 3 ngx_pool_t *ngx_create_pool (size_t size, ngx_log_t *log)
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 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); 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; }
1 2 3 4 5 6 7 8 9 10 11 #if (NGX_HAVE_POSIX_MEMALIGN || NGX_HAVE_MEMALIGN) 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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; }
4.申请内存的函数 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 41 42 43 44 45 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); } 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函数
1 2 3 4 5 6 7 8 9 10 11 12 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); }
5.小块内存分配(向内存池申请内存的过程) 1.小块内存分配函数ngx_palloc_small 小块内存分配函数ngx_palloc_small
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 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); }
2.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 = 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; }
重点解释一下最后这个for循环在做什么事情
1.我们要知道调用本函数传入的pool是内存分配失败以后传入进来,要让我们这个函数新开辟一个内存池来进行内存的分配的
2.所有创建的内存池只有第一个保留所有信息,以后的都只有四根指针
3.ngx_pool_t *current是小块内存池入口指针 每次分配都是从这个指针所指的内存池开始分配的,可以理解为是链表的头结点(先这样理解)
4.ngx_uint_t failed; 记录当前内存池分配失败的次数
5.从头遍历链表所有的节点,因为从current到当前新分配的结点,这一堆全都分配失败了,所以都要++。如果说当前内存池已经分配内存失败超过了4次,那说明当前这个内存池剩下的可用内存太小了
6.然后current指针往后移动,移动到next,以后就再也不用next之前的内存池了,因为太小了,分配也不会成功,下一次自然就从next这个结点开始
7.继续对下一个内存池重复这个动作直到最后一个
看到这里就明白了,一开始传入的current存放的也不一定是这个链表的头结点,可能是链表中间的某个结点
个人觉得施磊老师对小块函数讲解十分到位
6.大块内存分配与释放 1.分配 ngx_palloc_large函数
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 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); 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; }
2.释放 大块内存释放函数ngx_pfree
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) { ngx_log_debug1 (NGX_LOG_DEBUG_ALLOC, pool->log, 0 , "free: %p" , l->alloc); ngx_free (l->alloc); l->alloc = NULL ; return NGX_OK; } } return NGX_DECLINED; }
遍历内存池链表,哪个和要释放的一样就把哪个释放掉,然后把alloc置为空
7.内存池重置函数和小块内存回收方案 1.内存池重置函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 ; }
1 2 3 4 5 6 7 8 9 10 11 p->d.last = (u_char *) p + sizeof (ngx_pool_t ); p->d.failed = 0 ; for (p = pool; p; p = p->d.next) { p->d.last = (u_char *) p + sizeof (ngx_pool_data_t ); p->d.failed = 0 ; }
2.小块内存回收方案 没有提供任何的内存释放函数,实际上,从小块内存的分配方式来看(直接通过last指针偏移来分配内存),它也没办法提供回收函数。
nginx本质:http服务器
是一个短链接的服务器,客户端(浏览器)发起一个request请求,到达nginx服务器以后,处理完成,nginx给客户端返回一个response响应,http服务器就主动断开tcp连接(http 1.1 keep-avlie:60s)http服务器(nginx)返回响应以后,需要等待60s,60s之内客户端又发来请求,重置这个时间,否则60s之内没有客户端发来的响应,nginx就主动断开连接,此时nginx可以调用ngx_reset_pool重置内存池了,等待下一次连接。
总结:一个客户可以配一个内存池,重置就当回收了。只适合这种web http短连接。如果是TCP连接,那比较适合SGI STL空间配置器那种模式。
8.内存池外部资源释放和内存池销毁 1.外部资源释放 1.为什么要注意外部资源的释放 考虑下面这种情况
给结构体data分配内存是内存池里面的的,但是结构体里面的指针却又用malloc函数在堆上开辟了一块内存。这时候释放内存池只是把指针p释放掉了,而堆上的内存没有释放掉,造成了内存泄漏。所以就和类一样需要一个析构函数,先把堆上的内存释放掉。
外部资源不只是堆上的内存,还有文件描述符之类的,这里只是简单说明一下需要一个类似析构函数的东西。
所以我们需要预置一个资源释放的函数(通过回调函数的指针来实现)->充当析构函数的角色
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ngx_create_pool (512 , log); 512 -sizeof (nginx_pool_s)ngx palloc (512 ) ;= >大块内存分配struct Data{ char *p; xxxxxxX 一共需要51 字节 }; typedef struct Data stData;stData *pData = ngx_alloc (512 ); pData->p=(char *)malloc (12 ); strcpy (pData->p, hello world"): }:
2.ngx_pool_cleanup_t * cleanup 的作用 1 2 3 4 5 6 7 8 9 10 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; };
3.cleanup_add函数 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 ; } 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; }
4.cleanup举例 1 2 3 4 5 6 7 8 9 10 11 void release (void *p) { free (p); } ngx_pool_cleanup_t *pclean=ngx_pool_cleanup_add (poo1,sizeof (char *));pclean->handler = &release: pclean->data =pData->p;
2.内存池销毁 1.释放外部资源->handler回调函数
2.释放大块内存池
3.释放小块内存池
因为外部资源清理相关的cleanup和大块结构体信息全在小块内存池存着呢,所以小块最后释放
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 41 42 43 44 45 46 47 48 49 50 51 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) 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; ; 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; ; p = n, n = n->d.next) { ngx_free (p); if (n == NULL ) { break ; } } }
9.nginx源码编译测试内存池接口函数功能 在Ubuntu环境下测试
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 #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) { printf ("free ptr mem!" ); free (p); } void func2 (FILE *pf) { printf ("close file!" ); fclose (pf); } void main () { 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; ngx_destroy_pool (pool); return ; }
编译链接操作
1 2 3 4 gcc -c -g -I src/core -I src/event -I src/event/modules -I src/os/unix -I objs -I src/http -I src/http/modules -o ngx_testpool.o ngx_testpool.c gcc -o ngx_testpool ngx_testpool.o objs/src/core/ngx_palloc.o objs/src/os/unix/ngx_alloc.o
10.Nginx内存池总结 1.指针相关重点
current指针确定分配的内存池是哪个
last指针确定分配的内存在内存池的起始地址
next把内存池连接成为链表
2.小块的分配与回收
小块内存池依靠last偏移分配内存
小块内存池没有专门的回收函数
3.销毁内存池时的顺序
外部资源-大块内存池-小块内存池
11.Nginx和SGI STL空间配置器对比 Nginx 内存池和 SGI STL 内存池的设计和使用目标有所不同,各自具备特定的内存管理特点。以下是它们的异同点。
一、Nginx 内存池与 SGI STL 内存池的相同点
减少内存分配和释放的开销 :两者都通过内存池来减少频繁的 malloc
和 free
操作,提升内存管理效率。
提供固定大小的内存块分配 :它们在分配小内存块时都能够提升分配效率并降低碎片化。
适合小对象的分配 :两个内存池都更适合频繁分配、释放的小对象,这些对象的生命周期通常较短。
二、Nginx 内存池与 SGI STL 内存池的不同点
设计目的 :
Nginx 内存池 :主要用于高并发服务器中的短生命周期对象管理。Nginx 内存池更适合在 HTTP 请求处理过程中管理频繁生成和销毁的短生命周期对象。
SGI STL 内存池 :设计用于 C++ STL 容器的内存管理,为容器对象提供更高效的内存分配,支持小对象和灵活的内存回收,适合长生命周期的容器操作。
实现方式 :
Nginx 内存池 :分配一个大块内存后,按需划分小块,并不支持回收,整个内存池在使用完后一次性释放。
SGI STL 内存池 :使用自由链表管理小块内存,动态复用并支持按大小分组,回收时更高效。
内存碎片管理 :
Nginx 内存池 :通过单一大块内存的连续分配来减少碎片,管理更简单。
SGI STL 内存池 :使用分组管理,减少小块分配带来的碎片,但碎片化比 Nginx 内存池相对严重。
灵活性 :
Nginx 内存池 :固定分配模式,灵活性较低,不适合频繁变化的内存需求。
SGI STL 内存池 :分配灵活,适用于各种对象大小的动态分配和回收管理。
三、各自的优势与劣势 1. Nginx 内存池
优势
高效管理短生命周期内存 :不需要逐个释放对象,适合短生命周期数据块管理,减少了频繁的 malloc/free
调用。
减少碎片 :通过集中管理内存分配,降低了内存碎片产生的概率。
实现简单 :内存池结构简单,分配和释放速度较快。
劣势
灵活性低 :固定大小的内存池管理方式对动态内存需求变化适应性较差。
内存浪费 :池内内存块大小固定,可能导致某些情况下的内存浪费。
不适合长生命周期对象 :适合短生命周期对象,不适合需要长期驻留的对象管理。
2. SGI STL 内存池
优势
灵活 :适用于不同大小的内存块管理,具有更强的灵活性。
高效内存复用 :对小块内存使用自由链表进行回收,降低了 malloc
的调用次数。
适用于容器的内存分配 :尤其对频繁插入、删除的小对象有较好表现。
劣势
实现复杂 :相比 Nginx 内存池实现更为复杂。
内存碎片问题 :虽然通过分组策略减少了碎片,但不如 Nginx 内存池集中管理彻底。
适用性较窄 :更适用于 STL 容器内部的内存管理,对大块内存管理不够高效。
四、适用场景 1. Nginx 内存池 Nginx 内存池非常适合在高并发场景中使用,特别是对于具有短生命周期的大量对象。这类场景包括:
Web 服务器 :如 Nginx,频繁处理短时间内生成和销毁的 HTTP 请求数据。
数据缓存 :当数据缓存是短生命周期且访问频繁时,可有效减少内存分配开销。
其他高并发短生命周期场景 :如负载均衡器、反向代理等。
2. SGI STL 内存池 SGI STL 内存池通常用于 C++ 标准容器(如 vector
、list
等)的内存管理。适用的场景包括:
通用 C++ 应用 :任何需要使用 STL 容器且频繁创建销毁小对象的场景。
图算法、树形结构 :这种算法中对象较多且较小,适合 STL 内存池的管理方式。
嵌入式系统 :在资源受限的环境中使用容器管理小对象,可以降低内存开销和碎片。