PHP源码学习20190318-复习前面的内容

24次阅读

共计 6357 个字符,预计需要花费 16 分钟才能阅读完成。

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

baiyan

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

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

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

malloc 函数深入

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

  • 那么究竟是否是这样的呢?我们写一段测试代码验证一下:
#include <stdlib.h>
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 代码:
<?php
$a = "string" . time("Y-m-d");
echo $a;
$b = $a;
echo $a;
echo $b;
$b = "new string";
echo $a;
echo $b;
  • 利用 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 计算出来的字符串,存储在临时变量区。
  • 我们举一个例子:
<?php
$a = "hello" . time("Y-m-d"); // 临时字符串,因为 time()会随时间变化而变化
$b = "hello world";           // 常量字符串
  • 这里 $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

正文完
 0