php7源码分析

基本变量

php的zval结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct _zval_struct {
union {
zend_long lval; /* long value */
double dval; /* double value */
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;
} value;
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar type, /* active type */
zend_uchar type_flags,
zend_uchar const_flags,
zend_uchar reserved) /* call info for EX(This) */
} v;
uint32_t type_info;
} u1;
union {
uint32_t var_flags;
uint32_t next; /* hash collision chain */
uint32_t cache_slot; /* literal cache slot */
uint32_t lineno; /* line number (for ast nodes) */
uint32_t num_args; /* arguments number for EX(This) */
uint32_t fe_pos; /* foreach position */
uint32_t fe_iter_idx; /* foreach iterator index */
} u2;
};

这个新的zval在64位环境下,只需要16个字节,其主要分为两个部分,value和扩充字段。

  • value是一个size_t大小(一个指针大小),可以保存一个指针或者一个long/double
  • 扩充字段又分为u1,u2两个部分,u1是typeinfo,u2是各种辅助字段
    ** typeinfo部分保存了这个zval的类型,扩充辅助字段则会在多个地方使用,比如next,就用在取代hashtable中的原来的拉链指针。

PHP7中的zval, 已经变成了一个值指针, 它要么保存着原始值, 要么保存着指向一个保存原始值的指针。
php7中,引用计数不再是zval的字段,而是被设计在zval的value字段所指向的结构体中

变量类型

php7中变量类型定义在zend_types.h中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define IS_UNDEF 0 /*标记未使用类型*/
#define IS_NULL 1 /*NULL*/
#define IS_FALSE 2 /*布尔false*/
#define IS_TRUE 3 /*布尔true*/
#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 /*参考类型(内部使用)*/
#define IS_CONSTANT 11 /*常量类型*/
#define IS_CONSTANT_AST 12 /*常量类型的AST树*/
/*伪类型*/
#define _IS_BOOL 13
#define IS_CALLABLE 14
#define IS_ITERABLE 19
#define IS_VOID 18
/*内部类型*/
#define IS_INDIRECT 15 /*间接类型*/
#define IS_PTR 17 /*指针类型*/
#define _IS_ERROR 20 /*错误类型*/

PHP7中定义了20种宏,用来标记u1.v.type字段。以u1.v.type值为IS_ARRAY为例,那么取value.arr的值,对应zend_array。同样,如果u1.v.type为IS_LONG,则通过value.lval取值

常见类型

  • IS_UNDEF:表示数据可以删除或覆盖
  • IS_REFERENCE: 新增类型,用来处理引用”&”
  • IS_INDIRECT: 新增类型,由于php7中hashtable的设计和php5不同,所以在解决全局符号表的问题上,引入了IS_INDIRECT类型
  • IS_PTR: 该类型被定义为指针类型,用来解析value.ptr,通常用在函数类型上
  • IS_ERROR: 新增类型,校验zval类型是否合法

整数和浮点型
字符串
数组
引用
间接zval
常量和常量AST
资源
对象

变量作用域

全局变量
局部变量
中间变量
静态变量
常量

垃圾回收

gc基本结构

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _zend_refcounted_h {
uint32_t refcount; /* 32bit长度的引用计数 */
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type, /*当前元素的数据类型*/
zend_uchar flags, /* 标记字符串或者对象*/
uint16_t gc_info) /* 记录所在gc池中的位置和颜色 */
} v;
uint32_t type_info;
} u;
} zend_refcounted_h;

引用计数

循环引用问题

垃圾回收

垃圾回收机制

字符串

字符串的结构

