网站引流推广软件,镇江专业网站建设,第三方平台推广,建设网站需要什么技术内存管理的意义#xff1a;内存是系统中重要的基本资源之一#xff0c;内存的管理是指其分配、使用和回收的管理#xff1b;保障各个程序内存的正常分配和回收。 虽然操作系统以及提供了一套内存管理的函数#xff0c;但是PHP还是自己实现了一套内存管理方案-PHP内存管理器… 内存管理的意义内存是系统中重要的基本资源之一内存的管理是指其分配、使用和回收的管理保障各个程序内存的正常分配和回收。 虽然操作系统以及提供了一套内存管理的函数但是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/4KB512个page如图2所示。 图2 chunk和page示意图
在PHP 7中对于chunk大块内存的申请是使用mmap函数实现的其中mmap函数原型如下
/* MAP_FIXED leads to discarding of the old mapping, so it cant 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对齐则对齐后的内存应该是512B256的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 内存地址对齐示例
申请0x10000x1000-0x00010x1fff的内存也就是多申请0xfff的内存比如申请到的起始地址为0x103c60120结束地址为0x103c6211f因为此时的地址不是0x1000对齐的因为0x103c60120不是0x1000的整数倍所以要进行对齐操作。为了对齐先释放0x103c60120到0x103c61000恰好是起始地址和结束地址区间内0x1000的整数倍的0xee0长度的内存起始保证了起始地址为0x103c61000是与0x1000对齐的。释放0x103c62000到0x103c6211f的0x11f长度内存两次释放的内存长度0xee00x11f0xfff恰好为多申请的长度。剩下的即为需要的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_sizesize代表的是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时返回的是sizesize和real_size会在申请和释放内存时进行修改。 peak/real_peakpeak是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-9503。 PHP7page使用情况分析
free_tail此chunk的最后一块连续可用page的起始编号主要用于快速查找连续可用page此值并不准确但不影响最后结果如图9-5所示free_tail应该为363。free_map在64位机器下其为8个元素的数组每个元素为64bit的整型所以一共有8×64bit512bit对应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× 8512bit这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语言开发中的结构体中尤为常见用于结构体版本升级之类。10heap_slot在MM进行初始化时会创建第一个chunk而第一个chunk的此字段才有意义。其实全局指针alloc_globals.mm_heap指向的便是第一个chunk的heap_slot。
每申请一个chunk都需要对chunk进行初始化大致流程如下所示。 将此chunk放入环状双向链表main_chunk的最后面。 将free_pages置为512-1511第0个page被chunk的头信息占用。 将free_tail置为1。 将num在上一个元素的计数基础上加1chunk-prev-num1。 将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进行内存管理的常用方法内存池中空闲内存块组织为链表结果申请内存只需要查找空闲链表即可释放内存需要将内存块重新插入空闲链表
3PHP采用预分配内存策略提前向操作系统分配2M字节大小内存称为chunk同时将内存分配请求根据字节大小分为small、huge、large三种
4small内存采用“分离存储”思想将空闲内存块按照字节大小组织为多个空闲链表
5large内存每次回分配连续若干个页采用最佳适配算法
6huge内存直接使用mmap函数向操作系统申请内存申请大小是2M字节整数倍
7chunk中的每个页只会被切割为相同规格的内存块所以不需要再每个内存块添加头部只需要记录每个页的属性即可
8如何方便根据地址计算当前内存块属于chunk中的哪一个页PHP分配的chunk都是2M字节对齐的任意地址的低21位即是相对chunk首地址除以页大小则可获得页号 未完待续