扩展是PHP的重要组成部分,它是PHP提供给开发者用于扩展PHP语言功能的主要方式。开发者可以用C/C++定义自己的功能,通过扩展嵌入到PHP中,灵活的扩展能力使得PHP拥有了大量、丰富的第三方组件,这些扩展很好的补充了PHP的功能、特性,使得PHP在web开发中得以大展身手。ext目录下有一个standard扩展,这个扩展提供了大量被大家所熟知的PHP函数:sleep()、usleep()、htmlspecialchars()、md5()、strtoupper()、substr()、array_merge()等等。
C语言是PHP之母,作为世界上非常优秀的一门语言,自它诞生至今,C语言早就了大量优秀、知名的项目:Linux、Nginx、MySQL、PHP、Redis、Memcached等等,感谢里奇带给这个世界如此伟大的一份礼物。C语言的优秀也折射到PHP身上,但是PHP内核提供的功能终究有限,如果你发现PHP在某些方面已经满足不了你的需求了,那么不妨试试扩展。
常见的,扩展可以在以下几个方面有所作为:
当然扩展也不是万能,它只允许我们在PHP提供的框架之上进行一些特定的处理,同时限于SAPI的差异,扩展也必须要考虑到不同SAPI的实现特点。
PHP中的扩展分为两类:PHP扩展、Zend扩展,对内核而言这两个分别称之为:模块(module)、扩展(extension),本章主要介绍是PHP扩展,也就是模块。
在C语言中声明在任何函数之外的变量为全局变量,全局变量为各线程共享,不同的线程引用同一地址空间,如果一个线程修改了全局变量就会影响所有的线程。所以线程安全是指多线程环境下如何安全的获取公共资源。
PHP的SAPI多数是单线程环境,比如cli、fpm、cgi,每个进程只启动一个主线程,这种模式下是不存在线程安全问题的,但是也有多线程的环境,比如Apache,或用户自己嵌入PHP实现的环境,这种情况下就需要考虑线程安全的问题了,因为PHP中有很多全局变量,比如最常见的:EG、CG,如果多个线程共享同一个变量将会冲突,所以PHP为多线程的应用模型提供了一个安全机制:Zend线程安全(Zend Thread Safe, ZTS)。
PHP中专门为解决线程安全的问题抽象出了一个线程安全资源管理器(Thread Safe Resource Mananger, TSRM),实现原理比较简单:既然共用资源这么困难那么就干脆不共用,各线程不再共享同一份全局变量,而是各复制一份,使用数据时各线程各取自己的副本,互不干扰。
TSRM核心思想就是为不同的线程分配独立的内存空间,如果一个资源会被多线程使用,那么首先需要预先向TSRM注册资源,然后TSRM为这个资源分配一个唯一的编号,并把这种资源的大小、初始化函数等保存到一个tsrm_resource_type
结构中,各线程只能通过TSRM分配的那个编号访问这个资源;然后当线程拿着这个编号获取资源时TSRM如果发现是第一次请求,则会根据注册时的资源大小分配一块内存,然后调用初始化函数进行初始化,并把这块资源保存下来供这个线程后续使用。
TSRM中通过两个结构分别保存资源信息以及具体的资源:tsrm_resource_type、tsrm_tls_entry,前者是用来记录资源大小、初始化函数等信息的,具体分配资源内存时会用到,而后者用来保存各线程所拥有的全部资源:
struct _tsrm_tls_entry { void **storage; //资源数组 int count; //拥有的资源数:storage数组大小 THREAD_T thread_id; //所属线程id tsrm_tls_entry *next; }; typedef struct {
前面已经介绍过PHP变量的内存管理,即引用计数机制,当变量赋值、传递时并不会直接硬拷贝,而是增加value的引用数,unset、return等释放变量时再减掉引用数,减掉后如果发现refcount变为0则直接释放value,这是变量的基本gc过程,PHP正是通过这个机制实现的自动垃圾回收,但是有一种情况是这个机制无法解决的,从而因变量无法回收导致内存始终得不到释放,这种情况就是循环引用,简单的描述就是变量的内部成员引用了变量自身,比如数组中的某个元素指向了数组,这样数组的引用计数中就有一个来自自身成员,试图释放数组时因为其refcount仍然大于0而得不到释放,而实际上已经没有任何外部引用了,这种变量不可能再被使用,所以PHP引入了另外一个机制用来处理变量循环引用的问题。
下面看一个数组循环引用的例子:
$a = [1]; $a[] = &$a; unset($a);
unset($a)
之前引用关系:
注意这里$a的类型在&
操作后已经转为引用,unset($a)
之后:
可以看到,unset($a)
之后由于数组中有子元素指向$a
,所以refcount = 1
,此时是无法通过正常的gc机制回收的,但是$a已经已经没有任何外部引用了,所以这种变量就是垃圾,垃圾回收器要处理的就是这种情况,这里明确两个准则:
1) 如果一个变量value的refcount减少到0, 那么此value可以被释放掉,不属于垃圾
2) 如果一个变量value的refcount减少之后大于0,那么此zval还不能被释放,此zval可能成为一个垃圾
针对第一个情况GC不会处理,只有第二种情况GC才会将变量收集起来。另外变量是否加入垃圾检查buffer并不是根据zval的类型判断的,而是与前面介绍的是否用到引用计数一样通过zval.u1.type_flag
记录的,只有包含IS_TYPE_COLLECTABLE
的变量才会被GC收集。
目前垃圾只会出现在array、object两种类型中,数组的情况上面已经介绍了,object的情况则是成员属性引用对象本身导致的,其它类型不会出现这种变量中的成员引用变量自身的情况,所以垃圾回收只会处理这两种类型的变量。
#define IS_TYPE_COLLECTABLE
| type | collectable | +-------
zend针对内存的操作封装了一层,用于替换直接的内存操作:malloc、free等,实现了更高效率的内存利用,其实现主要参考了tcmalloc的设计。
源码中emalloc、efree、estrdup等等就是内存池的操作。
内存池是内核中最底层的内存操作,定义了三种粒度的内存块:chunk、page、slot,每个chunk的大小为2M,page大小为4KB,一个chunk被切割为512个page,而一个或若干个page被切割为多个slot,所以申请内存时按照不同的申请大小决定具体的分配策略:
chunk由512个page组成,其中第一个page用于保存chunk结构,剩下的511个page用于内存分配,page主要用于Large、Small两种内存的分配;heap是表示内存池的一个结构,它是最主要的一个结构,用于管理上面三种内存的分配,Zend中只有一个heap结构。
struct _zend_mm_heap { #if ZEND_MM_STAT size_t size; //当前已用内存数 size_t peak; //内存单次申请的峰值 #endif zend_mm_free_slot *free_slot[ZEND_MM_BINS]; // 小内存分配的可用位置链表,ZEND_MM_BINS等于30,即此数组表示的是各种大小内存对应的链表头部 ... zend_mm_huge_list *huge_list; //大内存链表 zend_mm_chunk *main_chunk;
PHP的异常处理与其它语言的类似,在程序中可以抛出、捕获一个异常,异常抛出必须只有定义在try{...}块中才可以被捕获,捕获以后将跳到catch块中进行处理,不再执行try中抛出异常之后的代码。
异常可以在任意位置抛出,然后将由最近的一个try所捕获,如果在当前执行空间没有进行捕获,那么将调用栈一直往上抛,比如在一个函数内部抛出一个异常,但是函数内没有进行try,而在函数调用的位置try了,那么就由调用处的catch捕获。
接下来我们从两个方面介绍下PHP异常处理的实现。
异常捕获及处理的语法:
try{ try statement; }catch(exception_class_1 $e){ catch statement 1; }catch(exception_class_2 $e){ catch statement 2; }finally{ finally statement; }
try表示要捕获try statement中可能抛出的异常;catch是捕获到异常后的处理,可以定义多个,当try中抛出异常时会依次检查各个catch的异常类是否与抛出的匹配,如果匹配则有命中的那个catch块处理;finally为最后执行的代码,不管是否有异常抛出都会执行。
语法规则:
statement: ... | T_TRY '{' inner_statement_list '}' catch_list finally_statement { $$ = zend_ast_create(ZEND_AST_TRY, $3, $5, $6); } ... ; catch_list: /* empty */ { $$ = zend_ast_create_list(0, ZEND_AST_CATCH_LIST); } | catch_list T_CATCH '(' name T_VARIABLE ')' '{' inner_statement_list '}' { $$ = zend_ast_list_add($1, zend_ast_create(ZEND_AST_CATCH, $4, $5,
PHP中的中断及跳转语句主要有break、continue、goto,这几种语句的实现基础都是跳转。
break用于结束当前for、foreach、while、do-while 或者 switch 结构的执行;continue用于跳过本次循环中剩余代码,进行下一轮循环。break、continue是非常相像的,它们都可以接受一个可选数字参数来决定跳过的循环层数,两者的不同点在于break是跳到循环结束的位置,而continue是跳到循环判断条件的位置,本质在于跳转位置的不同。
break、continue的实现稍微有些复杂,下面具体介绍下其编译过程。
上一节我们已经介绍过循环语句的编译,其中在各种循环编译过程中有两个特殊操作:zend_begin_loop()、zend_end_loop(),分别在循环编译前以及编译后调用,这两步操作就是为break、continue服务的。
在每层循环编译时都会创建一个zend_brk_cont_element
的结构:
typedef struct _zend_brk_cont_element { int start; int cont; int brk; int parent; } zend_brk_cont_element;
cont记录的是当前循环判断条件opcode起始位置,brk记录的是当前循环结束的位置,parent记录的是父层循环zend_brk_cont_element
结构的存储位置,也就是说多层嵌套循环会生成一个zend_brk_cont_element
的链表,每层循环编译结束时更新自己的zend_brk_cont_element
结构,所以break、continue的处理过程实际就是根据跳出的层级索引到那一层的zend_brk_cont_element
结构,然后得到它的cont、brk进行相应的opcode跳转。
各循环的zend_brk_cont_element
结构保存在zend_op_array->brk_cont_array
数组中,编译各循环时依次申请一个zend_brk_cont_element
,zend_op_array->last_brk_cont
记录此数组第一个可用位置,每申请一个元素last_brk_cont就相应的增加
实际应用中有许多具有规律性的重复操作,因此在程序中就需要重复执行某些语句。循环结构是在一定条件下反复执行某段程序的流程结构,被反复执行的程序被称为循环体。循环语句是由循环体及循环的终止条件两部分组成的。
PHP中的循环结构有4种:while、for、foreach、do while,接下来我们分析下这几个结构的具体的实现。
while循环的语法:
while(expression) { statement;//循环体 }
while的结构比较简单,由两部分组成:expression、statement,其中expression为循环判断条件,当expression为true时重复执行statement,具体的语法规则:
statement: ... | T_WHILE '(' expr ')' while_statement { $$ = zend_ast_create(ZEND_AST_WHILE, $3, $5); } ... ; while_statement: statement { $$ = $1; } | ':' inner_statement_list T_ENDWHILE ';' { $$ = $2; } ;
从while语法规则可以看出,在解析时会创建一个ZEND_AST_WHILE
节点,expression、statement分别保存在两个子节点中,其AST如下:
while编译的过程也比较简单,比较特别的是while首先编译的是循环体,然后才是循环判断条件,更像是do while,编译过程大致如下:
具体的编译过程:
void zend
程序并不都是顺序执行的,选择结构用于判断给定的条件,根据判断的结果来控制程序的流程。PHP中通过if、elseif、else和switch语句实现条件控制。这一节我们就分析下PHP中两种条件语句的具体实现。
If语句用法:
if(Condition1){ Statement1; }elseif(Condition2){ Statement2; }else{ Statement3; }
IF语句有两部分组成:condition(条件)、statement(声明),每个条件分支对应一组这样的组合,其中最后的else比较特殊,它没有条件,编译时也是按照这个逻辑编译为一组组的condition和statement,其具体的语法规则如下:
if_stmt: if_stmt_without_else %prec T_NOELSE { $$ = $1; } | if_stmt_without_else T_ELSE statement { $$ = zend_ast_list_add($1, zend_ast_create(ZEND_AST_IF_ELEM, NULL, $3)); } ; if_stmt_without_else: T_IF '(' expr ')' statement { $$ = zend_ast_create_list(1, ZEND_AST_IF, zend_ast_create(ZEND_AST_IF_ELEM, $3, $5)); } | if_stmt_without_else T_ELSEIF '(' expr ')' statement { $$ = zend_ast_list_add($1, zend_ast_create(ZEND_AST_IF_ELEM, $4, $6)); } ;
从上面的语法规则可以看出,编译if语句时首先会创建一个ZEND_AST_IF
的节点,这个节点是一个list,用于保存各个分支的condition、statement,编译每个分支时将创建一个ZEND_AST_IF_ELEM
的
PHP是弱类型语言,不需要明确的定义变量的类型,变量的类型根据使用时的上下文所决定,也就是变量会根据不同表达式所需要的类型自动转换,比如求和,PHP会将两个相加的值转为long、double再进行加和。每种类型转为另外一种类型都有固定的规则,当某个操作发现类型不符时就会按照这个规则进行转换,这个规则正是弱类型实现的基础。
除了自动类型转换,PHP还提供了一种强制的转换方式:
无论是自动类型转换还是强制类型转换,不是每种类型都可以转为任意其他类型。
这种转换比较简单,任意类型都可以转为NULL,转换时直接将新的zval类型设置为IS_NULL
即可。
当转换为 boolean 时,根据原值的TRUE、FALSE决定转换后的结果,以下值被认为是 FALSE:
所有其它值都被认为是 TRUE,比如资源、对象(这里指默认情况下,因为可以通过扩展改变这个规则)。
判断一个值是否为true的操作:
static zend_always_inline int i_zend_is_true(zval *op) { int result = 0; again: switch (Z_TYPE_P(op)) { case IS_TRUE: result = 1; break; case IS_LONG: //非0即真 if (Z_LVAL_P(op)) { result = 1; } break; case IS_DOUBLE: if (Z_D
在本节开始之前我们先分析一个例子:
class my_class { public $id = 123; public function test() { echo $this->id; } } $obj = new my_class; $obj->test(); $obj->test(); ...
这个例子定义了一个类,然后多次调用同一个成员方法,这个成员方法功能很简单:输出一个成员属性,根据前面对成员属性的介绍可以知道其查找过程为:"首先根据对象找到所属zend_class_entry,然后再根据属性名查找zend_class_entry.properties_info
哈希表,得到zend_property_info
,最后根据属性结构的offset定位到属性值的存储位置",概括一下这个过程就是:zend_object->zend_class_entry->properties_info->属性值,那么问题来了:每次执行my_class::test()
时难道上面的过程都要完整走一遍吗?
我们再仔细看下这个过程,字面量"id"在"$this->id"此条语句中就是用来索引属性的,不管执行多少次它的任务始终是这个,那么有没有一种办法将"id"与查找到的zend_class_entry、zend_property_info.offset建立一种关联关系保存下来,这样再次执行时直接根据"id"拿到前面关联的这两个数据,从而避免多次重复相同的工作呢?这就是本节将要介绍的内容:运行时缓存。
在执行期间,PHP经常需要根据名称去不同的哈希表中查找常量、函数、类、成员方法、成员属性等,因此PHP提供了一种缓存机制用于缓存根据名称查找到的结果,以便再次执行同一opcode时直接复用上次缓存的值,无需重复查找,从而提高执行效率。
开始提到的那个例子中会缓存两个东西:zend_class_entry、zend_property_info.offset,此缓存可以认为是opcode操作的缓存,它只属于"$this->id"此语句的opcode:这样再次执行这条opcode时就直接取出上次缓存的两个值。
所以运行时缓存机制是在同一opcode执行多次的情况下才会生效,特别注意这里的同一opcode指的并不是opcode值相同,而是指内存里的