制作精美网站建设独立800元做小程序网站
内存管理的意义:内存是系统中重要的基本资源之一,内存的管理是指其分配、使用和回收的管理;保障各个程序内存的正常分配和回收。
虽然操作系统以及提供了一套内存管理的函数,但是PHP还是自己实现了一套内存管理方案-PHP内存管理器(Zend Memory Manager简称MM)如下图:
PHP7内存管理器示意图
从图中可以看出PHP脚本运行所需内存不是直接从系统调用的,而是先通过内存管理器提供的一系列API接口(zend-mm-alloc-small、alloc-large、alloc-huge等,alloc意思为分配,huge为超大)申请:如果MM中有足够的内存,则直接分配给脚本;如果MM中不够用,则MM再向系统申请。这样可以有效减少PHP向系统调用的次数,并且优化内存空间使用效率。因为C、C++需要手动申请和释放内存,所以其比PHP开发要难。
在此引入一个内存池的概念:提供了一个更有效率的解决方案,即预先规划一定数量的内存区块,使得整个程序可以在运行期规划(allocate)、使用(access)、归还(free)内存区块。一个池子无非就是先占用一块内存,然后给需要的人使用。
内存管理准备知识
据PHP 7核心开发者描述,PHP 7在内存管理上的CPU时间节省达到了21%,提升巨大。
PH7其实是借鉴了前辈的内存管理方案:jemalloc和tcmalloc,这两个分别是火狐和chrome两大浏览器的内存管理器。这种内存管理器的内存分配思想大致就是:先申请一大块内存,自己先占着,然后再按照大中小三种规格分割成小块,放在内存池中。当程序申请内存时,MM从池子中挑选合适大小的内存给程序。
基本概念
PHP7内存管理器的的代码是在php-7.x.x/Zend/zenc_alloc.c中实现的。它维护了三种规格的内存,分别是chunk、page、slot;
这三种大小是在php-7.x.x/Zend/zenc_alloc_sizes.h中定义的:
#define ZEND_MM_CHUNK_SIZE (2 * 1024 * 1024) /* 2 MB */
#define ZEND_MM_PAGE_SIZE (4 * 1024) /* 4 KB */
#define ZEND_MM_PAGES (ZEND_MM_CHUNK_SIZE / ZEND_MM_PAGE_SIZE) /* 512 */
page是在chunk中分配的,那么一个chunk可以分为2MB/4KB=512个page,如图2所示。
图2 chunk和page示意图
在PHP 7中,对于chunk大块内存的申请是使用mmap函数实现的,其中mmap函数原型如下:
/* MAP_FIXED leads to discarding of the old mapping, so it can't be used. */
void *ptr = mmap(addr, size, PROT_READ | PROT_WRITE, flags /*| MAP_POPULATE | MAP_HUGETLB*/, ZEND_MM_FD, 0);//PHP7中对应的调用如下
ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_HUGETLB, -1, 0);
各个参数的含义如下:
-
start:映射区开始地址,0表示由系统决定的起始地址,PHP7传入的NULL,也就是0
-
length:映射区长度,以字节为单位,不足一页时按一页处理
-
prot
:期望的内存保护标志不能与文件的打开方式冲突。prot可以是以下的某个值,且可以使用or将合理的组合在一起:
- PROT_EXEC:页内容可执行
- PROT_READ:页内容可读取
- PROT_WRITE:页可以写入
- PROT_NONE:页不可访问
PHP7中的为PROT_READ | PROT_WRITE,即可读写
- flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个位的组合体,PHP 7使用的是MAP_PRIVATE | MAP_ANON,前者是建立一个写入时复制的私有映射,后者表示匿名映射,映射区不与任何文件关联。
- fd:有效的文件描述词。PHP 7中设置为-1,此时需要指定flags参数中的MAP_ANON,表明进行的是匿名映射。
- off_toffset:被映射对象内容的起点,PHP 7中设置为0。
PHP 7通过调用mmap函数,返回一大块内存,一般是chunk大小的倍数,后面的内存管理工作在这一大块内存上进行操作。
PHP 7的MM将申请内存按大小分成了3类:small内存、large内存、huge内存。
- small内存:小于等于3KB的内存。
- large内存:大于3KB且小于等于(2MB-4KB)的内存,可以对应整数倍的page,之所以要减掉4KB一个page的大小,后面会详细展开。
- huge内存:大于2MB-4KB的内存,可以直接对应整数倍的chunk。
与mmap相反的操作是int munmap(void *start, size_t length),用来取消参数start所指的映射内存起始地址,参数length则是欲取消的内存大小,该函数在释放内存的时候使用。
内存对齐
在用C/C++进行软件开发、申请内存时,编译器可以帮我们实现内存对齐,虽然看上去浪费了内存,但是提升了CPU访问内存的速度。
对齐举例:在PHP 7的内存池管理中,比如我们申请300B的内存,如果以256B对齐,则对齐后的内存应该是512B(256的2倍)。
PHP7中的内存对齐主要用到一下三个宏
//还是在zend_alloc.c中
#define ZEND_MM_ALIGNED_OFFSET(size, alignment) \(((size_t)(size)) & ((alignment) - 1))
#define ZEND_MM_ALIGNED_BASE(size, alignment) \(((size_t)(size)) & ~((alignment) - 1))
#define ZEND_MM_SIZE_TO_NUM(size, alignment) \(((size_t)(size) + ((alignment) - 1)) / (alignment))
如何理解这几个宏呢?下面举例来说明一下,假如要申请一个大小为4KB的内存,并以0x1000对齐,如图3所示。
图3 内存地址对齐示例
- 申请0x1000+0x1000-0x0001=0x1fff的内存(也就是多申请0xfff的内存),比如申请到的起始地址为0x103c60120,结束地址为0x103c6211f;因为此时的地址不是0x1000对齐的(因为0x103c60120不是0x1000的整数倍),所以要进行对齐操作。
- 为了对齐,先释放0x103c60120到0x103c61000(恰好是起始地址和结束地址区间内0x1000的整数倍)的0xee0长度的内存,起始保证了起始地址为0x103c61000,是与0x1000对齐的。
- 释放0x103c62000到0x103c6211f的0x11f长度内存(两次释放的内存长度0xee0+0x11f=0xfff,恰好为多申请的长度)。
- 剩下的即为需要的0x1000长度,起始地址为0x103c61000,结束地址为0x103c62000的内存。
使用此内存时,比如有一内存地址为0x103c61120,通过宏计算,可以得出,此内存所在的page的起始地址为0x103c61000,在此page的偏移量为0x120,能够快速定位内存地址所在的page,提高效率。
以上是内存管理的概念和内存对齐方法
内存管理的数据结构
PHP7的内存管理用到了一些结构体,其中核心的结构体有zend_mm_heap、zend_mm_page、zend_mm_chunk。其中zend_mm_page最简单,对应的是4KB的char数组,下面对zend_mm_heap和zenc_mm_chunk进行讨论。
_zend_mm_heap
以下为_zend_mm_heap的结构体定义
struct _zend_mm_heap {
#if ZEND_MM_CUSTOMint use_custom_heap;
#endif
#if ZEND_MM_STORAGEzend_mm_storage *storage;
#endif
#if ZEND_MM_STATsize_t size; /* current memory usage */size_t peak; /* peak memory usage */
#endifzend_mm_free_slot *free_slot[ZEND_MM_BINS]; /* free lists for small sizes */
#if ZEND_MM_STAT || ZEND_MM_LIMITsize_t real_size; /* current size of allocated pages */
#endif
#if ZEND_MM_STATsize_t real_peak; /* peak size of allocated pages */
#endif
#if ZEND_MM_LIMITsize_t limit; /* memory limit */int overflow; /* memory overflow flag */
#endifzend_mm_huge_list *huge_list; /* list of huge allocated blocks */zend_mm_chunk *main_chunk;zend_mm_chunk *cached_chunks; /* list of unused chunks */int chunks_count; /* number of allocated chunks */int peak_chunks_count; /* peak number of allocated chunks for current request */int cached_chunks_count; /* number of cached chunks */double avg_chunks_count; /* average number of chunks allocated per request */int last_chunks_delete_boundary; /* numer of chunks after last deletion */int last_chunks_delete_count; /* number of deletion over the last boundary */
#if ZEND_MM_CUSTOMunion {struct {void *(*_malloc)(size_t);void (*_free)(void*);void *(*_realloc)(void*, size_t);} std;struct {void *(*_malloc)(size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);void (*_free)(void* ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);void *(*_realloc)(void*, size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);} debug;} custom_heap;HashTable *tracked_allocs;
#endif
};
下面解释下变量的含义。
-
size/real_size:size代表的是MM当前申请的已使用的内存,real_size还包括申请的未使用的内存;可以通过PHP的函数memory_get_usage来获取,其PHP函数原型如下:
int memory_get_usage([bool $real_usage = false])
$real_usage默认为false,只返回使用的内存大小;对于true的情况,会返回包括没有使用的分配内存的大小。在PHP7的源码中有对应的实现:
ZEND_API size_t zend_memory_usage(int real_usage) { #if ZEND_MM_STATif (real_usage) {return AG(mm_heap)->real_size;} else {size_t usage = AG(mm_heap)->size;return usage;} #endifreturn 0; }
从源码中可以看出参数为true时,返回的是real_size;当为false时,返回的是size;size和real_size会在申请和释放内存时进行修改。
-
peak/real_peak:peak是emalloc上报的内存峰值,可以通过PHP的函数memory_get_peak_usage来获取,其PHP函数的原型如下:
int memory_get_peak_usage([bool $real_usage = false])
$real_usage默认为false,只返回emalloc上报的内存峰值大小;对于true的情况,会返回内存分配峰值的大小;在PHP7的源码中,有对应的实现:
ZEND_API size_t zend_memory_peak_usage(int real_usage){#if ZEND_MM_STATif (real_usage) {return AG(mm_heap)->real_peak;} else {return AG(mm_heap)->peak;}#endifreturn 0;}
从源码中,可以看出true时返回的是real_peak,同样,在申请和释放内存时real_peak和peak也会进行修改。
-
free_slot:指针数组,存储30种规格的small内存链表的首地址
-
limit:存储在MM可申请内存的最大值,MM每当向系统申请chunk或huge的内存时,会判断申请后的内存值是否大于limit,如果大于,则进行垃圾回收。该参数可以通过php.ini中的memory_limit配置。
-
overflow:当申请的内存总数超出MM的limit时,先进行垃圾回收,如果回收失败,则判断overflow是否为1,如果是1则抛出异常,中断进程(PHP项目中经常遇到的allowed memory size of ** byte exhausted tried to allocate ** bytes就是这样跑出来的)
-
main_chunk:双向链表,存储使用中的chunk的首地址
-
cached_chunks:双向链表,缓存的chunk的首地址
-
chunks_count:使用中的chunk个数,也就是链表main_chunk中的元素个数。
-
peak_chunks_count:此次http请求中申请的chunk个数最大值,初始化为1,且每次请求开始都会重置为1
-
cached_chunks_count:缓存中的chunk个数,也就是链表cached_chunks中的元素个数
-
avg_chunks_count:历次请求使用chunk的个数平均值,初始值为1.0,每次请求结束时,会重新计算此值,置为avg_chunks_count和peak_chunks_count的平均值。
对于chunk相关的变量,会在后续chunk章节详细展开
-
huge_list:用以挂载分配的大块内存的单向列表,方便后续MM关闭时释放。
结构体_zend_mm_heap本身是要占内存的,也保存在内存管理申请的内存中。
_zend_mm_heap中有一个非常重要的结构——_zend_mm_chunk,下面讨论一下这个结构体。
_zend_mm_chunk
PHP 7的MM是一个多级内存分配器——预先定义内存块级别,按需要分配空间的大小找到对应级别,对齐分配。前文提到,chunk大小为2MB;每个chunk可以切割为512个page,一个page是4KB。在chunk内部,以page为单位进行管理。参考以下宏:
#define ZEND_MM_CHUNK_SIZE (2 * 1024 * 1024) /* 2 MB */
#define ZEND_MM_PAGE_SIZE (4 * 1024) /* 4 KB */
#define ZEND_MM_PAGES (ZEND_MM_CHUNK_SIZE / ZEND_MM_PAGE_SIZE) /* 512 */
一个chunk大小为2MB, MM管理chunk的变量,使用的是结构体_zend_mm_chunk:
struct _zend_mm_chunk {zend_mm_heap *heap;zend_mm_chunk *next;zend_mm_chunk *prev;uint32_t free_pages; /* number of free pages */uint32_t free_tail; /* number of free pages at the end of chunk */uint32_t num;char reserve[64 - (sizeof(void*) * 3 + sizeof(uint32_t) * 3)];zend_mm_heap heap_slot; /* used only in main chunk */zend_mm_page_map free_map; /* 512 bits or 64 bytes */zend_mm_page_info map[ZEND_MM_PAGES]; /* 2 KB = 512 * 4 */
};
各变量的含义如下。
- heap:zend_mm_heap类型的指针,对应的是9.3.1节中AG里面的mm_heap的地址。
- next:zend_mm_chunk类型的指针,指向下一个chunk。
- prev:zend_mm_chunk类型的指针,指向上一个chunk。由next/prev可见zend_mm_chunk是双向链表。
- free_pages:此chunk中可用的page个数,如图9-5所示,此chunk一共使用了9个page,则free_pages为512-9=503。
PHP7page使用情况分析
- free_tail:此chunk的最后一块连续可用page的起始编号,主要用于快速查找连续可用page,此值并不准确,但不影响最后结果,如图9-5所示,free_tail应该为363。
- free_map:在64位机器下,其为8个元素的数组,每个元素为64bit的整型,所以一共有8×64bit=512bit,对应512个page。已使用的page,对应的bit置为1,灰色部分;未使用(可用)的page,对应的bit置为0,白色部分,如图所示。
free_map对应的512bit
-
map:512个元素的数组,每个元素为一个32bit的整型,用来记录每个page的使用情况,比较复杂,如图所示。
PHP7内存管理large内存的map使用情况示例s
高位的2个bit,用于标记此page的使用类型,有4种情况:0x0、0x1、0x2、0x3,其中0x0代表此page未使用,0x1代表此page用于large内存,0x2和0x3均代表此page用于small内存。当此page用于large内存时,如果低位的10个bit为0,则代表此page被其前面且连续的page一起用于一次申请的内存;如果非0,假定值为page_count,则代表此page开始的连续page_count个page一起用于一次申请的内存,比如图9-6中一次申请了3个连续的page,起始编号为360,那么map[360]、map[361]、map[362]的低10位分别为3、0、0。
注意free_map是8× 8B,也就是8× 8× 8=512bit,这512个bit对应512个page,每个bit只能取0或者1,代表对应page的使用情况。而map是512个uint32_t,也就是512× 4B,每一个uint32_t代表一个page的使用情况。
-
num:代表此chunk在链表main_chunk中的编号,很明显,当申请第一个chunk时,num为0。对于非第一个chunk, num的值为在前一个chunk的num上加1。
-
reserve:保留字段,在C语言开发中的结构体中尤为常见,用于结构体版本升级之类。10)heap_slot:在MM进行初始化时,会创建第一个chunk,而第一个chunk的此字段,才有意义。其实全局指针alloc_globals.mm_heap指向的便是第一个chunk的heap_slot。
每申请一个chunk,都需要对chunk进行初始化,大致流程如下所示。
-
将此chunk放入环状双向链表main_chunk的最后面。
-
将free_pages置为512-1=511(第0个page被chunk的头信息占用)。
-
将free_tail置为1。
-
将num在上一个元素的计数基础上加1(chunk->prev->num+1)。
-
将free_map[0]标记为1,代表第0个被使用。
-
将map[0]标记为0x40000000 | 0x01,0x40000000代表第0个page使用large内存,0x01代表从第0个page起,连续1个page被使用。
_zend_mm_chunk本身是要占用内存的,我们输出_zend_mm_chunk的size:
(gdb) p sizeof(zend_mm_chunk) $3 = 2552
这个结构体占了2552B,它存放在chunk的第0个page上,如图所示。
内存管理chunk和page在MM中的位置
当申请一个chunk时,MM先判断双向链表cached_chunks是否存在chunk,如果不存在,则直接向操作系统申请一个地址以2MB对齐的chunk,添加到main_chunk中,然后返回给申请者;如果cached_chunks中存在chunk,则讲头部的chunk摘除,然后添加chunk进行初始化,一个chunk被分成512个page,其中511个page可用,第0个page用于存放这个chunk的管理结构体struct_zend_mm_chunk。
释放一个chunk时,MM先将此chunk从main_chunk中移除,并将chunks_count减一。然后判断当前使用的chunk数是否小于历次请求使用的chunk个数平均值avg_chunks_count。如果小于,则将此chunk放入双向链表cached_chunks中;如果不小于,则直接向操作系统释放此块内存。
到此我们研究了AG里面mm_heap的结构,以及chunk和page结构和相互关系,有了这些准备后,再来看下PHP内存管理的详细实现。
PHP内存管理器初始化流程
PHP内存管理器初始化流程
内存分配的函数调用流程
可在php7.x.x/Zend/zend_alloc.c中搜索_emalloc追溯相关代码
PHP内存分配函数调用流程
内存释放的函数调用流程
ZEND_API void ZEND_FASTCALL _efree(void *ptr)
{zend_mm_free_heap(AG(mm_heap), ptr);
}static zend_always_inline void zend_mm_free_heap(zend_mm_heap *heap, void *ptr)
{//计算当前地址ptr相对于chunk的偏移size_t page_offset = ZEND_MM_ALIGNED_OFFSET(ptr, ZEND_MM_CHUNK_SIZE);//偏移为0,说明是huge内存,直接释放if (UNEXPECTED(page_offset == 0)) {if (ptr != NULL) {zend_mm_free_huge(heap, ptr);}} else {//计算chunk首地址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];//small内存if (EXPECTED(info & ZEND_MM_IS_SRUN)) {zend_mm_free_small(heap, ptr, ZEND_MM_SRUN_BIN_NUM(info));}//large内存else /* if (info & ZEND_MM_IS_LRUN) */ {int pages_count = ZEND_MM_LRUN_PAGES(info);//将页标记为空闲zend_mm_free_large(heap, chunk, page_num, pages_count);}}
}static zend_always_inline void zend_mm_free_small(zend_mm_heap *heap, void *ptr, int bin_num)
{zend_mm_free_slot *p;//插入空闲链表头部即可p = (zend_mm_free_slot*)ptr;p->next_free_slot = heap->free_slot[bin_num];heap->free_slot[bin_num] = p;
}
内存释放函数调用关系
PHP内存管理总结
1)需要明白一点:任何内存分配器都需要额外的数据结构来记录内存的分配情况;
2)内存池是代替直接调用malloc/free、new/delete进行内存管理的常用方法;内存池中空闲内存块组织为链表结果,申请内存只需要查找空闲链表即可,释放内存需要将内存块重新插入空闲链表;
3)PHP采用预分配内存策略,提前向操作系统分配2M字节大小内存,称为chunk;同时将内存分配请求根据字节大小分为small、huge、large三种;
4)small内存,采用“分离存储”思想;将空闲内存块按照字节大小组织为多个空闲链表;
5)large内存每次回分配连续若干个页,采用最佳适配算法;
6)huge内存直接使用mmap函数向操作系统申请内存(申请大小是2M字节整数倍);
7)chunk中的每个页只会被切割为相同规格的内存块;所以不需要再每个内存块添加头部,只需要记录每个页的属性即可;
8)如何方便根据地址计算当前内存块属于chunk中的哪一个页?PHP分配的chunk都是2M字节对齐的,任意地址的低21位即是相对chunk首地址,除以页大小则可获得页号;
未完待续