当前位置: 首页 > 后端技术 > PHP

【PHP7源码学习】2019-03-07PHP内存管理1笔记

时间:2023-03-29 18:18:22 PHP

baiyan所有视频:https://segmentfault.com/a/11...源视频地址:http://replay.xesv5.com/ll/24...malloc和free函数考虑字符串的非二进制安全性:如果存储的字符串内容包含\0,则视为字符串结束,提前确定字符串结束而不是字符字符串的结尾正常结束。malloc一块内存后,它的大小存在于哪里?free一块内存,怎么知道有多少freememory?解决方案:在分配的内存地址空间附近分配额外的内存记录大小。malloc()系统调用非常慢,相当于去面包店买面包,需要揉面、发酵等一系列繁琐的工作,以避免多次从用户态切换到内核对于内存分配模式,PHP会先向操作系统内核申请一块大内存空间,然后用户态PHP虚拟机会实现自己的一套内存管理机制来管理这块大内存。以后申请内存只需要PHP虚拟机实现具体的内存分配操作,不需要再向内核申请。这减少了操作系统的内存申请次数,以空间换取时间,避免了频繁的系统调用,提高了性能。操作系统内存块基本概念:2MB大小内存页:4KB大小内存两者关系:一个chunk由512页组成(2MB/4KB=512)(512=2^9,4KB=2^12,2MB=2^二十一)。mmap():将文件或其他对象映射到内存并分配块的系统调用。内存不够怎么办:选择一个占用内存多的进程挂起,把它占用的内存换到硬盘分配给新的进程。当被挂起的进程被唤醒时,被唤醒的进程会在内存中寻找对应的页,但是找不到,于是产生缺页中断,操作系统会将硬盘中的页替换回内存中,供唤醒进程继续运行。PHP内存分配PHP内存分类:small/large/hugesmall:size<=3KB(有30种规格)large:3KB=2MB-4KB思考:为什么大规格内存<=2MB-4KB?答:在small和large内存分配规范中,先分配一个chunk,在chunk的第一页存放一个结构体,用于存放chunk中每一页的相关存储信息(即mm_heap在zend_alloc.h结构中)。而huge规格则不需要,直接使用即可(因为huge内存已经大于2MB(即一个chunk)),不需要记录内部存储细节)。想一个问题:一个页面有4KB,至于用这么大的空间来保存吗?struct_zend_mm_heap{#ifZEND_MM_CUSTOMintuse_custom_heap;#endif#ifZEND_MM_STORAGEzend_mm_storage*storage;#endif#ifZEND_MM_STATsize_tsize;/*当前使用的内存(PHP库函数memory_get_usage()方法就是获取该字段的值)*/size_tpeak;/*内存使用峰值*/#endifzend_mm_free_slot*free_slot[ZEND_MM_BINS];/*是一个大小为ZEND_MM_BINS=30的数组,每个存储单元是一个单向链表,用于挂载30个不同规格的smallMemory(下面会讲到)*/#ifZEND_MM_STAT||ZEND_MM_LIMITsize_treal_size;/*当前分配页面的大小*/#endif#ifZEND_MM_STATsize_treal_peak;/*分配页面的峰值大小*/#endif#ifZEND_MM_LIMITsize_tlimit;/*内存限制*/intoverflow;/*内存溢出标志*/#endifzend_mm_huge_list*huge_list;/*巨大的分配块列表*/zend_mm_chunk*主块;zend_mm_chunk*缓存块;/*未使用的块列表*/intchunks_count;/*分配的块数*/intpeak_chunks_count;/*当前请求分配块的峰值数量*/intcached_chunks_count;/*缓存块的数量*/doubleavg_chunks_count;/*每个请求分配的平均块数*/intlast_chunks_delete_boundary;/*最后一次删除后的块数*/intlast_chunks_delete_count;/*最后一个边界的删除数*/#ifZEND_MM_CUSTOMunion{struct{void*(*_malloc)(size_t);无效(*_free)(无效*);void*(*_realloc)(void*,size_t);}标准;struct{void*(*_malloc)(size_tZEND_FILE_LINE_DCZEND_FILE_LINE_ORIG_DC);空白(*_免费)(无效*ZEND_FILE_LINE_DCZEND_FILE_LINE_ORIG_DC);void*(*_realloc)(void*,size_tZEND_FILE_LINE_DCZEND_FILE_LINE_ORIG_DC);调试;}custom_heap;#endif};重点看中文注释的部分,我们看到zend_mm_heap结构不是很大,接下来继续关注关键的zend_mm_chunk结构:struct_zend_mm_chunk{zend_mm_heap*heap;zend_mm_chunk*下一个;zend_mm_chunk*prev;uint32_tfree_pages;/*空闲页数*/uint32_tfree_tail;/*块的空闲页数*/uint32_tnum;charreserve[64-(sizeof(void*)*3+sizeof(uint32_t)*3)];zend_mm_heapheap_slot;/*仅在主块中使用*/zend_mm_page_mapfree_map;/*512位或64字节*/zend_mm_page_infomap[ZEND_MM_PAGES];/*2KB=512*4*/};1个chunk有512个pagesfirstheap字段是一个zend_mm_heap类型的结构体指针,方便直接在512pages中找到存放附加信息的第一个page。下面两个next和prev字段构成了一个双向链表,用于chunk之间的连接。关注zend_mm_page_map类型的倒数第二个字段,有512位(即64B),每一位用于存储当前chunk中的每一页是否被使用(因为一共有512页)。除了检查是否被使用外,还需要检查每页的具体尺寸和其他相关情况,并记录相关信息。zend_mm_page_info类型的最后一个字段解决了上面的问题。ZEND_MM_PAGES=(ZEND_MM_CHUNK_SIZE/ZEND_MM_PAGE_SIZE)=2MB/4KB=512个存储单元,每个存储单元为typedefuint32_tzend_mm_page_info类型,即4B大小用于存储额外的使用信息,该字段一共2KB。它是用来存储大小和使用的附加信息的,所以这里2KB加上剩下的字段会用掉差不多4KB的空间,所以需要保留chunk的第一页来记录这些信息。然后这个page_info(uint32_t),这里的32位怎么用,先看内存规格怎么标记#defineZEND_MM_IS_FRUN0x00000000#defineZEND_MM_IS_LRUN0x40000000#defineZEND_MM_IS_SRUN0x80000000第32位为1表示小内存,也就是0x8。然后低5位(2^5>30规格)存储bit_num(即ZEND_MM_BINS_INFO宏的索引,以便快速找到需要的页数等信息(下文介??绍))。第31位为1表示大内存,即0x4。然后低10位表示分配的页数(>=512就够了)。为什么不使用9位?2^9=512够吗???第31位和第32位均为1,即0xC,低5位表示bit_num,其中3KB规格为29,16~25位表示偏移量。以申请3KB为例,必须有3个4KB的页面,分成4个3KB的页面(见下图)。前3KB以0xC为起始标志(约定)连续分配4个3KB,偏移量为0,bit_num为29;第二个3KB是0x8,偏移量是1,bit_num也是29;第三,四个3KB内存相同,只是偏移量不同。_emalloc()->zend_mm_alloc_heap()->分配三种内存规格之一:staticzend_always_inlinevoid*zend_mm_alloc_heap(zend_mm_heap*heap,size_tsizeZEND_FILE_LINE_DCZEND_FILE_LINE_ORIG_DC){void*ptr;if(size<=pSEND_MM_SIMAX_){zend_mm_alloc_small(heap,size,ZEND_MM_SMALL_SIZE_TO_BIN(size)ZEND_FILE_LINE_RELAY_CCZEND_FILE_LINE_ORIG_RELAY_CC);返回指针;}elseif(size<=ZEND_MM_MAX_LARGE_SIZE){ptr=zend_mm_alloc_large(heap,sizeZEND_FILE_LINE_RELAY_CCZEND_FILE_LINE_ORIG_RELAY_CC);返回指针;}else{returnzend_mm_alloc_huge(heap,sizeZEND_FILE_LINE_RELAY_CCZEND_FILE_LINE_ORIG_RELAY_CC);}}smallmemory注意一个page可以包含多个smallmemoryblock,首先一次分配的pages总量要能除以分配的size。也就是说,尽量不要有内存碎片。例如:分配3KB的小内存,不能只分配一页,因为会留下1KB大小的内存碎片,无法再利用和分配。因此,需要得到一个规格和4KB的最小公倍数作为总分配空间。但如果最小公倍数过大,导致需要的页数过多,那么退而求其次,即使剩下的内存碎片很少也能满足需求。下面举几个例子:例1:如果分配8B的小内存:我们先找到能分8B的最小pagesize。因为4KB可以分成8B,所以我们可以取1页,所以可以分成512个8B内存。我们取1个8B内存返回给用户,然后剩下的511个8B内存会挂在zend_mm_heap结构体的zend_mm_free_slot*free_slot[ZEND_MM_BINS]字段中。这里ZEND_MM_BINS宏=30,正好这30个数组单元对应30种小内存规格,一共有30个单向链表,每个单向链表挂载多分配内存。对于上面例子中的8B内存,会挂载分配的剩余511个8B内存。下次再次有分配请求时,直接从链表中取出即可,无需重新分配。同时也避免了内存碎片的产生。释放内存时,直接使用头插入法(相比尾插入法的复杂度O(1))插入到链表的头部即可。小内存的最小分配规格是8B,因为指针在64位系统下是8B,可以满足free_slot字段中单向链表的要求。例2:如果分配的是3KB规格的小内存:我们也是先找一个能被3KB整除的页大小。因为4KB不能分成3KB,虽然1页可以满足3KB的分配要求,但是还是会有1KB的内存碎片,所以我们不能直接使用1页。我们取最小的4KB能被3KB整除的倍数,得到3(4KB*3/3KB=4)个3KB大小的内存块的值。然后我们也拿一块3KB内存返回给用户,剩下的3块3KB内存挂在上面zend_mm_heap结构中free_slot字段的链表上,等待后续的分配请求。例3:分配40B小内存,为什么下面的代码只用了一页?是因为4KB和40B的最小公倍数太大,导致一次性申请的页面太多,不能盲目求两者的最小公倍数,所以退而求其次只分配1页。虽然4096/40=102.5,但是代码直接取102作为最终分配数量。第一个返回给用户,剩下的101个挂在free_slot链表上等待后续的分配请求。下面都是小内存分配规范:/*bin_num,size,count,pages*//*数组下标,分配大小,这个大小的内存块可以分配多少个,需要多少页*/#defineZEND_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)思考:给定一个size,如何确定它的bin_num(也就是index),从而快速找到count和pages?staticzend_always_inlineintzend_mm_small_size_to_bin(size_tsize){...unsignedintt1,t2;if(size<=64){/*我们需要支持size==0...*/return(size-!!size)>>3;}else{t1=大小-1;t2=zend_mm_small_size_to_bit(t1)-3;t1=t1>>t2;t2=t2-3;t2=t2<<2;返回(int)(t1+t2);}#endif}当size<=64时,size会增加8,那么可以直接通过>>3位(除以8)得到索引的偏移量。很容易理解,这里的size是为了兼容size=0,这样右移3位前不会出现负值;减去!!size(即减去1)是为了与边界条件兼容,例如size为8B。当size>64时,详见:小内存规格计算。核心思想:size可以看成一个等差数列,按照公差进行分组。先找出当前size属于哪个group,group内的offset是多少,最后把两者相加。小内存分配过程:staticzend_always_inlinevoid*zend_mm_alloc_small(zend_mm_heap*heap,size_tsize,intbin_numZEND_FILE_LINE_DCZEND_FILE_LINE_ORIG_DC){...if(EXPECTED(heap->free_slot[bin_num]!=NULL)){=zend_mm_f>free_slot[bin_num];heap->free_slot[bin_num]=p->next_free_slot;返回(无效*)p;}else{returnzend_mm_alloc_small_slow(heap,bin_numZEND_FILE_LINE_RELAY_CCZEND_FILE_LINE_ORIG_RELAY_CC);如果内存不为空,直接从free_slot链表的首位取,然后将链指针向后移动,等待下一次内存分配。否则,调用zend_mm_alloc_small_slow函数。注意这个慢是指重新申请chunk,分配page等,这里不展开。内存对齐内存对齐:请求内存返回的起始地址是一个逻辑地址(下面会介绍),地址必须是2MB的整数倍,这就是内存对齐。在PHP中可以判断一定是申请的hugememory。因为小内存和大内存的第一页存放的是zend_mm_heap结构,所以一定不能是2MB地址的整数倍。这样可以快速计算出当前地址属于哪个chunk。例如,如果地址是4096,则它属于第三个块。所以,内存对齐的好处是,如果对齐了,就可以直接判断是一个巨大的规范,不需要额外存储字段供后面的判断使用。x64架构的计算机地址一共有64位,任何内存地址都可以分为首地址+偏移量,比如1019地址(十进制)=1000(首地址)+19(偏移量);从二进制看,2MB=1000000000000000000000,低21位全为0(偏移量为0),高43位为首地址(第22位为1,高64-22=42位全为0)就形成了一个64位的逻辑地址。这样就可以方便的进行逻辑地址和物理地址的转换。向操作系统申请内存是以页为单位的,所以申请的内存是4KB的整数倍,不会出现4K+1等逻辑地址。思考:PHP以chunk为单位向操作系统申请内存,但操作系统只返回4KB整数倍的内存起始地址,而不是2MB内存起始地址。在一个chunk中,起始地址分配给2MB的整数倍的概率是1/512,如何让操作系统请求的地址直接是2MB的整数倍?答:申请更多的存储空间,切头切尾,保证起始地址2MB对齐。gdb调试相关gdb需要指定一个可执行的二进制程序进行调试,相关命令可以在google中使用,常用r/b/n/s/p在使用gdbbmain时,为什么php没有main()入口功能?上面的gdb命令还能正常执行吗?因为虚拟机默认会帮你加一个main()并返回编译器优化:直接在函数中注入内联函数,替换宏等,所以编译时需要关闭编译器优化,方便调试

最新推荐
猜你喜欢