php7字符串具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct _zend_string {
zend_refcounted_h gc; /*8字节,内嵌的gc 引用计数及字符串类别存储*/
zend_ulong h; /*哈希值,8字节,字符串的哈希值,它的值只有当字符串需要被作为数组key时才会初始化*/
size_t len; /*8字节,字符串的长度 1.时间换空间 避免重复计算长度 2保证字符串操作二进制安全*/
char val[1]; /*柔性数数组,占1位,字符串的值存储位置 1.连续的内存 相比php5降低了读写次数*/
};
typedef struct _zend_refcounted_h { /*gc,整块占用8字节*/
uint32_t refcount; /*4字节,引用计数的值存储 */
union {/* 4字节 */
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type, /*等同于zval的u1.v.type*/
zend_uchar flags, /*字符串的类型数据*/
uint16_t gc_info /*垃圾回收标识颜色用*/
} v;
uint32_t type_info;
} u;
} zend_refcounted_h;

zend_string结构体占用32个字节
20190707156247996354019.png

php7与php5字符串内存分布对比
20190707156248022464623.png

字符串的二进制安全

对于C语言来首’\0’就是字符串的结束符,对于PHP7来说,其通过zend_string结构体对字符串重新封装,读取的数据长度以自身len值为准。

zend_string API

20190702156205927542890.png

1
2
3
4
5
6
7
zend_string_init 把一个普通字符串初始化成zend_string
实际申请内存大小 = 结构体大小(24)+ 字符串长度(len)+1
zend_string_extend 扩容
当需扩容的字符串是普通字符串且refcount等于1时,直接调用peralloc函数分配内存,扩容一步到位。
perealloc函数,当参数persistent=1时调用系统函数realloc申请内存,当persistent!=1时调用PHP内存池erealloc函数申请内存。
两者实现功能类似,以realloc为例,它首先判断当前指针是否有足够的连续空间,如果有扩大mem_address指向的地址,并将mem_address返回,如果空间不够,先按照newsize指定的大小分配内存,将原有数据从头到尾拷贝到新分配的内存区域,然后释放原来mem_address所指内存区域,同时返回新分配的内存区域首地址
当需扩容字符串引用计数大于1或类型为内部字符串时,则调用zend_string_alloc申请一块新内存,并把原值拷贝进去。对于普通字符串还需要对老字符串refcount--

智能字符串

智能字符串具体实现

smart_str是php7智能字符串管理函数

1
2
3
4
5
6
7
8
9
10
typedef struct {
zend_string * s ; /*字符串值存储在zend_string.val中*/
size_t a ; /*申请的内存空间总大小*/
} smart_str ;
struct _zend_string {
zend_refcounted_h gc; /*引用计数及字符串类别存储*/
zend_ulong h; /*哈希值*/
size_t len; /*已使用内存的字符串长度*/
char val[1]; /*字符串值的存储位置*/
};

smart_str字段对应含义:

  1. s字段: 指向zend_string结构体
  2. a字段: 智能字符串申请的内存空间总大小

智能字符串优点

使用智能字符串优化了内存申请的环节,不需要频繁申请和释放内存,可以更高性能的完成字符串的追加扩容。具体原因在于:
申请内存时会先申请一块较大的连续内存,并把申请的总长度写入zend_str.a字段中,把已使用的长度写入smart_str.s.len中。当智能字符串需要追加新字符串时,直接检查剩余内存块长度够不够,不够则重新申请一块更大的内存。通过空间换时间的方法,避免每次都去申请或释放内存。

smart_str API

进阶

字符串的赋值与写时分离赋值

字符串赋值操作中refcount的变化

临时字符串赋值
当字符串的值不是一个常量字符串时,每次赋值斗湖执行字符串的refcount++

1
2
$a = 'hello'.time(); /*$a的gc.refcount=1*/
$b = $a; /*$a$b指向同一块地址,gc.refcount=2*/

字符串常量赋值
当字符串是常量字符串时,赋值只修改zval中str的指针地址,两个字符串指向同一个str地址,但是refcount的值始终都是0。字符串的gc.flags会被标识为2

1
2
$a = 'hello'; /*$a的gc.refcount=0 */
$b = $a; /*$b的gc.refcount=0 */

整型常量赋值

1
2
$a = 1; /*或者$a = time();*/
$b = $a; /*zval是int类型,无refcount,会复制$a的值*/

