看了一个多月zend的内存管理,查了很多资料,看了书籍
<<unix高级环境编程>>和<<linux系统编程>>中进程环境以及系统内存管理部分的章节
参考了博客
(如何实现一个malloc)http://blog.codinglabs.org/articles/a-malloc-tutorial.html
(php内存管理)https://blog.csdn.net/luoyu_/article/details/82857778#articleHeader7
(php内存管理,看了一点视频,第一章还没看完。。,视频看起来太费劲)https://segmentfault.com/a/1190000018488313
(tipi 胖子的php内核解析)http://www.php-internals.com/book/?p=chapt06/06-00-memory-management
(关于首次适应算法和最佳适应算法)https://blog.csdn.net/weixin_39924920/article/details/81054205
学习了linux内存中的伙伴管理算法:
https://blog.csdn.net/qq_32783703/article/details/103736754
还有写了文章(linux内存管理的一些)
https://blog.csdn.net/qq_32783703/article/details/103516782
https://blog.csdn.net/qq_32783703/article/details/103763657
在工作中还是不建议自己去写这种内存管理的,需要技术底子非常的深,而且十分容易出bug,出了bug很难排查,当然我们在写php 扩展的时候可以设置一个ALLOC的环境变量,让申请直接走系统可以配合valgrind去排查问题,所以想要用内存池的话推荐直接用jemalloc,多线程用tcmalloc,不要自己写!!!!!!
之前说道https://blog.csdn.net/qq_32783703/article/details/103516782我们在写linux程序也好windows程序也罢,频繁的使用malloc和free有一个很大的缺陷就是会造成内存碎片,因为malloc可能把一个连续内存块造成不连续,而且还会产生向系统申请资源的浪费,我们也要注意malloc不是系统调用,不是系统调用,sbrk和brk才是!!!
之前有一张图很好的介绍了linux进程环境
自高往下是linux内核的一些地址,环境变量,参数 ,栈 ,映射段,堆,未初始化数据段,数据段,正文段
下面细说一下zend内存管理,在我读代码php-cgi或者写扩展的时候经常用的是emalloc而不是malloc,emalloc可以有效的减少内存碎片,和向系统申请的开销
#define emalloc(size) _emalloc((size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)
最后落到的代码段是在这里:
ZEND_API void* ZEND_FASTCALL _emalloc(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
#if ZEND_MM_CUSTOM
if (UNEXPECTED(AG(mm_heap)->use_custom_heap)) {
if (ZEND_DEBUG && AG(mm_heap)->use_custom_heap == ZEND_MM_CUSTOM_HEAP_DEBUG) {
return AG(mm_heap)->custom_heap.debug._malloc(size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
} else {
return AG(mm_heap)->custom_heap.std._malloc(size);
}
}
#endif
return zend_mm_alloc_heap(AG(mm_heap), size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
}
我们忽略掉非zend内存池的部分,直接看zend_mm_alloc_heap
AG这个宏就是一个全局结构体,由于c语言里没有对象的概念,所以结构体是一个及其重要的数据类型,其实如果你看过结构体和对象的底层结构其实都是一样的,因为都是一个连续的内存,在这里贴出alloc_globals这个详细的结构
typedef struct _zend_alloc_globals {
zend_mm_heap *mm_heap;
} zend_alloc_globals;
上面这个函数我们需要重点关注的是zend_mm_alloc_heap,我们看一下这个函数做了什么
static zend_always_inline void *zend_mm_alloc_heap(zend_mm_heap *heap, size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
void *ptr;
#if ZEND_DEBUG
size_t real_size = size;
zend_mm_debug_info *dbg;
/* special handling for zero-size allocation */
size = MAX(size, 1);
size = ZEND_MM_ALIGNED_SIZE(size) + ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info));
if (UNEXPECTED(size < real_size)) {
zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (%zu + %zu)", ZEND_MM_ALIGNED_SIZE(real_size), ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info)));
return NULL;
}
#endif
if (EXPECTED(size <= ZEND_MM_MAX_SMALL_SIZE)) {
ptr = zend_mm_alloc_small(heap, size, ZEND_MM_SMALL_SIZE_TO_BIN(size) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
#if ZEND_DEBUG
dbg = zend_mm_get_debug_info(heap, ptr);
dbg->size = real_size;
dbg->filename = __zend_filename;
dbg->orig_filename = __zend_orig_filename;
dbg->lineno = __zend_lineno;
dbg->orig_lineno = __zend_orig_lineno;
#endif
return ptr;
} else if (EXPECTED(size <= ZEND_MM_MAX_LARGE_SIZE)) {
ptr = zend_mm_alloc_large(heap, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
#if ZEND_DEBUG
dbg = zend_mm_get_debug_info(heap, ptr);
dbg->size = real_size;
dbg->filename = __zend_filename;
dbg->orig_filename = __zend_orig_filename;
dbg->lineno = __zend_lineno;
dbg->orig_lineno = __zend_orig_lineno;
#endif
return ptr;
} else {
#if ZEND_DEBUG
size = real_size;
#endif
return zend_mm_alloc_huge(heap, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
}
}
php对我们malloc的内存尺寸进行了分类,我们用一张图来更加清晰的看这个程序代码块
我们先来看一下zend_mm_alloc_small
static zend_always_inline void *zend_mm_alloc_small(zend_mm_heap *heap, size_t size, int bin_num ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
#if ZEND_MM_STAT
do {
size_t size = heap->size + bin_data_size[bin_num];
size_t peak = MAX(heap->peak, size);
heap->size = size;
heap->peak = peak;
} while (0);
#endif
if (EXPECTED(heap->free_slot[bin_num] != NULL)) {
zend_mm_free_slot *p = heap->free_slot[bin_num];
heap->free_slot[bin_num] = p->next_free_slot;
return (void*)p;
} else {
return zend_mm_alloc_small_slow(heap, bin_num ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
}
}
如果说free_slot这个规格上有这个规格的话直接从free_slot上去取然后转换成void*就可以我门上层没有任何应用感知,在这里我们要重点说一个结构zend_mm_heap这个zend自定义的堆结构,注意一个细节,当我们要使用这块地址的时候需要把他从free_slot上摘除掉
我们这里进一步要查看zend_mm_alloc_small_slow
static zend_never_inline void *zend_mm_alloc_small_slow(zend_mm_heap *heap, uint32_t bin_num ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
zend_mm_chunk *chunk;
int page_num;
zend_mm_bin *bin;
zend_mm_free_slot *p, *end;
#if ZEND_DEBUG
bin = (zend_mm_bin*)zend_mm_alloc_pages(heap, bin_pages[bin_num], bin_data_size[bin_num] ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
#else
bin = (zend_mm_bin*)zend_mm_alloc_pages(heap, bin_pages[bin_num] ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
#endif
if (UNEXPECTED(bin == NULL)) {
/* insufficient memory */
return NULL;
}
chunk = (zend_mm_chunk*)ZEND_MM_ALIGNED_BASE(bin, ZEND_MM_CHUNK_SIZE);
page_num = ZEND_MM_ALIGNED_OFFSET(bin, ZEND_MM_CHUNK_SIZE) / ZEND_MM_PAGE_SIZE;
chunk->map[page_num] = ZEND_MM_SRUN(bin_num);
if (bin_pages[bin_num] > 1) {
uint32_t i = 1;
do {
chunk->map[page_num+i] = ZEND_MM_NRUN(bin_num, i);
i++;
} while (i < bin_pages[bin_num]);
}
/* create a linked list of elements from 1 to last */
end = (zend_mm_free_slot*)((char*)bin + (bin_data_size[bin_num] * (bin_elements[bin_num] - 1)));
heap->free_slot[bin_num] = p = (zend_mm_free_slot*)((char*)bin + bin_data_size[bin_num]);
do {
p->next_free_slot = (zend_mm_free_slot*)((char*)p + bin_data_size[bin_num]);
#if ZEND_DEBUG
do {
zend_mm_debug_info *dbg = (zend_mm_debug_info*)((char*)p + bin_data_size[bin_num] - ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info)));
dbg->size = 0;
} while (0);
#endif
p = (zend_mm_free_slot*)((char*)p + bin_data_size[bin_num]);
} while (p != end);
/* terminate list using NULL */
p->next_free_slot = NULL;
#if ZEND_DEBUG
do {
zend_mm_debug_info *dbg = (zend_mm_debug_info*)((char*)p + bin_data_size[bin_num] - ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info)));
dbg->size = 0;
} while (0);
#endif
/* return first element */
return (char*)bin;
}
这里我们要重点看一下zend_mm_alloc_pages这个函数,看一下他的调用代码
bin = (zend_mm_bin*)zend_mm_alloc_pages(heap, bin_pages[bin_num]);
再看一个哎zend_small 中一个非常重要的宏定义
/* num, size, count, pages */
#define ZEND_MM_BINS_INFO(_, x, y) \
_( 0, 8, 512, 1, x, y) \
_( 1, 16, 256, 1, x, y) \
_( 2, 24, 170, 1, x, y) \
_( 3, 32, 128, 1, x, y) \
_( 4, 40, 102, 1, x, y ) \
_( 5, 48, 85, 1, x, y) \
_( 6, 56, 73, 1, x, y) \
_( 7, 64, 64, 1, x, y) \
_( 8, 80, 51, 1, x, y) \
_( 9, 96, 42, 1, x, y) \
_(10, 112, 36, 1, x, y) \
_(11, 128, 32, 1, x, y) \
_(12, 160, 25, 1, x, y) \
_(13, 192, 21, 1, x, y) \
_(14, 224, 18, 1, x, y) \
_(15, 256, 16, 1, x, y) \
_(16, 320, 64, 5, x, y) \
_(17, 384, 32, 3, x, y) \
_(18, 448, 9, 1, x, y) \
_(19, 512, 8, 1, x, y) \
_(20, 640, 32, 5, x, y) \
_(21, 768, 16, 3, x, y) \
_(22, 896, 9, 2, x, y) \
_(23, 1024, 8, 2, x, y) \
_(24, 1280, 16, 5, x, y) \
_(25, 1536, 8, 3, x, y) \
_(26, 1792, 16, 7, x, y) \
_(27, 2048, 8, 4, x, y) \
_(28, 2560, 8, 5, x, y) \
_(29, 3072, 4, 3, x, y)
第一列是健值,第二列是字节数,第三列是数量,第四页是对应的内存页页数,在这里我在看网上一些列子的时候说linux内存页是4k,但是linux系统编程中说到,32位是4k,64位是8k,在这里就按照4k来把
以3072为例子 3k*4 就是3个4k的内存页这里的count和pages就是这么算的zend_mm_alloc_pages传入的就是堆的地址heap和要申请的页数,zend_mm_alloc_pages是一个十分重要的函数他的作用是申请count个page,下面来一点点分析这个至关重要的函数
在zend_mm_heap中有一个free_pages字段用来记载可以用的空闲页,当我们申请的时候free_pages小于pages_count直接goto到not_found
if (UNEXPECTED(chunk->free_pages < pages_count)) {
goto not_found;
#if 0
...
#endif
}
我们再看一下not found 中做了什么?
如果main_chunk后面的下一个chunk就是main_chunk了(环形链表),首先看一下循环到的块是否是最后一块,如果不是那么就继续去找下一块,如果已经到最后一块了,会去看缓存块中有没有可用的,有的话从缓存块的链表中摘下来,然后把值给chunk,没有的话会用zend_mm_mmap去申请
if (chunk->next == heap->main_chunk) {
if (heap->cached_chunks) {
heap->cached_chunks_count--;
chunk = heap->cached_chunks;
heap->cached_chunks = chunk->next;
}
heap->chunks_count++;
if (heap->chunks_count > heap->peak_chunks_count) {
heap->peak_chunks_count = heap->chunks_count;
}
zend_mm_chunk_init(heap, chunk);
page_num = ZEND_MM_FIRST_PAGE;
len = ZEND_MM_PAGES - ZEND_MM_FIRST_PAGE;
goto found;
}
然后chunk的数字+1
heap->chunks_count++;
如果说当前使用的块大于峰值时候使用的块,则赋值peak_chunk_count,然后通过zend_mm_chunk_init给这个当前块初始化,然后直接跳入found,我们看一下found做了什么?
found:
if (steps > 2 && pages_count < 8) {
/* move chunk into the head of the linked-list */
chunk->prev->next = chunk->next;
chunk->next->prev = chunk->prev;
chunk->next = heap->main_chunk->next;
chunk->prev = heap->main_chunk;
chunk->prev->next = chunk;
chunk->next->prev = chunk;
}
/* mark run as allocated */
chunk->free_pages -= pages_count;
zend_mm_bitset_set_range(chunk->free_map, page_num, pages_count);
chunk->map[page_num] = ZEND_MM_LRUN(pages_count);
if (page_num == chunk->free_tail) {
chunk->free_tail = page_num + pages_count;
}
return ZEND_MM_PAGE_ADDR(chunk, page_num);
首先看一下chunk变化
我们发现他把获取到的chunk从原来的链表上摘除,然后移植到了main_chunk上,chunk->free_pages -= pages_count;在空闲页上减去要申请的页数
再说一下free_map这个标志位记录,我们看一下free_map的具体情况
typedef zend_mm_bitset zend_mm_page_map[ZEND_MM_PAGE_MAP_LEN]; /* 64B */
直接一点就是存了8个long,我们知道一个字节是8位,1个long就是64位,8个long就是512位,每一位用来标识一个内存页使用情况0代表未使用,1代表已经使用,使用zend_mm_bitset_set_range就是设置free_map的使用情况的
再来看map这个成员变量是做什么的
zend_mm_page_info map[ZEND_MM_PAGES];
这里有512个zend_mm_page_info指针用来存储内存页使用情况,每一个元素是一个uint32_t 4个字节
chunk->map[page_num] = ZEND_MM_LRUN(pages_count);
这个宏ZEND_MM_LRUN来设置这个内存页的使用情况
最后就是记录使用页的尾部
if (page_num == chunk->free_tail) {
chunk->free_tail = page_num + pages_count;
}
刚才看了存在cache的情况,再看一下没有cache块的情况,直接看一个函数zend_mm_chunk_alloc,这个函数实质上就是使用的mmap,我们看一下mmap这个函数,在linux内存中,超过128k的内存申请,linux系统编程中推荐使用的是mmap,我们看一下这个函数的mmap是文件映射还是匿名映射
static void *zend_mm_mmap(size_t size)
{
#ifdef _WIN32
void *ptr = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (ptr == NULL) {
#if ZEND_MM_ERROR
stderr_last_error("VirtualAlloc() failed");
#endif
return NULL;
}
return ptr;
#else
void *ptr;
#ifdef MAP_HUGETLB
if (zend_mm_use_huge_pages && size == ZEND_MM_CHUNK_SIZE) {
ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_HUGETLB, -1, 0);
if (ptr != MAP_FAILED) {
return ptr;
}
}
#endif
ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
if (ptr == MAP_FAILED) {
#if ZEND_MM_ERROR
fprintf(stderr, "\nmmap() failed: [%d] %s\n", errno, strerror(errno));
#endif
return NULL;
}
return ptr;
#endif
}
采取的匿名映射,因为fd参数是-1,并不是文件映射,mmap是把文件内容映射到内存中,我认为是映射在了进程栈和堆之间的位置(详细内容看最上面的图,进程环境的图,在stack和heap之间的位置),总结一点如果发现了chunk我们走的是found,直接从之前的chunk链表上摘除掉,然后放到main_chunk上,如果说not_found,我们就首先看cache_chunk, 然后如果没有缓存块就用zend_mm_chunk_alloc去申请chunk,以此来节约内存开销,看到这里就要继续去看另一部分代码段
就是关于最佳适应算法,和首次适合算法这两种内存算法,首次适合算法可以降低cpu减少时间复杂度,但是肯能会造成空间复杂度高一些,最佳适应算法时间复杂度高,需要预先读取,然后再找寻最适合的块,但是时间复杂度高一些
我们看一下zend是如何处理的
int best = -1;
uint32_t best_len = ZEND_MM_PAGES;
uint32_t free_tail = chunk->free_tail;
zend_mm_bitset *bitset = chunk->free_map;
zend_mm_bitset tmp = *(bitset++);
uint32_t i = 0;
while (1) {
/* skip allocated blocks */
while (tmp == (zend_mm_bitset)-1) {
i += ZEND_MM_BITSET_LEN;
if (i == ZEND_MM_PAGES) {
if (best > 0) {
page_num = best;
goto found;
} else {
goto not_found;
}
}
tmp = *(bitset++);
}
/* find first 0 bit */
page_num = i + zend_mm_bitset_nts(tmp);
/* reset bits from 0 to "bit" */
tmp &= tmp + 1;
/* skip free blocks */
while (tmp == 0) {
i += ZEND_MM_BITSET_LEN;
if (i >= free_tail || i == ZEND_MM_PAGES) {
len = ZEND_MM_PAGES - page_num;
if (len >= pages_count && len < best_len) {
chunk->free_tail = page_num + pages_count;
goto found;
} else {
/* set accurate value */
chunk->free_tail = page_num;
if (best > 0) {
page_num = best;
goto found;
} else {
goto not_found;
}
}
}
tmp = *(bitset++);
}
/* find first 1 bit */
len = i + zend_ulong_ntz(tmp) - page_num;
if (len >= pages_count) {
if (len == pages_count) {
goto found;
} else if (len < best_len) {
best_len = len;
best = page_num;
}
}
/* set bits from 0 to "bit" */
tmp |= tmp - 1;
}
其中:
/* skip allocated blocks */
while (tmp == (zend_mm_bitset)-1) {
i += ZEND_MM_BITSET_LEN;
if (i == ZEND_MM_PAGES) {
if (best > 0) {
page_num = best;
goto found;
} else {
goto not_found;
}
}
tmp = *(bitset++);
}
这一块说了是free_maps当前元素上64也都被标记为使用那么给i+64,如果已经找到最后一页了,那么直接让best>0,赋值给page_num,然后跳转到found,可见这个best 就是块上的最佳位置!!
再来看下面这一小块代码
/* find first 0 bit */
page_num = i + zend_mm_bitset_nts(tmp);
/* reset bits from 0 to "bit" */
tmp &= tmp + 1;
/* skip free blocks */
while (tmp == 0) {
i += ZEND_MM_BITSET_LEN;
if (i >= free_tail || i == ZEND_MM_PAGES) {
len = ZEND_MM_PAGES - page_num;
if (len >= pages_count && len < best_len) {
chunk->free_tail = page_num + pages_count;
goto found;
} else {
/* set accurate value */
chunk->free_tail = page_num;
if (best > 0) {
page_num = best;
goto found;
} else {
goto not_found;
}
}
}
tmp = *(bitset++);
}
这一块代码是如果当前的代码64页都没有使用的情况下运行,注意一个很关键的点!!!!
if (len >= pages_count && len < best_len) {
代码中的best就是我们要申请的块里的最佳的page_num
总结:这个最佳适应算法就是会找寻main_chunk上所有的块,去找寻最佳的chunk和page_num的位置
然后我们使用ZEND_MM_PAGE_ADDR(chunk, page_num);去获得偏移地址
#define ZEND_MM_PAGE_ADDR(chunk, page_num) \
((void*)(((zend_mm_page*)(chunk)) + (page_num)))
注意加上可page_num就是加上了page_num*4k,获取的是页的地址
也就是说zend_mm_alloc_pages找到了最佳的块以及页的位置,然后把这个块的页地址返回,就是说chunkd的页地址位置,这和chunk是2M的所以我们要偏移出页的位置,因为page是4k所以可以偏移的出page的地址,如图:
`
到此位置small的我们都看完了,large也是直接调用zend_mm_alloc_pages去申请,huge的话直接调用zend_mm_chunk_alloc去申请,然后会加入到zend_mm_heap的huge_list这个列表,调用zend_mm_add_huge_block
到此为止zend 申请内存的全部已经整理完了,我们再用一张图来进行总结
看完了zend 内存的申请我们最后再要看一个释放的过程
efree
核心代码是
ZEND_API void ZEND_FASTCALL _efree(void *ptr ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
#if ZEND_MM_CUSTOM
if (UNEXPECTED(AG(mm_heap)->use_custom_heap)) {
if (ZEND_DEBUG && AG(mm_heap)->use_custom_heap == ZEND_MM_CUSTOM_HEAP_DEBUG) {
AG(mm_heap)->custom_heap.debug._free(ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
} else {
AG(mm_heap)->custom_heap.std._free(ptr);
}
return;
}
#endif
zend_mm_free_heap(AG(mm_heap), ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
}
我们再看一下zend_mm_free_heap
static zend_always_inline void zend_mm_free_heap(zend_mm_heap *heap, void *ptr ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
size_t page_offset = ZEND_MM_ALIGNED_OFFSET(ptr, ZEND_MM_CHUNK_SIZE);
if (UNEXPECTED(page_offset == 0)) {
if (ptr != NULL) {
zend_mm_free_huge(heap, ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
}
} else {
zend_mm_chunk *chunk = (zend_mm_chunk*)ZEND_MM_ALIGNED_BASE(ptr, ZEND_MM_CHUNK_SIZE);
int page_num = (int)(page_offset / ZEND_MM_PAGE_SIZE);
zend_mm_page_info info = chunk->map[page_num];
ZEND_MM_CHECK(chunk->heap == heap, "zend_mm_heap corrupted");
if (EXPECTED(info & ZEND_MM_IS_SRUN)) {
zend_mm_free_small(heap, ptr, ZEND_MM_SRUN_BIN_NUM(info));
} else /* if (info & ZEND_MM_IS_LRUN) */ {
int pages_count = ZEND_MM_LRUN_PAGES(info);
ZEND_MM_CHECK(ZEND_MM_ALIGNED_OFFSET(page_offset, ZEND_MM_PAGE_SIZE) == 0, "zend_mm_heap corrupted");
zend_mm_free_large(heap, chunk, page_num, pages_count);
}
}
}
既然有对应的small,large和huge就会有对应的释放,但是这里我们需要思考一个核心的问题点,调用free的时候我们只是传入了一个参数的地址(注意这个地址是块上的page地址),那么他是如何通过这个参数地址来获取到我们malloc时候到底是小型的内存还是中型的内存,还是巨型内存呢?你可能也会说通过map来记录的页来表示,那么巨型内存没有map的记录他又怎么知道是一个巨型内存呢?
zend_mm_free_heap的时候,他会首先调用的宏是这个ZEND_MM_ALIGNED_OFFSET
#define ZEND_MM_ALIGNED_OFFSET(size, alignment) \
(((size_t)(size)) & ((alignment) - 1))
如果说page_offset大于0那么他就是一个巨型块
if (UNEXPECTED(page_offset == 0)) {
if (ptr != NULL) {
zend_mm_free_huge(heap, ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
}
}
如果说是0的话就去通过page的地址反推chunk的地址
zend_mm_chunk *chunk = (zend_mm_chunk*)ZEND_MM_ALIGNED_BASE(ptr, ZEND_MM_CHUNK_SIZE);
去看这个chunk的map对应的page_num的标记,标记这是长内存还是小内存
if (EXPECTED(info & ZEND_MM_IS_SRUN)) {
zend_mm_free_small(heap, ptr, ZEND_MM_SRUN_BIN_NUM(info));
} else /* if (info & ZEND_MM_IS_LRUN) */ {
int pages_count = ZEND_MM_LRUN_PAGES(info);
ZEND_MM_CHECK(ZEND_MM_ALIGNED_OFFSET(page_offset, ZEND_MM_PAGE_SIZE) == 0, "zend_mm_heap corrupted");
zend_mm_free_large(heap, chunk, page_num, pages_count);
}
注意一个细节 就是zend内存释放掉之后不会立马还给linux内核,而是把他挂载到了free_slot上
static zend_always_inline void zend_mm_free_small(zend_mm_heap *heap, void *ptr, int bin_num)
{
zend_mm_free_slot *p;
#if ZEND_MM_STAT
heap->size -= bin_data_size[bin_num];
#endif
#if ZEND_DEBUG
do {
zend_mm_debug_info *dbg = (zend_mm_debug_info*)((char*)ptr + bin_data_size[bin_num] - ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info)));
dbg->size = 0;
} while (0);
#endif
p = (zend_mm_free_slot*)ptr;
p->next_free_slot = heap->free_slot[bin_num];
heap->free_slot[bin_num] = p;
}
这是small类型申请的时候,我们再看一下large
static zend_always_inline void zend_mm_free_large(zend_mm_heap *heap, zend_mm_chunk *chunk, int page_num, int pages_count)
{
#if ZEND_MM_STAT
heap->size -= pages_count * ZEND_MM_PAGE_SIZE;
#endif
zend_mm_free_pages(heap, chunk, page_num, pages_count);
}
代码调用了zend_mm_free_pages
static zend_always_inline void zend_mm_free_pages_ex(zend_mm_heap *heap, zend_mm_chunk *chunk, uint32_t page_num, uint32_t pages_count, int free_chunk)
{
chunk->free_pages += pages_count;
zend_mm_bitset_reset_range(chunk->free_map, page_num, pages_count);
chunk->map[page_num] = 0;
if (chunk->free_tail == page_num + pages_count) {
/* this setting may be not accurate */
chunk->free_tail = page_num;
}
if (free_chunk && chunk->free_pages == ZEND_MM_PAGES - ZEND_MM_FIRST_PAGE) {
zend_mm_delete_chunk(heap, chunk);
}
}
他把这个块的chunk的map信息置为了0
chunk->map[page_num] = 0;
然后调用zend_mm_delete_chunk
static zend_always_inline void zend_mm_delete_chunk(zend_mm_heap *heap, zend_mm_chunk *chunk)
{
chunk->next->prev = chunk->prev;
chunk->prev->next = chunk->next;
heap->chunks_count--;
if (heap->chunks_count + heap->cached_chunks_count < heap->avg_chunks_count + 0.1
|| (heap->chunks_count == heap->last_chunks_delete_boundary
&& heap->last_chunks_delete_count >= 4)) {
/* delay deletion */
heap->cached_chunks_count++;
chunk->next = heap->cached_chunks;
heap->cached_chunks = chunk;
} else {
#if ZEND_MM_STAT || ZEND_MM_LIMIT
heap->real_size -= ZEND_MM_CHUNK_SIZE;
#endif
if (!heap->cached_chunks) {
if (heap->chunks_count != heap->last_chunks_delete_boundary) {
heap->last_chunks_delete_boundary = heap->chunks_count;
heap->last_chunks_delete_count = 0;
} else {
heap->last_chunks_delete_count++;
}
}
if (!heap->cached_chunks || chunk->num > heap->cached_chunks->num) {
zend_mm_chunk_free(heap, chunk, ZEND_MM_CHUNK_SIZE);
} else {
//TODO: select the best chunk to delete???
chunk->next = heap->cached_chunks->next;
zend_mm_chunk_free(heap, heap->cached_chunks, ZEND_MM_CHUNK_SIZE);
heap->cached_chunks = chunk;
}
}
}
他把这个chunk从main_chunk的链表上摘了下来,放到了cached_chunks上
总结:
zend内存池可以有效减少内存碎片和频繁向系统申请内存的开销,但是不推荐自己写程序时候自己设计内存池,还是推荐使用tcmalloc和jemalloc