内容
内存池是为了更高效地管理小块内存的频繁开辟、释放。
C++
STL:标准模板库。
SGI STL:第三方厂商开发的,后来被纳入C++
标准,成为了C++
STL中管理内存的底层实现。
Nginx内存池设计
C++ STL
空间配置器
先用vector举个例子
1 2 3 4 5 template <typename T, typename _Alloc = allocator<T>>class vector{ };
可以看到,vector第二个模板参数是一个空间配置器。
主要包含了四个方法:
allocate
:负责给容器开辟内存空间=>malloc
deallocate
:负责释放容器内存空间=>free
construct
:负责在容器中构造对象=>定位new
:是基于已经开辟好了的容器空间中直接构造的。
destroy
:负责析构容器中的对象=>p->~T()
空间配置器的核心作用:
拆开了new的两个操作——对象的内存开辟、对象构造;
拆开了delete的两个操作——对象的析构,内存的内存释放。
把空间和对象本身分开,在容器这个场景下更为适合。
SGI STL 的两级 allocator
提供了两个allocator的实现:
一级allocator,实际上就是malloc/free
;
二级allocator,是基于内存池的内存管理。
本文主要剖析SGI的二级allocator,即内存池的实现。
通过阅读源码,发现:
SGI STL底层对于容器的对象的构造、析构是通过自定义的全局模板函数Construct和Destroy完成的。
而点进去发现本质上仍是通过定位new 、调用对象的析构 函数完成的。这些都是内存申请、释放工作之外的动作。
因此,可以推断,SGI STL的空间配置器主要工作的区别在于 allocate 和 deallocate ,即对容器内存申请、释放的管理 。
SGI STL 内存池的实现
内存池的粒度信息
1 2 3 enum { _ALIGN = 8 };enum { _MAX_BYTES = 128 };enum { _NFREELISTS = 16 };
每一个内存chunk块的头信息
1 2 3 4 5 union _Obj { union _Obj * _M_free_list_link; char _M_client_data[1 ]; };
组织所有自由链表的指针数组。
这是静态变量,多线程共享,volatile避免了读取缓存的脏数据。
1 static _Obj* __STL_VOLATILE _S_free_list[_NFREELISTS];
图示:
一个数组,第 1 个位置存放的是 8 字节的内存池,第 2 个位置存放的是 16 字节的内存池,…,最后一个位置存放的是 128 字节的内存池。数组的大小和每个位置对应的字节数由内存池的粒度信息 决定。
allocate(size_t)
定义在stl_alloc.h
中
统一的allocate接口。外部申请 n 个字节。如果是大块内存,则普通malloc。
如果是128字节及以下的内存,则会从内存池中分配,这就是二级空间配置。
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 * allocate (size_t __n) { void * __ret = 0 ; if (__n > (size_t ) _MAX_BYTES) { __ret = malloc_alloc::allocate (__n); } else { _Obj* __STL_VOLATILE* __my_free_list = _S_free_list + _S_freelist_index(__n); # ifndef _NOTHREADS _Lock __lock_instance; # endif _Obj* __RESTRICT __result = *__my_free_list; if (__result == nullptr ) { __ret = _S_refill(_S_round_up(__n)); } else { *__my_free_list = __result -> _M_free_list_link; __ret = __result; } } return __ret; };
_S_freelist_index(size_t)
用于返回freelist的下标,0下标存放的是8字节的内存池块。1下标存放的是16字节的内存池块。以此类推。
比如,
外部申请 1 字节,则 ( 1 + 8 − 1 ) / 8 − 1 = 1 − 1 = 0 (1 + 8 - 1) / 8 - 1 = 1 - 1 = 0 ( 1 + 8 − 1 ) / 8 − 1 = 1 − 1 = 0
外部申请 7 字节,则 ( 7 + 8 − 1 ) / 8 − 1 = 1 − 1 = 0 (7 + 8 - 1) / 8 - 1 = 1 - 1 = 0 ( 7 + 8 − 1 ) / 8 − 1 = 1 − 1 = 0
外部申请 8 字节,则 ( 8 + 8 − 1 ) / 8 − 1 = 1 − 1 = 0 (8 + 8 - 1) / 8 - 1 = 1 - 1 = 0 ( 8 + 8 − 1 ) / 8 − 1 = 1 − 1 = 0
外部申请 9 字节,则 ( 9 + 8 − 1 ) / 8 − 1 = 2 − 1 = 1 (9 + 8 - 1) / 8 - 1 = 2 - 1 = 1 ( 9 + 8 − 1 ) / 8 − 1 = 2 − 1 = 1
1 2 3 4 static size_t _S_freelist_index(size_t __bytes){ return (__bytes + (size_t )_ALIGN - 1 ) / (size_t )_ALIGN - 1 ; }
_S_round_up(size_t)
外部申请 n 字节,返回的是 n 字节对应的在内存池块中,一小块的实际大小(8的整数倍)。
类似于向上取整(取8的整倍数的最小值),比如输入1到8,输出8。输入9到16,输出16。(输入0,输出0)
比如,
外部申请 1 字节,则 ( 1 + 8 − 1 ) & ∼ ( 8 − 1 ) = 8 & ∼ ( 7 ) = 1000 & ∼ ( 0111 ) = 1000 & 1000 = 8 (1 + 8 - 1) \& \sim(8 - 1) = 8 \& \sim(7) = 1000 \& \sim(0111) = 1000 \& 1000 = 8 ( 1 + 8 − 1 ) & ∼ ( 8 − 1 ) = 8 & ∼ ( 7 ) = 1 0 0 0 & ∼ ( 0 1 1 1 ) = 1 0 0 0 & 1 0 0 0 = 8
外部申请 7 字节,则 ( 7 + 8 − 1 ) & ∼ ( 15 − 1 ) = 15 & ∼ ( 7 ) = 1110 & ∼ ( 0111 ) = 1110 & 1000 = 8 (7 + 8 - 1) \& \sim(15 - 1) = 15 \& \sim(7) = 1110 \& \sim(0111) = 1110 \& 1000 = 8 ( 7 + 8 − 1 ) & ∼ ( 1 5 − 1 ) = 1 5 & ∼ ( 7 ) = 1 1 1 0 & ∼ ( 0 1 1 1 ) = 1 1 1 0 & 1 0 0 0 = 8
外部申请 8 字节,则 ( 8 + 8 − 1 ) & ∼ ( 16 − 1 ) = 15 & ∼ ( 7 ) = 1111 & ∼ ( 0111 ) = 1111 & 1000 = 8 (8 + 8 - 1) \& \sim(16 - 1) = 15 \& \sim(7) = 1111 \& \sim(0111) = 1111 \& 1000 = 8 ( 8 + 8 − 1 ) & ∼ ( 1 6 − 1 ) = 1 5 & ∼ ( 7 ) = 1 1 1 1 & ∼ ( 0 1 1 1 ) = 1 1 1 1 & 1 0 0 0 = 8
外部申请 9 字节,则 ( 9 + 8 − 1 ) & ∼ ( 17 − 1 ) = 16 & ∼ ( 7 ) = 10000 & ∼ ( 00111 ) = 10000 & 11000 = 16 (9 + 8 - 1) \& \sim(17 - 1) = 16 \& \sim(7) = 10000 \& \sim(00111) = 10000 \& 11000 = 16 ( 9 + 8 − 1 ) & ∼ ( 1 7 − 1 ) = 1 6 & ∼ ( 7 ) = 1 0 0 0 0 & ∼ ( 0 0 1 1 1 ) = 1 0 0 0 0 & 1 1 0 0 0 = 1 6
1 2 3 4 5 static size_t _S_round_up(size_t __bytes) { return (__bytes + (size_t )_ALIGN - 1 ) & ~((size_t )_ALIGN - 1 ); }
_S_refill(size_t)
调用_S_chunk_alloc(__n, __nobjs)
,在内存池块中尽量找一个合适的小字节块。
_S_chunk_alloc
内部会帮你处理底层的开辟内存池,处理内存碎片,管理内存池的指示信息等等。
由于_S_chunk_alloc
第二个参数__nobjs
传入的是引用,有可能__nobjs
会被改变。
调用之前,__nobjs
是我们想要申请的小字节块的个数。
调用结束后,__nobjs
更新为了实际分配到的小字节块的个数。
如果是 1 ,此次分配完之后,这个内存池块正好用完了,不构建freelist下标的链表。
其他情况,构建相应的freelist下标的链表。
for循环中做的是遍历内存池大块中的每个单元小块,联合体_Obj*
的_M_free_list_link
指向紧挨着的下一个单元小块。
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 template <bool __threads, int __inst>void *__default_alloc_template<__threads, __inst>::_S_refill(size_t __n) { int __nobjs = 20 ; char * __chunk = _S_chunk_alloc(__n, __nobjs); _Obj* __STL_VOLATILE* __my_free_list; _Obj* __result; _Obj* __current_obj; _Obj* __next_obj; int __i; if (1 == __nobjs) return (__chunk); __my_free_list = _S_free_list + _S_freelist_index(__n); __result = (_Obj*)__chunk; *__my_free_list = __next_obj = (_Obj*)(__chunk + __n); for (__i = 1 ; ; __i++) { __current_obj = __next_obj; __next_obj = (_Obj*)((char *)__next_obj + __n); if (__nobjs - 1 == __i) { __current_obj -> _M_free_list_link = 0 ; break ; } else { __current_obj -> _M_free_list_link = __next_obj; } } return (__result); }
图示:
_S_chunk_alloc(size_t, int& nobjs)
在内存池块中尽量找一个合适的小字节块。
期间,可能会改变外部传入的__nobjs
。(因为是引用,外部会受影响)
__total_bytes
记录的是欲开辟的内存池大小(根据__nobjs
,这个是小字节块数)。
__bytes_left
指的是_S_end_free - _S_start_free
,指目前未被开发的大小。
__bytes_left
还足够,返回,无需开辟新空间
如果__bytes_left
还足够(至少是 1 个小字节块),则返回_S_start_free
。同时移动_S_start_free
到新的位置。
成功返回。无需额外操作。
__bytes_left
不足,开辟新的更大的内存池块
如果__bytes_left
不足,则将要开辟新的更大的内存池块。__bytes_to_get = 2 * __total_bytes + _S_round_up(_S_heap_size >> 4)
__bytes_left > 0
,头插到合适的一个链表,管理这个内存碎片
如果__bytes_left > 0
。这时,为了不浪费__bytes_left
而造成内存碎片,把这部分尚余的小内存给其对应的freelist下标的链头插。比如,剩余 32 字节,则找下标 3 ,头插 进去。(见下文图示)
开辟新的大块内存_malloc正常
以下是__bytes_left
不足时,都需要做的操作,目的是开辟新的大块内存。返回的地址赋给_S_start_free
。
如果malloc正常(malloc返回 != 0
),更新_S_heap_size += __bytes_to_get;
,更新_S_end_free = _S_start_free + __bytes_to_get;
。
malloc成功,递归调用return(_S_chunk_alloc(__size, __nobjs));
。
开辟新的大块内存_malloc失败:找内存池中已开辟的大单元链表的空间
如果malloc失败(malloc返回 == 0
)。则处理:
遍历freelist的下标各个内存池块。从size_t __n
对应的freelist下标,依次往后找有没有还有空闲的。
__i
初始化为__n
,循环,每次__i += (size_t) _ALIGN
(即加8)。比如,__n
等于 40 字节,我们依次去找40、48、56等等的freelist下标的内存池块,看看有没有能分配出来空间的。
如果有,则_S_start_free
指向第一个空闲块。更新_S_end_free = _S_start_free + __i;
好了,成功在更大单元的内存池块找到,递归调用return(_S_chunk_alloc(__size, __nobjs));
。
开辟新的大块内存_malloc失败:异常处理
如果以上的for循环找了后面更大单元的内存池块,仍没有可用空间,则是系统内存不足的迹象:
需要调用malloc_alloc::allocate(__bytes_to_get);
。
内部最后一次进行普通malloc的挣扎。
如果malloc仍然返回 0 ,则进行异常处理(绑定的回调)。
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 73 template <bool __threads, int __inst>char *__default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size, int & __nobjs) { char * __result; size_t __total_bytes = __size * __nobjs; size_t __bytes_left = _S_end_free - _S_start_free; if (__bytes_left >= __total_bytes) { __result = _S_start_free; _S_start_free += __total_bytes; return (__result); } else if (__bytes_left >= __size) { __nobjs = (int )(__bytes_left / __size); __total_bytes = __size * __nobjs; __result = _S_start_free; _S_start_free += __total_bytes; return (__result); } else { size_t __bytes_to_get = 2 * __total_bytes + _S_round_up(_S_heap_size >> 4 ); if (__bytes_left > 0 ) { _Obj* __STL_VOLATILE* __my_free_list = _S_free_list + _S_freelist_index(__bytes_left); ((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list; *__my_free_list = (_Obj*)_S_start_free; } _S_start_free = (char *)malloc (__bytes_to_get); if (0 == _S_start_free) { size_t __i; _Obj* __STL_VOLATILE* __my_free_list; _Obj* __p; for (__i = __size; __i <= (size_t ) _MAX_BYTES; __i += (size_t ) _ALIGN) { __my_free_list = _S_free_list + _S_freelist_index(__i); __p = *__my_free_list; if (0 != __p) { *__my_free_list = __p -> _M_free_list_link; _S_start_free = (char *)__p; _S_end_free = _S_start_free + __i; return (_S_chunk_alloc(__size, __nobjs)); } } _S_end_free = 0 ; _S_start_free = (char *)malloc_alloc::allocate (__bytes_to_get); } _S_heap_size += __bytes_to_get; _S_end_free = _S_start_free + __bytes_to_get; return (_S_chunk_alloc(__size, __nobjs)); } }
头插小内存碎片,图示
oom异常处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 template <int __inst>class __malloc_alloc_template { private :#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG static void (* __malloc_alloc_oom_handler) () ; #endif public : static void * allocate (size_t __n) { void * __result = malloc (__n); if (0 == __result) { __result = _S_oom_malloc(__n); } return __result; } };
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 template <int __inst>void *__malloc_alloc_template<__inst>::_S_oom_malloc(size_t __n) { void (* __my_malloc_handler)(); void * __result; for (;;) { __my_malloc_handler = __malloc_alloc_oom_handler; if (0 == __my_malloc_handler) { __THROW_BAD_ALLOC; } (*__my_malloc_handler)(); __result = malloc (__n); if (__result) { return (__result); } } }
_S_start_free
、_S_end_free
、_S_heap_size
这三个变量,只会在_S_chunk_alloc(size_t, int&)
函数执行中改变。
1 2 3 4 5 6 7 8 template <bool __threads, int __inst>char * __default_alloc_template<__threads, __inst>::_S_start_free = 0 ;template <bool __threads, int __inst>char * __default_alloc_template<__threads, __inst>::_S_end_free = 0 ;template <bool __threads, int __inst>size_t __default_alloc_template<__threads, __inst>::_S_heap_size = 0 ;
deallocate(void* p, size_t)
定义于stl_alloc.h
头插归还,看图示。
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 template <bool threads, int inst>class __default_alloc_template {public : static void deallocate (void * __p, size_t __n) { if (__n > (size_t ) _MAX_BYTES) { malloc_alloc::deallocate (__p, __n); } else { _Obj* __STL_VOLATILE* __my_free_list = _S_free_list + _S_freelist_index(__n); _Obj* __q = (_Obj*)__p; # ifndef _NOTHREADS _Lock __lock_instance; # endif __q -> _M_free_list_link = *__my_free_list; *__my_free_list = __q; } } };
reallocate(void* p, old_sz, new_sz)
对已开辟的内存池块的扩容、缩容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 template <bool threads, int inst>void *__default_alloc_template<threads, inst>::reallocate (void * __p, size_t __old_sz, size_t __new_sz) { void * __result; size_t __copy_sz; if (__old_sz > (size_t )_MAX_BYTES && __new_sz > (size_t )_MAX_BYTES) { return (realloc (__p, __new_sz)); } if (_S_round_up(__old_sz) == _S_round_up(__new_sz)) { return (__p); } __result = allocate (__new_sz); __copy_sz = __new_sz > __old_sz ? __old_sz : __new_sz; memcpy (__result, __p, __copy_sz); deallocate (__p, __old_sz); return (__result); }
SGI STL内存池总结
SGI STL 二级空间配置器内存池的实现优点:
对于每一个字节数的chunk块分配,都是给出一部分进行使用,另一部分作为备用,这个备用可以给当前字节数使用,也可以给其它字节数使用
对于备用内存池划分完chunk块以后,如果还有剩余的很小的内存块,再次分配的时候,会把这些小的内存块再次分配出去,备用内存池使用的干干净净!防止小块内存频繁的分配,释放,造成内存很多的碎片出来,内存没有更多的连续的大内存块。所以应用对于小块内存的操作,一般都会使用内存池来进行管理。
malloc内存分配失败,还会调用oom_malloc
这么一个预先设置好的以后的回调函数,如果没设置,则throw bad_alloc
。设置了则for(;;)(*oom_malloc_handler)();
。
Nginx内存池设计和实现
区分大小内存块的申请和释放,大于池尺寸的定义为大内存块,使用单独的大内存块链表保存,即时分配和释放;小于等于池尺寸的定义为小内存块,直接从预先分配的内存块中提取,不够就扩充池中的内存,在生命周期内对小块内存不做释放,直到最后统一销毁。
Nginx内存池结构图
Nginx源码
本次分析的是Nginx-release-1.13.1的源码。
src目录下,有好多模块,其中内存池的模块位于/src/core
目录下。使用的是C语言。
对于不同的操作系统,有不同的实现,位于/src/os/unix
和/src/os/win32
下。
指标
1 2 3 4 5 6 7 8 9 10 11 12 13 #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)
NGX_MAX_ALLOC_FROM_POOL
定义了可以从内存池中申请的最大内存。默认是Nginx页面大小减1
。x86系统下是4096字节减1
。
NGX_DEFAULT_POOL_SIZE
定义了Nginx内存池默认大小。是16 * 1024B
即 16KB
。
NGX_POOL_ALIGNMENT
,内存池,分配内存时的对齐大小。默认是16。
NGX_MIN_POOL_SIZE
定义了内存池的最小大小。
其需要通过ngx_align
计算,定义如下:发现和STL的round_up
一样,向上取 d 的整 a 倍数。比如,a等于16、d等于7的话,那d取整后就是16,d等于17的话,取整后就是32。
其中d是(sizeof(ngx_pool_t) + 2 * sizeof(ngx_pool_large_t))
。a是NGX_POOL_ALIGNMENT
,默认是16。
1 2 #define ngx_align(d, a) ( ( (d) + (a - 1) ) & ~(a - 1))
关键数据结构
定义于ngx_palloc.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 typdef strcut { 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; ngx_chain_t *chain; ngx_pool_large_t *large; ngx_pool_cleanup_t *cleanup; ngx_log_t *log ; };
在ngx_core.h
中,
1 typedef struct ngx_pool_s ngx_pool_t ;
即npx_pool_t
是struct ngx_pool_s
的别名。
在Nginx内存池中,npx_pool_t
这个结构只出现在第一个内存池块的头部上,后续链接的内存池块头部只有ngx_pool_data_t
。
struct ngx_pool_s
(别名ngx_pool_t
)结构示意图(大小为1024的池)
npx_create_pool
创建内存池
声明于ngx_palloc.h
,定义于ngx_palloc.c
。
1 ngx_pool_t * ngx_create_pool (size_t size, ngx_log_t *log ) ;
返回npx_pool_t *
。
开辟指定大小的内存池。根据不同系统、不同的对齐方法,调用不同的API。
一般是普通的malloc。
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 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; }
ngx_memalign(POOL_ALIGNMENT, size, log)
或ngx_alloc(size, log)
这是个宏定义,定义于/src/os/unix
和/src/os/win32
下的ngx_alloc.h
。
在Unix下,有对齐的区别:如果要做内存对齐,则size_t alignment
参数生效,否则忽略对齐参数
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 ) ;#else #define ngx_memalign(alignment, size, log) ngx_alloc(size, log) #endif
在Win32下,没有对齐限制。#define ngx_memalign(alignment, size, log) ngx_alloc(size, log)
名字上,暂时一样。但是相应的ngx_alloc(size, log)
函数,在两个操作系统上就是不同的实现了。
我们看Unix的:实际就是套了个普通malloc 。然后输出了一些日志信息。
定义于/src/os/unix/ngx_alloc.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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; }
从创建好的内存池中申请内存
定义于ngx_palloc.c
3个接口:ngx_palloc
、ngx_pnalloc
、ngx_pcalloc
ngx_palloc
和ngx_pnalloc
的区别在于在申请小块内存 时,前者考虑对齐 ,后者不考虑对齐
ngx_pcalloc
是调用ngx_palloc
,之后的额外操作是清零申请的区域 。
ngx_palloc(pool, size)
如果用户申请小于等于pool头信息中max大小的内存,则按小块管理。
1 2 3 4 5 6 7 8 9 10 11 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); }
1 2 3 4 5 6 7 8 9 10 11 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); }
1 2 3 4 5 6 7 8 9 10 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(pool, size, align)
第3个参数是标志位,1表示考虑对齐,0表示不考虑对齐。
找current,即从该内存池大块之中分配内存。
ngx_align_ptr
,将p->d.last
指向的可用数据地址按NGX_ALIGNMENT
对齐到指定数(默认是32)的倍数,比如如果地址是8,则舍弃一部分内存,对齐到32。
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 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_align_ptr(p, a)
内存对齐
定义于ngx_config.h
如果没有定义NGX_ALIGNMENT
,则默认是sizeof(unsigned long)
,按照32位对齐。这是内存单元对齐。
注意要和ngx_palloc.h
中定义的#define NGX_POOL_ALIGNMENT 16
区分。那是内存池对齐。
即向定义的对齐大小(32)向上取整32倍。
操作是:传入的指针地址,加上指针大小32(4字节,32位)减1,和32减1取反后,相与。
比如:传入的指针地址是8:0000 1000
(前面补0),则加32减1:0010 1000
0010 0000 - 1 = 0001 1111
取反1110 0000
(前面补1)。0010 1000
和1110 0000
相与后:0010 0000
,由原先的8,对齐到了32。
1 2 3 4 5 6 #ifndef NGX_ALIGNMENT #define NGX_ALIGNMENT sizeof(unsigned long) #endif #define ngx_align_ptr(p, a) \ (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
ngx_palloc_block(pool, size)
再开辟一大块内存池
从pool的头块找到end - pool
的大小,这是内存池每一个大块的大小(包含头信息的大小)
这是为了再开辟一大块内存池 做准备。
ngx_memalign
和第一次创建内存池一样,如果没有对齐要求,则普通malloc。
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 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; }
ngx_palloc_large(pool, size)
大块内存分配管理
用户申请比pool->max
大的内存时,按大块内存分配管理。
用ngx_alloc
分配size大小的内存(和分配内存池块的方法一样)见ngx_memalign(POOL_ALIGNMENT, size, log)
或ngx_alloc(size, log)
。
遍历找large链的前5个,看是否有large的alloc为空的,直接让alloc指向一开始malloc得到的p。
如果连续找了5个,发现large的alloc都不为空,则跳出循环,不再找了。
这个for循环是为了快速在前5个large中,找到一个之前开辟的,但已经空闲了的大内存块头信息,其alloc管理的大内存块已经释放了,所以为NULL,直接让alloc指向一开始malloc得到的p即可。
如果没有进入循环,说明large一开始就是空的,内存池没有申请过大块内存;或者是找了5个发现large的alloc都不空:执行下面的操作(头插大内存块的头信息到pool的large链):
记录大内存块(large管理)的 头信息ngx_pool_large_t
,是按照ngx_palloc_small
方法,存放到了内存池块的小块内存中。
ngx_palloc_small
返回空说明系统内存不够用了,失败,释放一开始malloc得到的大块内存,返回NULL。
如果正常,则在这个大块内存头信息填写:
alloc
,即这个大块内存的地址,为一开始malloc得到的地址。
next
,指向pool的large链头,pool的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 40 41 42 43 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; }
图示:
如图,大块内存的头信息,是按小块内存管理,分配到了内存池块中。
下面要提到的外部资源所绑定的清理的头信息,也像是大块内存头信息一样,按小块内存管理,分配到了内存池块中。
ngx_pool_cleanup_add
分配一个需要管理外部资源的数据(比如指针、fd)
按小块内存分配ngx_pool_cleanup_t
,这是清理的头信息块。
有:handler、data、next。
之后才是分配size。
之后,像头插大内存块的头信息链表一样,头插清理类的头信息链表。
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 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; }
通过返回的ngx_pool_cleanup_t *
绑定清理回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct Data { char * p; }; struct Data *pData = ngx_alloc(512 );pData->p = (char *)malloc (128 ); strcpy (pData->p, "hello world" );void my_release (void *p) { free (p); } ngx_pool_cleanup_t *pclean = ngx_pool_cleanup_add(pool, sizeof (char *));pclean->handler = &my_release; pclean->data = pData->p;
ngx_pfree(pool, void *p)
大块内存释放
用于释放大块内存。先free,后把large头信息中的alloc置空。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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; }
小块内存释放:无
Nginx的小块内存一旦分配了之后,就无法精确地回收。
不像SGI STL一样,假设A、B、C三个小内存单元相邻,A、C空闲,A是freelist的第一个空闲块,A的next是C。现在释放B,则B头插到freelist,现在是B连着A连着C。这样可以精确地释放,并下次还能重新分配(可以发现,由于是头插到了freelist的第一个空闲区域,所以最后释放的最先分配)。
Nginx呢,每个内存池块只是由last
和end
两个指针管理,只能指示当前内存池块的未分配的部分。
当已分配的部分中,有 1 个小块内存要释放,无法精确管理。所以Nginx只能连续地释放一整段空间,与last相连。
ngx_reset_pool(pool)
遍历large链表,释放每个大块内存。最后large置空。
遍历每个内存池块,把last拉到头信息的末尾即可,相当于释放了内存池的数据。最后current置为pool。
注意,只是释放了大块内存,所有内存池块都没有free,只是更新了last。
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 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) { if (p == pool) { p->d.last = (u_char*)p + sizeof (ngx_pool_t ); } else { p->d.last = (u_char *) p + sizeof (ngx_pool_data_t ); } p->d.failed = 0 ; } pool->current = pool; pool->chain = NULL ; pool->large = NULL ; }
ngx_destroy_pool(pool)
遍历“清理”链表,每个都按照绑定的handler释放其data外部资源。
遍历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 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 ; } } }
Nginx和STL释放内存的策略适用的场景
Nginx大块内存分配=》内存释放ngx_free函数
Nginx小块内存分配=》没有提供任何的内存释放函数。
实际上,从小块内存的分配方式来看(直接通过last指针偏移来分配内存),根本没法进行中间部分的小块内存的回收。
Nginx本质:HTTP服务器是一个短链接的服务器,客户端(浏览器)发起一个request请求,到达Nginx服务器以后,处理完成,Nginx给客户端返回一个response响应,HTTP服务器就主动断开tcp连接。
假设HTTP 1.1 keep-alive:60s,HTTP服务器(nginx)返回响应以后,需要等待60s,60s之内客户端又发来请求,重置这个时间;
否则60s之内没有客户端发来的响应,Nginx也是最终会主动断开连接,此时Nginx可以调用ngx_reset_pool重置内存池了,等待下一次客户端的请求。
因此,Nginx内存池的设计适用于间歇性、短连接的服务。虽然有内存泄漏,但效率高,空间换时间。
如果是长连接,且小块内存分配、释放较多,最好用STL二级空间配置器,避免内存泄漏过多。