字符串引用赋值
引用赋值时会多出zend_reference结构体,里面包含gc及zval字段,赋值时再进行refcount++

1
2
$a = 'hello';
$b = &$a; /*强制引用类型;*/

总结:
可以知道不是所有的PHP变量赋值都会用到引用计数,对于一个能否使用引用计数的变量也分以下几个类别:
1)变量是简单类型(true/false/double/long/null)时直接拷贝值,不需要引用计数;
2)变量是临时的字符串,在赋值时会用到引用计数,但如果变量是字符常量,则不会用到;
3)变量是对象(zval.v.type=IS_OBJECT)、资源(zval.v.type=IS_RESOURCE)、引用(zval.v.type=IS_REFERENCE,即$a=&$b)类型时,赋值一定会用到引用计数;
4)变量是普通的数组,赋值时也会用到引用计数,变量是IS_ARRAY_IMMUTABLE时,赋值不使用引用计数
一个zval是否支持引用计数,是通过zval.u1.type_flag来标识的,当type_flag的第三位被标识成1(IS_TYPE_REFCOUNTED标识),则代表可以引用计数。当然type_flag除了标识zval是否支持引用计数外,剩下的几位还可做其他标识,按位分割使用

字符串的写时分离
当字符串的refcount>1时,也就是有多个变量引用同一块内存值,对其中一个变量的值进行修改,会触发写时分离,此机制的好处就是,保证了各变量间的独立性。
字符串的类别

php源码为了实现对特殊字符串的管理,对字符串进行了分类,实现方式就是在gc.u.flags字段中标识

  • 临时普通字符串,flags标识为0
  • 内部字符串,用于存储PHP代码中的字面量、标识符等,flags字段被标识成IS_STR_PERSISTENT|IS_STR_INTERNED
  • 对于PHP已知字符串,flags字段会被标识成IS_STR_PERSISTENT|IS_STR_INTERNED|IS_STR_PERMANENT

几个概念的定义:
字面量: 代码中写死的变量值,比如整型字面量、字符串字面量等
标识符: 变量名、函数名、方法名、类名等
php已知字符串: 保留字(this、class等),超全局数组GLOBALS、$_GET等
保留字: 无法用作函数名、类等关键字,例如class等

字符串的类型转换
字符串的双引号与单引号
PHP常用字符串函数实现

数组的实现

基本概念

数组的语义

什么是数组?本质上数组是一个有序字典
两个语义

  • 字典
  • 有序

数组的概念

php数组zend_array对应的是HashTable,其中有如下信息:
slot:槽,HashTable有多个槽,一个bucket必须从属于具体的某一个slot,一个slot可以有多个bucket
bucket: 桶,HashTable中存储数据的单元。
哈希函数: 存储的时候会对key应用哈希函数确定所在的slot
哈希冲突

设计思路: 哈希表实现字典,全局链表实现有序遍历

hash表查找思路(php5 php7都是这样)
通过key找到索引表中slot位置,然后在slot中通过链式顺序查找找到指定key

php5数组实现

php5的bucket和hashtable结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typedef struct bucket {
ulong h; /* Used for numeric indexing */ //表示数字key或者字符串key的h值
uint nKeyLength; //arKey的长度。当nKeyLength等于0时,表示数字key
void *pData; //对应hashtable设计中的value
void *pDataPtr; //对应hashtable设计中的value
struct bucket *pListNext; //全局链表下一个
struct bucket *pListLast; //全局链表上一个
struct bucket *pNext; //局部链表下一个
struct bucket *pLast; //局部链表上一个
const char *arKey; //key
} Bucket;
typedef struct _HashTable {
uint nTableSize; //arBuckets指向的连续内存中指针的个数,即slot数量
uint nTableMask; //掩码。总是等于nTableSize-1。key经过hash1函数转为h,h通过hash2函数转为slot值。这里hash2函数就是slot=h&nTableMask
uint nNumOfElements; //bucket元素的个数
ulong nNextFreeElement;
Bucket *pInternalPointer;
Bucket *pListHead; //全局链表头
Bucket *pListTail; //全局链表尾
Bucket **arBuckets; //指针,指向一段连续的数组内存,这段数组内存并没有存储bucket,而是存储着指向bucket的指针。每一个指针代表一个slot,并且指向slot局部链表的首元素。
dtor_func_t pDestructor;
zend_bool persistent;
unsigned char nApplyCount;
zend_bool bApplyProtection;
#if ZEND_DEBUG
int inconsistent;
#endif
} HashTable;

