资讯专栏INFORMATION COLUMN

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

shiyang6017 / 978人阅读

摘要:换句话说,尽量不要有内存碎片。例如分配的内存,不能只分配个,因为会剩下大小的内存碎片,不能够再次利用并分配。但是如果最小公倍数过大,导致所需要的太多,那么退而求其次,即使留有很少的内存碎片,也算是可以满足需求的。同时也避免了内存碎片的产生。

baiyan

全部视频:https://segmentfault.com/a/11...

源视频地址:http://replay.xesv5.com/ll/24...

malloc和free函数思考

字符串的非二进制安全:如果存的字符串内容中带有,那么它会被当做字符串结束标志,提前判定字符串结束,而并非到字符串末尾正常结束。

malloc一块内存之后,它的size是存在哪里的?

free一块内存,如何知道free多大的内存?

解决方案:在分配的内存地址空间附近额外分配一块内存记录size。

malloc()系统调用是非常慢的,相当于你去面包店买一个面包,需要和面、发酵...等等一系列繁琐的工作,在操作系统层面会引起用户态和内核态的切换,耗时很长,用户是不能接受的,所以PHP实现了自己的一套内存管理机制,避免频繁的进行系统调用。

操作系统内存基本概念

chunk:2MB大小内存

page:4KB大小内存

二者关系:一个chunk由512个page组成(2MB/4KB = 512)(512 = 2^9,4KB = 2^12,2MB = 2^21)。

mmap():系统调用,将一个文件或者其它对象映射进内存,分配chunk。

内存不够用怎么办:选择一个占用内存大的进程并挂起,并将其使用的内存置换到硬盘上,分配给新进程使用。当挂起进程被唤醒的时候,唤醒进程会到内存中找对应的页,但是无法找到,故产生缺页中断,操作系统会将硬盘中的页置换回内存,供唤醒进程继续运行。

PHP内存分配 PHP内存分类:small/large/huge

small:size <= 3KB(有30种规格)

large:3KB < size <= 2MB - 4KB

huge:size >= 2MB - 4KB

思考:为什么large规格内存是 <= 2MB - 4KB?

答:在small和large内存分配规格中,首先会分配一个chunk,chunk的第一个page中存有一个结构体,用来保存chunk中每一个page的相关存储信息(即zend_alloc.h中的mm_heap结构体)。但是huge规格不需要,直接拿来用即可(因为huge内存已经大于2MB(即一个chunk)),无需记录内部存储细节信息)。思考一个问题:一个page有4KB,至于用这么大的空间去存吗?

struct _zend_mm_heap {
#if ZEND_MM_CUSTOM
    int                use_custom_heap;
#endif
#if ZEND_MM_STORAGE
    zend_mm_storage   *storage;
#endif
#if ZEND_MM_STAT
    size_t             size;                  /* 当前使用内存(PHP库函数memory_get_usage()方法就是取此字段的值)*/
    size_t             peak;                /*  内存使用峰值 */
#endif
    zend_mm_free_slot *free_slot[ZEND_MM_BINS];  /*它是一个大小为ZEND_MM_BINS = 30的数组,每个存储单元是一个单向链表,用来挂载30种不同规格的small内存(下面会讲)*/
#if ZEND_MM_STAT || ZEND_MM_LIMIT
    size_t             real_size;               /* current size of allocated pages */
#endif
#if ZEND_MM_STAT
    size_t             real_peak;               /* peak size of allocated pages */
#endif
#if ZEND_MM_LIMIT
    size_t             limit;                   /* memory limit */
    int                overflow;                /* memory overflow flag */
#endif

    zend_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 alocated 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_CUSTOM
    union {
        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;
#endif
};

重点关注中文注释的部分,我们看到zend_mm_heap结构体并不是很大,那么继续关注其中重点的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 */
};

1个chunk有512个page。

第一个heap字段是zend_mm_heap类型的结构体指针,方便在512个page中直接找到第一个存储额外信息的page。下面两个next和prev字段构成一个双向链表,用于chunk与chunk之间的连接。

