资讯专栏INFORMATION COLUMN

【PHP源码学习】2019-03-18 复习前面的内容

lindroid / 3264人阅读

摘要:调用函数时,它将用户释放的内存块连接到空闲链上。这个联合体共占用字节。是数字,且顺序递增位置固定,如访问是的元素,即,就直接访问数组的第个位置即可即,这样就不需要前面的索引数组。

baiyan

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

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

本笔记中部分图片截自视频中的片段,图片版权归视频原作者所有。

malloc函数深入

在PHP内存管理1笔记中提到,malloc()函数会在分配的内存空间前面额外分配32位,用来存储分配的大小和几个标志位,如图:

那么究竟是否是这样的呢?我们写一段测试代码验证一下:

#include 
int main() {
    void *ptr = malloc(8);
    return 1;
}

利用gdb调试这段代码:

首先打印ptr的地址,为0x602010,利用x命令往后看20个内存单元(1个内存单元 = 4个字节),故一共展示了80个字节,后面的x是以16进制打印内容。

我们发现紧邻0x602010地址的上面32位均是0,没有任何内容,不符合我们的预期。

上图只是一个最简单的思路,但绝大多数操作系统是按照如下的方式实现的:

操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表(Free List)。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块(根据不同的算法而定(将最先找到的不小于申请的大小内存块分配给请求者,将最合适申请大小的空闲内存分配给请求者,或者是分配最大的空闲块内存块)。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。如果无法获得符合要求的内存块,malloc函数会返回NULL指针,因此在调用malloc动态申请内存块时,一定要进行返回值的判断。
结构体与联合体 结构体

在PHP内存管理2笔记中,我们谈到了一种特殊情况:

在b是char类型的时候,a和b的内存地址是紧邻的;如果b是int类型的话,就会出现如图所示的情况。我们可以这样记忆:不看b之后的字段,a和b之前也是按照它们的最小公倍数对齐的(如果b是int类型,a和b的最小公倍数是4,按4对齐;如果b是char类型,最小公倍数为1,按1对齐,就会出现a和b紧邻的情况)

如果不想对齐,有如下解决方案:

编译的时候不加优化参数

代码层面:在struct后加关键字,例如redis中的sds简单动态字符串的实现:

    struct __attribute__ ((packed)) sdshdr16 {
        uint16_t len;
        uint16_t alloc;
        unsigned char flags;
        char buf[];
    }

联合体

所有字段共用一段内存,用于PHP中变量值的存储(因为变量只有一种类型),也可以用来判断机器的大小端问题。

宏定义

宏就是替换。

关于下面这段代码的复杂宏替换问题,在PHP内存管理3笔记中已经有详细解释,此处不再赘述。

#define _BIN_DATA_SIZE(num, size, elements, pages, x, y) size,
static const uint32_t bin_data_size[] = {
  ZEND_MM_BINS_INFO(_BIN_DATA_SIZE, x, y)
};

关于C语言宏定义中的##等特殊符号的用法,参考:#define宏定义中的#,##,@#, 这些符号的神奇用法

PHP7中的基本变量

在PHP7中,所有变量都以zval结构体来表示。一个zval是16字节;在PHP5中,一个zval是48字节。

struct _zval_struct {
    zend_value value;
    union u1;
    union u2;
};

存储变量需要考虑两个要素:值与类型。

变量值的存放

在PHP7中,变量的值存在zend_value 这个联合体中。只有整型和浮点型是直接存在zend_value中,其余类型都只存放了一个指向专门存放该类型的结构体指针。这个联合体共占用8字节。

typedef union _zend_value {
    zend_long         lval;    //整型
    double            dval;    //浮点
    zend_refcounted  *counted; //引用计数
    zend_string      *str; //字符串
    zend_array       *arr; //数组
    zend_object      *obj; //对象
    zend_resource    *res; //资源
    zend_reference   *ref; //引用
    zend_ast_ref     *ast; //抽象语法树
    zval             *zv;  //内部使用
    void             *ptr; //不确定类型,取出来之后强转
    zend_class_entry *ce;  //类
    zend_function    *func;//函数
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww; //这个union一共8B,这个结构体每个字段都是4B,因为所有联合体字段共用一块内存,故相当于取了一半的union
} zend_value;
变量类型的存放

在PHP7中,其变量的类型存放在zval中的u1联合体中:

...
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    type,     /* 在这里用unsigned char存放PHP变量值的类型 */
                zend_uchar    type_flags,
                zend_uchar    const_flags,
                zend_uchar    reserved)        /* call info for EX(This) */
        } v;
        uint32_t type_info;
    } u1;