20190703156215714964760.png
20190703156215775719374.jpg

rehash
随着slot中bucket的增多,_HashTable的访问逐渐退化为链表操作,此时就需要做rehash操作

php5数组实现存在的问题:
1.每个bucket都需要一次内存分配
2.为了保证数组两个语义,每个bucket需要维护4个指向bucket的指针。在32位/64位系统,每个bucket将为这4个指针付出16字节/32字节的内存

php7数组实现

如何使用连续数组实现链表?

php7的链表是一种逻辑上的链表,所有bucket都分配在连续的数组内存中,不再通过指针维护上下游关系,每个bucket只维护下一个bucket在数组中的索引(因为是连续内存,通过索引可以快速定位到bucket)

php7在分配buckets数组内存时,在bucket数组前额外申请了一些内存,这段内存是一个索引数组(也叫索引表),数组里每个元素代表一个slot,存放着每个slot链表的第一个bucket在bucket数组中的下标。如果当前slot没有任何bucket元素,那么索引值为-1。而为了实现逻辑链表,由于bucket元素的val是zval,php7通过bucket.val.u2.next表达链表中下一个元素在数组中的下标,见下图

其中巧妙的arData[0..n-1]表示bucket数组元素,arData[-1..-n]表示索引数组元素,因此在计算bucket属于哪个slot时,要做的就是确定它在索引数组中的下标,而这个下标是从-n~-1的负数,分别代表slot1到slotN,也正是因此HashTable的掩码nTableMask是负数

基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
typedef struct _zend_array zend_array;
typedef struct _zend_array HashTable;
typedef struct _Bucket {
zval val;
zend_ulong h; /* hash value (or numeric index) */
zend_string *key; /* string key or NULL for numerics */
} Bucket;
struct _zend_array {
zend_refcounted_h gc;
union {
struct {
_ENDIAN_LOHI_4(
zend_uchar flags,
zend_uchar nApplyCount,
zend_uchar nIteratorsCount,
zend_uchar consistency)
} v;
uint32_t flags;
} u;
uint32_t nTableMask;
Bucket *arData;
uint32_t nNumUsed;
uint32_t nNumOfElements;
uint32_t nTableSize;
uint32_t nInternalPointer;
zend_long nNextFreeElement;
dtor_func_t pDestructor;
};

rehash
内存分布上,有效bucket与无效bucket交替分布,但都在未使用bucket之前,插入时永远在未使用bucket上进行。当由于删除等操作导致无效bucket非常年多,而有效bucket很少时,会对整个bucket数组进行rehash操作,这样稀疏的有效bucket会重新变得连续而紧密,部分无效bucket会被重新利用而变为有效bucket。还有一部分有效bucket和无效bucket会被释放出来,重新变为未使用bucket

1.bucket结构分析

20190703156215808885020.png
val: 对应HashTable设计中的value,始终是zval
h: 表示数组key或字符串key的h值
key: 对应hashtable设计中的key,表示字符串key

bucket从使用角度分为3种
20190704156223875639786.png
从内存分布上,有效bucket和无效bucket会交替分布,但都在未使用bucket的前面。插入的时候永远在未使用bucket上进行。当由于删除等操作导致无效bucket非常多,而有效bucket很少时,会对整个bucket数组进行rehash操作。这样稀疏的有效bucket就会变得有效而紧密,部分无效bucket会被重新利用而变为有效bucket。还有一部分有效bucket和无效bucket会被释放出来,重新变为未使用bucket。