重点关注倒数第二个字段zend_mm_page_map类型的字段,有512bit(即64B),每一个bit用来存储当前这个chunk中的每一个页(因为一共有512个页)是否使用。

除了看它是否使用,还要看每个page的具体使用的大小和其他相关情况并记录相关信息。

最后一个zend_mm_page_info类型的字段解决了上述问题。ZEND_MM_PAGES = (ZEND_MM_CHUNK_SIZE / ZEND_MM_PAGE_SIZE) = 2MB/4KB = 512个存储单元,每个存储单元是typedef uint32_t zend_mm_page_info类型,即4B大小用来存储额外的使用情况信息,这个字段一共是2KB。用来存储使用的大小和额外信息,所以这里2KB加上剩余字段,差不多会使用4KB的空间,所以chunk的第一个page需要留出来记录这些信息。

那么这个page_info(uint32_t),这里32位是如何利用的呢,先看如何标记内存规格

#define ZEND_MM_IS_FRUN                  0x00000000
#define ZEND_MM_IS_LRUN                  0x40000000
#define ZEND_MM_IS_SRUN                  0x80000000

第32位为1代表small内存,即0x8。然后低5位(2^5 > 30种规格)存储bit_num(即ZEND_MM_BINS_INFO宏的索引,以便快速找到所需页的数量等信息(下面会讲))

第31位为1代表large内存,即0x4。然后低10位表示分配的页数(>= 512即可)为什么不用9位呢?2^9 = 512足够了???

第31、32位都是1,即0xC,低5位表示bit_num,这里3KB规格就是29,16~25bit表示偏移量。以申请3KB为例,一定是3个4KB的page,分为4个3KB(见下文)。第一个3KB就是0xC作为连续分配4个3KB的起始标记(约定),偏移量是0,bit_num是29;第二个3KB就是0x8,偏移量是1,bit_num同样是29;第3、4个3KB内存同理,只是偏移量不同。

_emalloc() -> 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 (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);
        return ptr;
    } else if (size <= ZEND_MM_MAX_LARGE_SIZE) {
        ptr = zend_mm_alloc_large(heap, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
        return ptr;
    } else {
        return zend_mm_alloc_huge(heap, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
    }
}
small内存

注意一个page可以包含多个small内存块,首先满足一次性分配的page总量必须能够除尽分配的规格大小。换句话说,尽量不要有内存碎片。例如:分配3KB的small内存,不能只分配1个page,因为会剩下1KB大小的内存碎片,不能够再次利用并分配。所以需要求得一个规格和4KB的最小公倍数,作为总的分配空间。但是如果最小公倍数过大,导致所需要的page太多,那么退而求其次,即使留有很少的内存碎片,也算是可以满足需求的。下面举几个例子:

例1:假如分配8B规格的small内存:我们先找最小的可以除尽8B的page大小。因为4KB可以除尽8B,所以我们取1个page就够了,故一共正好可以分成512个8B的内存。我们取1个8B内存返回给用户,然后剩余511个8B内存会挂在zend_mm_heap结构体的zend_mm_free_slot *free_slot[ZEND_MM_BINS]字段中。这里ZEND_MM_BINS宏 = 30,恰好这30个数组单元对应30种small内存规格,共有30个单向链表,每一个单向链表都是挂载的多分配的内存。如上例中的8B内存,就会挂载剩下多分配的511个8B内存。当下次再次有分配请求的时候,直接从链表上拿即可,不用再去重新分配。同时也避免了内存碎片的产生。当释放内存的时候,直接利用头插法(相比尾插法复杂度O(1))插入到链表头部即可。

small内存最小的分配规格就是8B,因为指针在64位系统下是8B,能够满足free_slot字段单向链表的需求。

例2:假如分配3KB规格的small内存:我们同样先找可以整除3KB的page大小。因为4KB不可以除尽3KB,所以1个page虽然可以满足3KB的分配需求,但是还是会留有1KB的内存碎片,所以我们不能直接拿1个page来用。我们取最小的可以整除3KB的4KB倍数,求得该值为3(4KB * 3 / 3KB = 4)个3KB大小的内存块。那么同样我们取1个3KB内存返回给用户,剩余3个3KB内存挂在上面的zend_mm_heap结构体中free_slot字段的链表上,等待后续的分配请求。

例3:假如分配40B规格的small内存,为什么下面代码也仅仅用了1个page,那是因为4KB和40B的最小公倍数过大,导致一次性申请的page太多,所以不能一味的找二者的最小公倍数,所以退而求其次,只分配1个page。虽然4096/40 = 102.5,代码中直接取了102作为最终分配的数量。第1个返回给用户,剩下101个挂在free_slot链表上等待后续的分配请求。

下面是所有的small内存分配规格:

/* bin_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)

思考:给一个size,如何确定它的bin_num(也就是索引),从而快速找到count和pages?

static zend_always_inline int zend_mm_small_size_to_bin(size_t size)
{
...
    unsigned int t1, t2;

    if (size <= 64) {
        /* we need to support size == 0 ... */
        return (size - !!size) >> 3;
    } else {
        t1 = size - 1;
        t2 = zend_mm_small_size_to_bit(t1) - 3;
        t1 = t1 >> t2;
        t2 = t2 - 3;
        t2 = t2 << 2;
        return (int)(t1 + t2);
    }