...

PHP7中所有的变量类型:

/* regular data types */
#define IS_UNDEF                    0
#define IS_NULL                        1
#define IS_FALSE                    2
#define IS_TRUE                        3
#define IS_LONG                        4
#define IS_DOUBLE                    5
#define IS_STRING                    6
#define IS_ARRAY                    7
#define IS_OBJECT                    8
#define IS_RESOURCE                    9
#define IS_REFERENCE                10

/* constant expressions */
#define IS_CONSTANT                    11
#define IS_CONSTANT_AST                12

/* fake types */
#define _IS_BOOL                    13
#define IS_CALLABLE                    14
#define IS_ITERABLE                    19
#define IS_VOID                        18

/* internal types */
#define IS_INDIRECT                 15
#define IS_PTR                        17
#define _IS_ERROR                    20
PHP7中的字符串 字符串基本结构

设计字符串存储的数据结构两大要素:字符串值和长度。

PHP7字符串存储结构的设计:

struct _zend_string {
    zend_refcounted_h gc;         /*引用计数,与垃圾回收相关,暂不展开*/
    zend_ulong        h;          /* 冗余的hash值,计算数组key的哈希值时避免重复计算*/
    size_t            len;        /* 长度 */
    char              val[1];     /* 柔性数组,真正存放字符串值 */
};

由为什么存长度引申出二进制安全的问题。二进制安全:写入的数据和读出来的数据完全相同,就是二进制安全的,详情见PHP字符串笔记

字符串写时复制

看下面一段PHP代码:



利用gdb调试这段代码,观察其引用计数情况。

在第一个echo语句处打断点,并查看$a中zend_stinrg中的引用计数gc.refcount = 1(下简称refcount)。因为现在只有一个$a引用zend_string。

利用gdb的c命令继续运行下一行PHP代码$b = $a,然后观察$a的zend_sting,我们发现$a引用的zend_string的refcount变为2:

查看此时的$b,发现引用的zend_string的refcount也是2,且地址均是0x7ffff5e6b0f0,说明$a与$b所引用的是同一个zend_string。

此时的内存结构如图所示:

这样做的优点就是仅仅需要1个zend_string就可以存储两个PHP变量的值,而不是2个zend_string,节省了1个zend_string的内存空间。

那么我们看接下来$b = "new string",这样的话,$a和$b由于存储的内容不同,故不可以继续引用同一个zend_string,这时就会发生写时复制。我们继续gdb调试,看一下是否符合预期:

给$b赋值后,观察$a的存储情况:

我们看到,此时$a所指向的zend_string的refcount变为了1,接下来再看一下$b的存储情况:

注意此时$b所指向的zend_string的refcount变为了0(注意这里为什么是0而不是1呢?下面会讲),而且b指向的zend_string的地址为0x7ffff5e6a5c8,与$a所指向的zend_string的地址0x7ffff5e6b0f0不同,说明发生了写时复制,即由于字符串值的改变,被迫生成了一个新的zend_string结构体,用来专门存储$b的值;而$a指向的zend_string只是refcount减少了1,其余并未发生变化。

那么为什么$b所指向的zend_string的refcount是0呢,我们先给PHP中的字符串分个类:

常量字符串:在PHP代码中硬编码的字符串,在编译阶段初始化,存储在全局变量表中,refcount一直为0,其在请求结束之后才被销毁(方便重复利用)。

临时字符串:计算出来的临时字符串,是执行阶段经过zend虚拟机执行opcode计算出来的字符串,存储在临时变量区。

我们举一个例子:



这里$a由于调用了time()函数,所以最终的值是不确定的,是临时字符串。

$b也可以叫做字面量,是被硬编码在PHP代码中的,是常量字符串。

我们画一下最终$a与$b的内存结构图:

由此我们可以清晰地看到,$a与$b不在引用同一个zend_string。那么我们给写时复制下一个定义:给$b重新赋值而导致不能与$a共用一个zend_string的现象,叫做写时复制。

PHP7中的数组

PHP7中的数组是一个hashtable,key-value对存储于bucket中。

PHP7数组基本结构:

struct _zend_array {
    zend_refcounted_h gc;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    flags,
                zend_uchar    nApplyCount,
                zend_uchar    nIteratorsCount,
                zend_uchar    consistency)
        } v;
        uint32_t flags;
    } u;
    uint32_t          nTableMask;       //数组大小减一,用来做或运算,packed array初始值是-2,hash array初始值是-8
    Bucket            *arData;          //指针,指向实际存储数组元素的bucket
    uint32_t          nNumUsed;         //使用了多少bucket,但是unset的时候这个值不减少
    uint32_t          nNumOfElements;   //真正有多少元素,unset的时候会减少
    uint32_t          nTableSize;       //bucket的个数
    uint32_t          nInternalPointer;
    zend_long         nNextFreeElement; //支持$arr[] = 1;语法,没插入1个元素就会递增1
    dtor_func_t       pDestructor;
};

typedef struct _zend_array HashTable;

此结构在内存中的结构图如下:

思考:为什么要存储gc字段?因为gc字段冗余存储了变量的类型,给任意一个变量,把它强转成zend_refcounted_h类型,都可以拿到它的类型,zend_refcounted_h类型结构如下:

typedef struct _zend_refcounted_h {
    uint32_t         refcount;            /* 引用计数 */
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar    type,
                zend_uchar    flags,    /* used for strings & objects */
                uint16_t      gc_info)  /* keeps GC root number (or 0) and color */
        } v;
        uint32_t type_info;
    } u;
} zend_refcounted_h;

进行强制类型转换之后,通过取该变量的u.type字段,就可以拿到当前变量的类型了。

我们接着看一下bucket的结构:

typedef struct _Bucket {
    zval              val;   //元素的值,注意这里直接存了zval而不是一个zval指针
    zend_ulong        h;     //冗余的哈希值,避免重复计算哈希值
    zend_string      *key;   //元素的key值,指向一个zend_string结构体
} Bucket;

思考如果利用$arr[] = 1;语法进行数组赋值,key字段的值是多少?答案是0x0,就是一个空指针。

hashtable的问题:哈希冲突,解决冲突的方法有开放定制法和链地址法,常用的是链地址法。

PHP7中并没有采用真正的链表结构,而是利用数组模拟链表。这个时候需要在Bucket数组之前额外开辟一段内存空间(叫做索引数组,每个索引数组的单元叫一个slot),来存储同一hash值的第一个bucket的索引下标。

看一个简单的数组查找过程:

经过time33哈希算法算出哈希值h

计算出索引数组的nIndex = h | nTableMask = -7(假设),这个nIndex也别称做slot

访问索引数组,取出索引为-7位置上的元素值为3

访问bucket数组,取出索引为3位置上的key,为x,发现并不等于s,那么继续查找,访问val.u2.next指针,为2

取出索引为2位置上的key,为s,发现正好是我们要找的那个key

取出对应的val值3

注意如果bucket的存储空间满了,需要重新计算和nIndex(即slot)的值并将值放到正确的bucket位置上,这个过程也叫做rehash。

具体的插入过程详见PHP基本变量笔记的文章末尾。

PHP7中的数组分为两种:packed array与hash array。

packed array:

key是数字,且顺序递增

位置固定,如访问key是0的元素,即$arr1[0],就直接访问bucket数组的第0个位置即可(即arData[0]),这样就不需要前面的索引数组。

如果不满足上述条件,就是hash array

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

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

相关文章

  • 【每日学习记录】使用录像设备记录每天学习

    摘要:在这里使用学而思网校的录像设备,记录每天学习的内容闫昌李乐阶段李乐李乐李乐李乐李乐李乐马运运李乐李乐李乐源码集群闫昌源码闫昌源码主从复制李乐源码施洪宝源码施洪宝韩天 在这里使用学而思网校的录像设备,记录每天学习的内容: 2019-06-24 ~ 2019-06-28 06-27 nginx by 闫昌 06-26 nginx module by 李乐 06-25 nginx http ...

    szysky 评论0 收藏0
  • 【每日学习记录】使用录像设备记录每天学习

    摘要:在这里使用学而思网校的录像设备,记录每天学习的内容执行潘森执行潘森执行潘森赵俊峰红黑树景罗红黑树景罗配置三叉树田志泽新建模块马运运配置田志泽田志泽田志泽李乐田志泽田志泽文件系统 在这里使用学而思网校的录像设备,记录每天学习的内容: 2019-07-15 ~ 2019-07-19 07-18 nginx http 执行 by 潘森 07-17 nginx http 执行 by 潘森 07...

    pkhope 评论0 收藏0

发表评论

0条评论

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