20190704156223892333957.png
HashTable字段信息:

gc: 引用计数相关
arData:实际存储容器。通过指针指向一段连续的内存,存储着bucket数组
nTableSize:HashTable的大小。
nNumUsed: 指所有已使用的bucket的数量,包括有效bucket和无效bucket的数量。
nNumOfElements: 有效bucket的数量
nTableMask: 掩码。一般为-nTableSize
nInternalPointer: HashTable的全局默认游标。php7中reset/key/current/next/prev等函数与该字段有密切关系。
nNextFreeElement: HashTable的自然key。自然key是指HashTable的应用语法是纯数组时,插入元素指定key,key会以nNextFreeElement的值为准。该字段初始值为0.比如$a[] =1,实际是插入到key等于0的bucket上,然后nNextFreeElement会递增变为1,代表下一个自然插入的元素的key是1
pDestructor:析构函数。当bucket元素被更新或删除时,会对bucket的value调用该函数,如果value是引用计数的类型,那么会对value引用计数减一,进而引发可能的gc。
u: 一个联合体,4字节,可以存储一个uint32_t类型的flags,也可以存储由4个unsigned char组成的结构体v
u.v.flags: 用各个bit来表达HashTable的各种标记。
u.v.nApplyCount: 递归遍历计数。为了解决循环引用导致的死循环问题
u.v.nlteratorsCount: 迭代器计数。PHP每一个foreach语句都会在全局变量EG中创建一个迭代器,迭代器包含正在遍历的HashTable和游标信息。

2019070415622389978607.png

为什么HashTable的掩码是负数
实际上PHP7在分配bucket数组内存的时候,在bucket数组的前面额外多申请了一些内存,这段内存是一个索引数组(也叫索引表),数组里面的每个元素代表一个slot,存放着每个slot链表的第一个bucket在bucket数组中的下标。如果当前slot没有任何bucket元素,那么索引值为-1.而为了实现逻辑链表,由于bucket元素的val是zval,PHP7通过bucket.val.u2.next表达链表中下一个元素在数组中的下标。为了得到介于[-n,-1]之间的负数的下摆哦,PHP7的HashTable设计中的hash2函数(根据h值取得slot值)是nIndex = h | ht->nTableMask;(其中nIndex就是slot指)

20190704156223956355597.png
初始化
20190704156223977926107.png

packed array和hash array

PHP数组有两种用法,一种是纯数组,一种是基于key-value的map。对于这两种用法PHP7引申出了packed array和hash array的概念。当HashTable的u.v.flags&HASH_FLAG_PACKED>0时,表示当前数组是packed array,否则当前数组是hash array。

PHP数组的有序性是通过arData的顺序插入保证的

packed array

特点

  • key全是数字key
  • key按插入顺序排列,仍然是递增的
  • 每一个key-value对的存储位置都是确定的,都存储在bucket数组的第key个元素上。
  • packed array不需要索引数组
    它实际利用了bucket数组的连续性特点,对于某些只有数字key的场景进行了优化。由于不再需要索引数组,从内存上节省了(nTableSize-2)*sizeof(uint32_t)个字节。另外,由于存取bucket是直接操作bucket数组,性能上也有所提升

php7中packed array的实现
2019070415622477523089.png

而hash array则相反,它依赖索引数组来维护每一个slot链表中首元素在bucket数组中的下标。

php7中hash array实现示意图
20190704156224791776210.png

hash array的nTableMask始终等于-nTableSize
hash array的arData由nIndex和bucket数组组成。nIndex数组与bucket数组实际是共享一块连续的内存,两个数组长度一致

数组的初始化

20190704156224802029476.png
20190704156224812559425.png
20190704156224802833385.png
20190704156224803858590.png

数据插入、更新、查找和删除**