#endif
}

当size <= 64的时候,size会递增8,那么直接>>3位(除以8)就可以得到索引的偏移量,很好理解。这里的!!size是为了兼容size = 0的情况,这样在右移3位之前就不会出现负值;减去!!size(即减1)是为了兼容size为8B规格等边界情况。

当size > 64的时候,详情参考:small内存规格的计算 核心思想:可以将size看成等差数列,按照公差分组。首先找出当前size属于哪个组,在组内的偏移量是多少,最终把二者相加。

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 (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链表的第一个位置上拿就可以了,然后链指针向后移,等待下次内存分配。否则调用zend_mm_alloc_small_slow函数,注意这个slow就代表重新申请chunk、分配page等等,这里暂不展开。

内存对齐

内存对齐:所申请的内存返回的起始地址都是逻辑地址(下面会讲),且该地址一定是2MB的整数倍,这就是内存对齐。在PHP中,可以判定一定是申请的huge规格的内存。因为small和large内存的第一个page存放了zend_mm_heap结构体,肯定不是2MB地址的整数倍。这样可以快速计算出当前地址属于第几个chunk。比如地址是4096,就属于第3个chunk。

所以内存对齐的优点就是如果对齐了,就可以直接判断是huge规格,不需要额外存储用于以后判断的字段。

x64体系结构的计算机的地址共有64位,任何一个内存地址都可以分为首地址 + 偏移量,如1019地址(10进制) = 1000(首地址) + 19 (偏移量);从二进制角度来看,2MB = 10 0000 0000 0000 0000 0000,低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

当利用gdb b main的时候,为什么php没有main()入口函数却仍可以正常执行上述gdb命令?因为虚拟机会默认帮助你加一个main()和return

编译器优化:将inline函数直接注入到函数里面去,把宏进行替换等等,所以编译的时候为方便调试需要禁用编译器优化

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/31271.html

相关文章

  • 【LNMPR源码学习笔记汇总

    摘要:此文用于汇总跟随陈雷老师及团队的视频,学习源码过程中的思考整理与心得体会,此文会不断更新视频传送门每日学习记录使用录像设备记录每天的学习源码学习源码学习内存管理笔记源码学习内存管理笔记源码学习内存管理笔记源码学习基本变量笔记 此文用于汇总跟随陈雷老师及团队的视频,学习源码过程中的思考、整理与心得体会,此文会不断更新 视频传送门:【每日学习记录】使用录像设备记录每天的学习 PHP7...

    Barrior 评论0 收藏0

发表评论

0条评论

shiyang6017

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<