当前位置: 首页 > 后端技术 > PHP

PHP的底层运行机制和原理

时间:2023-03-29 21:20:18 PHP

PHP的设计理念和特点多进程模型:由于PHP是多进程模型,不同的请求不会互相干扰,保证了一个请求失败不会影响整体服务。当然,随着时代的发展,PHP已经支持多线程模型了。弱类型语言:与C/C++、Java、C#等语言不同,PHP是一种弱类型语言。变量的类型不是一开始就确定的,而是在运行过程中确定的,可能会发生隐式或显式类型转换。这种机制的灵活性在web开发中是非常方便和高效的。引擎(Zend)+组件(ext)的模式,减少内部耦合。中间层(sapi)隔离web服务器和PHP。语法简单灵活,没有过多的规范。PHP的核心架构PHP的核心架构如下图所示,从下到上简单分为四层:Zend引擎:纯C实现,是PHP的核心部分,翻译PHP代码(词法、语法分析等一系列编译过程)是一切的核心,所有外围功能都是围绕Zend实现的。扩展:围绕Zend引擎,扩展以基于组件的方式提供各种基本服务。我们常用的内置函数(如数组系列)、标准库等都是通过扩展来实现的。Sapi:全称是ServerApplicationProgrammingInterface。Sapi通过一系列的钩子函数使PHP能够与外围数据进行交互。这是一个非常优雅和成功的PHP设计。通过sapi,PHP本身与上层应用成功集成。解耦和隔离,PHP可以不再考虑如何兼容不同的应用,应用本身也可以根据自己的特点实现不同的处理方式。一些常见的sapi有:apa??che2handler:这是使用apache作为webserver,运行在mod_PHP模式下的处理方式,也是使用最广泛的一种。cgi:这是webserver和PHP直接交互的另一种方式,也就是大名鼎鼎的fastcgi协议。近年来,fastcgi+PHP的使用越来越多,也是异步webserver唯一支持的方式。cli:命令行调用的应用模式Upperapplication:这是我们平时写的PHP程序,通过不同的sapi方法可以得到各种应用模式,比如通过webserver实现web应用,在命令行下运行脚本等..PHP的执行过程PHP实现了一个典型的动态语言执行过程:得到一段代码后,经过词法分析、语法分析等阶段,将源程序翻译成指令(opcodes),然后ZEND虚拟机依次执行这些指令完成操作。PHP本身是用C实现的,所以最终调用的都是C函数。其实PHP可以看成是用C开发的软件,PHP执行的核心就是一条条翻译过来的指令,也就是opcode。Opcode是PHP程序执行的最基本单位。一个操作码由两个参数(op1、op2)、返回值和处理函数组成。PHP程序最终被翻译成一组OPCODE处理功能的顺序执行。+2ZEND_IS_EQUAL_SPEC_CV_CONST:判别等a==1ZEND_IS_IDENTICAL_SPEC_CV_CONST:判别等a===1Zend引擎引入端引擎作为PHP的核心。经典的设计机制有很多,主要如下:4.1HashTable数据结构的实现HashTable是Zend的核心数据结构。它用于实现PHP中几乎所有的常用功能。我们所知道的PHP数组就是它的典型应用。另外,在Zend中,函数符号表、全局变量等功能也是基于哈希表实现的。Zendhashtable实现了典型的hashtablehash结构,同时通过附加一个双向链表,提供了对数组正向和反向遍历的功能。其结构如下图所示。哈希表中既有key->value形式的哈希结构,也有双向链表的模式,非常方便地支持快速查找和线性遍历。哈希结构:Zend的哈希结构是典型的哈希表模型,通过链表解决冲突。需要注意的是,zend的哈希表是一个自增的数据结构。当哈希表数量满时,会动态扩容2倍,重新定位元素。两者的初始大小均为8。另外,在进行key->value快速查找时,zend本身也做了一些优化,以空间换时间来加快速度。例如,变量nKeyLength将在每个元素中用于标识密钥的长度,以便快速确定。双向链表:Zend哈希表通过链表结构实现元素的线性遍历。理论上,使用单向链表进行遍历就足够了。使用双向链表的主要目的是快速删除和避免遍历。Zend哈希表是一个复合结构。当用作数组时,它支持常见的关联数组,也可以用作顺序索引号,甚至允许两者混合使用。PHP关联数组:关联数组是一个典型的hash_table应用。一个查询过程经过以下步骤(从代码可以看出,这是一个普通的hash查询过程,为了加快查找速度,加入了一些快速判断):getKeyHashValueh;index=n&nTableMask;Bucket*p=arBucket[index];while(p){if((p->h==h)&&(p->nKeyLength==nKeyLength)){RETURNp->data;}p=p->next;}返回错误;PHP索引数组:索引数组就是我们常见的数组,通过下标访问。比如arr[0],ZendHashTable内部已经归一化了,hash值和nKeyLength(0)也赋给了索引类型key。内部成员变量nNextFreeElement为当前分配的最大id,每次push后自动加1。正是这种规范化过程使PHP能够实现关系和非关系的混合。由于推送操作的特殊性,索引键在PHP数组中的顺序不是由下标的大小决定的,而是由推送的顺序决定的。例如,arr[1]=2;arr[2]=3;对于double类型的key,ZendHashTable会把它当作一个indexkey。4.2PHP变量实现原理:PHP是一种弱类型语言,没有严格区分变量的类型。PHP在声明变量时不需要指定类型。PHP可能会在程序执行期间进行变量类型的隐式转换。与其他强类型语言一样,程序中也可以进行显式类型转换。PHP变量可以分为简单类型(int、string、bool)、集合类型(array、resource、object)和常量(const)。以上所有变量都在同一个结构体zval下。Zval是zend中另一个非常重要的数据结构,用于PHP变量的标识和实现。其数据结构如下:Zval结构体主要由三部分组成:type:指定变量描述的类型(整数、字符串、数组等)refcount&is_ref:用于实现引用计数(后面会详细介绍)value:核心部分,存储变量的实际数据。Zvalue用于保存变量的实际数据。因为要存储多种类型,所以zvalue是一个union,也是实现了弱类型。PHP变量类型与其实际存储的对应关系如下:IS_LONG->lvalueIS_DOUBLE->dvalueIS_ARRAY->htIS_STRING->strIS_RESOURCE->lvalue4.2.1整型和浮点型变量整型和浮点型是PHP中的基本类型之一,它们也是一个简单的类型变量。对于整数和浮点数,直接将对应的值存入zvalue。它的类型分别是long和double。从zvalue结构可以看出,对于整数类型,与C等强类型语言不同,PHP不区分int、unsignedint、long、longlong等类型,对于它来说,只有一种整数类型,它是long。由此可见,在PHP中,整数的取值范围是由编译器的位数决定的,而不是固定的。对于浮点数,类似于整数,不区分float和double,只区分double的一种。在PHP中,整数范围越界怎么办?在这种情况下,它将自动转换为double类型。小心这一点,因为它会产生许多技巧。4.2.2字符变量和整数一样,字符变量也是PHP中的基本类型和简单变量。从zvalue结构可以看出,在PHP中,一个字符串是由一个指向实际数据的指针和一个length结构组成的,类似于C++中的字符串。由于长度是用实际变量表示的,不像c,它的字符串可以是二进制数据(包括\0),而在PHP中,求字符串长度strlen是一个O(1)的操作。在添加、修改和追加字符串操作时,PHP会重新分配内存以生成新的字符串。最后,出于安全原因,PHP仍会在字符串末尾添加\0。常见字符串拼接及速度比较:假设有以下4个变量:strA='123';strB='456';intA=123;intB=456;现对以下几种字符串拼接方式进行对比说明:1res=strA.strB和res=“strAstrB”这种情况下,zend会重新malloc一块内存并进行相应处理,速度一般。2strA=strA.strB这个是最快的,zend会直接根据当前的strA进行relloc,避免重复拷贝3res=intA.intB这个比较慢,因为需要做隐式格式转换,实际写在程序里,你还要注意避免4strA=sprintf("%s%s",strA,strB);这将是最慢的方式,因为sprintf在PHP中不是语言结构,它本身是为了格式识别和处理需要大量时间,而且机制本身也是malloc。但sprintf方法的可读性最高,实际中可以根据具体情况灵活选择。4.2.3数组变量PHP数组自然是通过ZendHashTable来实现的。foreach操作是如何实现的?数组的foreach是通过遍历哈希表中的双向链表来完成的。对于有索引的数组,通过foreach遍历的效率比for高很多,省去了key->value的查找。计数操作直接调用HashTable->NumOfElements,O(1)操作。对于像'123'这样的字符串,zend会将其转换为整数形式。arr['123']和arr[123]是等价的4.2.4资源变量资源类型变量是PHP中最复杂的变量,也是一个复合结构。PHP的zval可以表示范围很广的数据类型,但是很难完全描述自定义数据类型。由于没有有效的方法来表示这些复合结构,因此无法对它们使用传统的运算符。要解决这个问题,只需通过本质上任意的标识符(标签)来引用指针,这称为资源。在zval中,对于resource,lval是作为一个指针,直接指向资源所在的地址。Resource可以是任意复合结构,熟悉的mysqli、fsock、memcached等都是资源。如何使用资源:1注册:对于一个自定义的数据类型,你想把它当作一个资源来使用。首先,它需要注册,zend会给它分配一个全球唯一的标识符。2获取一个资源变量:对于资源,zend维护了一个id->actualdata的hash_tale。对于资源,只有它的id记录在zval中。取的时候通过id在hash_table中找到具体的值返回。3资源破坏:资源的数据类型多种多样。Zend本身没有办法摧毁它。因此,用户在注册资源时需要提供销毁功能。当unsetresources时,zend会调用相应的函数完成销毁。同时将其从全局资源表中删除。资源可以长期存在,不仅在引用它的所有变量超出范围之后,甚至在请求结束并发出新请求之后也是如此。这些资源称为持久性资源,因为它们在SAPI的整个生命周期中持续存在,除非有意销毁。在很多情况下,持久化资源可以在一定程度上提高性能。比如我们常见的mysql_pconnect,持久化资源是通过pemalloc分配内存的,这样在请求结束的时候不会释放。对于zend来说,两者本身并没有区别。4-3。PHP变量管理——引用计数和写时复制:引用计数广泛应用于内存回收、字符串操作等,Zval的引用计数是通过成员变量is_ref和ref_count实现的。通过引用计数,多个变量可以共享同一份数据。避免频繁复制造成的大量消耗。赋值操作时,zend将变量指向同一个zval,而ref_count++,unset操作时,指向对应的ref_count-1。只有当ref_count减为0时,才会执行真正的销毁操作。如果是引用赋值,zend会修改is_ref为1。PHP变量通过引用计数实现变量共享数据,那么其中一个变量值改变了怎么办?当试图写一个变量时,如果Zend发现该变量指向的zval被多个变量共享,它会复制一个ref_count为1的zval,并递减原zval的refcount。这个过程称为“zval分离”。可见zend只有在写操作发生的时候才会进行复制操作,所以也被称为写时复制(copy-on-write)。对于引用变量,要求和非引用类型相反,引用赋值的变量必须绑定,修改一个变量会修改所有捆绑变量。4-4。PHP局部变量和全局变量的实现:PHP中局部变量和全局变量是如何实现的?对于一个请求,PHP随时可以看到两个符号表(symbol_table和active_symbol_table),前者用于维护全局变量。后者是指向当前活动变量符号表的指针。当程序进入一个函数时,zend会为其分配一个符号表x,并将active_symbol_table指向a。这样就实现了全局变量和局部变量的区分。获取变量值:PHP的符号表是通过hash_table实现的,为每个变量分配一个唯一的标识符。获取时,根据标识符从表中找到对应的zval返回。在函数中使用全局变量:在函数中,我们可以通过显式声明global来使用全局变量。在active_symbol_table中创建对symbol_table同名变量的引用(引用变量的值需要更新,大家一起更新),如果symbol_table中没有同名变量,则首先创建。