packetd array的插入:
插入操作时,无须去计算hash值,也无须去维护索引数组,直接插入到bucket数组中去,插入的位置为idx=bucket.h=$i,

hash array的查找:
显然无法像packed array一样,直接根据key定位到在bucket数组的下标,这时索引数组就派上用场了。当要根据一个key去取value的时候,先得到key的hash值,根据h得到nIndex,去除value在arData存储的位置idx,再去除bucket的val值.拿key为x举例,字符串x的h值是9223372036854953501,它与nTableMask(-8)做位或运算之后,结果是-3,然后我们索引数组去查询-3这个slot的值,得出该slot链表首元素在bucekt数组的下标为0。因此按照这个下标找下去,肯定会找到key为x的元素,目前看,其实正是bucket数组的第0个元素

hash array的插入:
新增value在bucket中的位置idx=ht->nNumUsed++,bucket[idx]=value,然后将idx更新到索引数组中。如果确定索引数组中的位置呢?先要找到key对应的h值
如果是字符串类型,h=zend_string_hash_val(key)
如果是数字类型,h=key
然后与掩码取”|”得到其在索引数组中的位置nIndex,接下来就建立索引数组索引与bucket数组索引之间的关系,arData[nIndex]=HT_IDX_TO_HASH(idx)。

删除:并非真的删除,找到bucket后,将slot对应索引值改为-1,bucket的bucket.u1.v.type改为IS_UNDF

哈希冲突
指不同key经过hash函数得到相同的值,但是这些值需要同时插入nIndex数组,当出现冲突时,将原有arData[nIndex]存储的位置信息保存到新插入的value的zval.u2.next中

扩容和rahash
当插入的数组容量不够时,会进行扩容操作,新申请的arData的容量是当前数组容量的两倍,所以nTableSize始终为2^n

扩容和rehash

hash array在重置一个key时并不会真正触发删除操作,只是做一个标识,删除是在扩容和重建索引时触发。
插入时引发扩容及rehash整体流程图如下
20190704156224193156119.png

  • 当容量足够时直接执行插入操作
  • 当容量不够时(nNumUsed>=nTableSize),检查已删除元素占比,
    • 假如达到阈值,则将已删除元素从HashTable中移除,并重建索引。
    • 如果未到阈值,则要进行扩容操作,新的容量扩大到当前大小的2倍(2*nTableSize),将当前bucket数组复制到新的空间,然后重建索引

重建完索引后,有足够的空余空间后在执行插入操作

重建索引

rehash对应源码中的zend_hash_rehash方法
rehash的主要功能就是把HashTable数组中标识为IS_UNDEF的数据剔除,把有效数据重新聚合到bucket数组并更新插入索引表
rehash不重新申请内存,整个过程是在原有结构上做聚合调整
20190704156224241512205.png
具体步骤

  1. 重置所有nIndex数组值为-1
  2. 初始化两个bucket类型的指针p、q,循环遍历bucket数组
  3. 每次遍历,p++,遇到第一个IS_UNDEF时,q=p;继续循环数组
  4. 当再一次遇到一个正常数据时,把正常你数据拷贝到q指向的位置,q++
  5. 直到遍历完数组,更新nNumUsed等计数

生命周期和运行模式

SAPI(服务端应用编程接口)

相当于PHP外部环境的代理器,PHP可以应用在终端上,也可以应用在web服务器中,应用在终端上的SAPI叫做CLI SAPI,应用于WEB服务器中的叫做CGI SAPI

FPM的声明周期

20190708156251901183142.jpg
FPM模式舍命周期有5个阶段:
1.调用php_module_startup,加载所有模块
2.进入循环,调用fcgi_accept_request实际调用accept,阻塞等待请求;如果请求进来,就会被唤起,进入php_request_startup
3.进入php_execute_script,对脚本执行编译
4.调用php_request_shutdown关闭请求,继续进入循环
5.如果进程退出,调用php_module_shutdown关闭所有模块
6.如果请求数大于max_requests,则跳转5

多进程管理
进程创建
进程管理
worker创建完后,对请求的处理工作由worker来进行,而master进程负责对worker进程的监控管理,比如php-fpm reload和php-fpm stop。
计分板
为了解各worker进程工作情况,FPM提供了一个计分板的功能
网络编程
socket创建
在Linux中,Nginx服务器和PHP-FPM可以通过TCP socket和Unix Socket两种方式实现。Unix Socket是一种终端,可以使同一个操作系统上的两个或多个进程进行数据通信。这种方式需要在Nginx文件中填写PHP-FPM的pid文件,效率要比TCP Socket高。

1
2
3
4
5
6
7
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;;
fastcgi_pass 127.0.0.1:9000;//TCP socket
#fastcgi_pass unix:/var/run/php7-fpm.sock; //UNIX socket
fastcgi_index index.php;
}

整个FPM模式实际上是剁成金模式,首先calling process进程fork出master进程,master进程会创建socket,然后fork出worker进程,worker进程会在accept处阻塞,请求过来时,由其中一个worker进程处理,按照FastCG模式进行各阶段的读取,然后解析PHP并执行,最后按照FastCGI协议返回数据,继续进入accept处阻塞等待。另外FPM建立了计分板机制,可以关注全局和每个worker的工作情况,方便使用者监控

zend虚拟机

当PHP收到一个请求或执行命令时,会根据参数去加载对应的php代码,进行词法和语法分析,生成AST,在生成字节码,PHP中称为opcode,继而在zend虚拟机中逐行执行字节码,得到结果返回。

基础知识

符号表:符号表是编译程序在编译过程中用来记录源程序中各种名字名称的特性信息,所以也称为特性表。名字一般包括程序名、过程名、函数名、变量名、常量名等。特性信息指的是名字的种类、类型、维数、参数个数、数值及目标地址等。

符号表有什么作用呢?
1.协助进行语义检查,比如检查一个名字的引用和之前的声明是否相符
2.协助中间代码生成,最终的是在目标代码生成阶段,当需要为名字分配地址时,符号表中的信息是地址分配的主要根据

EG 全局变量executor_globals,EG(v)对应的取值宏,executor_globals对应的是结构体_zend_executor_globals,它是PHP声明周期中非常核心的数据结构。这个结构维护了符号表(symbol_table、function_table、class_table等)、执行栈(zend_vm_stack)以及包含执行指令的zend_execute_data。另外还包含了include的文件列表、autoload函数、异常处理handler等重要信息

符号表:
symbol_table
用于存放变量信息,其类型是HashTable,符号表中有我们常见的超全局变量$_GET、$_POST等,还有全局变量$a
function_table
class_table

内存管理

PHP内存管理器示意图
20190707156250555418071.png
PHP脚本运行所需内存不是直接从系统申请,而是调用zend memory manager(zend内存管理器,简称MM)提供的一系列接口函数申请;如果MM中可用内存足够,直接分配给PHP程序,如果MM中可用内存不够,MM再从系统中申请。这样可以有效减少系统调用次数,并优化内存空间的使用效率。

PHP内存管理器使用了内存池技术,当申请者第一次来申请内存时,直接申请一块大内存(通常是一页),将此次申请需要的内存部分返回给申请者,并将剩下的内部放到池子中,以后申请者在申请内存时,直接在剩下的部分中选取合适的大小返回申请者。

申请大块内存后,如何管理分配呢?
PHP内存管理器在向系统申请大块内存后,按照几种固定的规格分割成小的内存块,由内存池统一管理。当调用方申请内存时,从池子中匹配已经预分配的合适大小的内存块返回。
php7的MM将申请内存按大小分为3类: small、large、huge

源码分析工具

vim+ctags

使用前配置:

1
2
项目目录下`ctags -R .`生成tags文件
打开/etc/vim/vimrc,在最后追加`set tags=/path/to/project/tags`

使用:

1
2
ctrl+] 跳转到定义处
ctrl+t 跳回