PHP底层工作机制及原理剖析:简单但不易
PHP说是简单,但掌握起来并不容易。除了知道如何使用它之外,我们还需要知道它是如何工作的。
PHP 是一种适合 Web 开发的动态语言。更具体地说,它是一个使用C语言实现大量组件的软件框架。从狭义上讲,它可以被认为是一个强大的UI框架。
了解PHP实现的目的是什么?要想用好动态语言,首先要理解它。内存管理和框架模型值得我们借鉴。通过扩展开发,我们可以实现更强大的功能并优化程序的性能。
1。 PHP设计理念及特点
多进程模型:由于PHP是多进程模型,不同的请求不会互相干扰。这保证了单个请求的失败不会影响整个服务。当然,随着Aja的不断发展,PHP已经支持多线程模型了。
弱类型语言:与C/C++、Java、C#等语言不同,PHP是一种弱类型语言。变量的类型最初并未设置。这是在运行时确定的,并且可能会发生隐式或显式类型转换。这种机制的灵活性在Web开发中非常方便和高效。稍后将在 PHP 中讨论详细信息。详细描述了变量。
Engine (Zend) + Component (ext) 模式减少内部耦合力。
中间层(sapi)隔离Web服务器和PHP。
语法简单灵活,没有过多的规范。缺点导致风格混乱,但程序员再差,也不会写出太过离谱、对大局造成威胁的程序。 ?纯C 实现是PHP 的核心部分。它将PHP代码(一系列的编译过程,如词法、语法分析)翻译成处理可执行的操作码,并实现相应的处理方法,并实现基本的数据结构(如hashtable、oo)、内存分配和管理,并提供了相应的api方法。用于外部呼叫。这是一切的核心。所有外围功能都是围绕Zend实现的。
扩展:围绕 Zend 引擎,扩展在逐个组件的基础上提供不同的核心服务。我们常见的内置函数(比如数组序列化)、标准库等都是通过扩展来实现的。如果需要,用户还可以使用扩展。实现您的扩展,以实现功能扩展、性能优化等目标(例如贴吧目前使用的PHP中间层和富文本解析就是典型的扩展应用)。
Sapi:Sapi的全称是Server Application Programming Interface,即服务器应用程序编程接口。 Sapi 使用许多钩子函数来允许 PHP 与外部数据交互。这是一个非常优雅和成功的 PHP 设计。通过胆汁成功。通过将PHP与自身的上层应用解耦、隔离,PHP可以不再思考如何兼容不同的应用,而应用本身可以根据自身的特点实现不同的处理方式。
顶层应用程序:这是我们平时编写的PHP程序。我们通过sap的不同方式得到不同的应用模式,比如通过web服务器部署web应用,以脚本方式在命令行运行等等。
如果 PHP 是一辆汽车,那么汽车的框架就是 PHP 本身,Zend 就是汽车的引擎(引擎),Ext 下的各个组件就是汽车的轮子。胆可以认为是一条路,汽车可以在不同类型的道路上行驶,执行PHP程序就意味着汽车在道路上行驶。因此我们需要:高性能发动机+合适的车轮+合适的履带。
3。 Sapi
如前所述,Sapi允许外部应用程序通过多个接口与PHP交换数据,并根据不同的应用特性实现特定的处理方法。我们最常见的一些句柄是:
apache2handler:这是使用 apache 作为 Web 服务器并在 mod_PHP 模式下运行时的处理程序方法。它也是当今使用最广泛的。
cgi:这是Web服务器与PHP之间的另一种直接通信方式,这就是著名的fastcgi协议。 Fastcgi+PHP近年来被越来越多地使用,也是异步Web服务器支持的唯一方法。
cli:命令行调用的应用模式
4。 PHP执行流程和Opcode
我们首先看一下PHP代码执行流程。
从图中可以看到,PHP实现了一个典型的动态语言执行过程:获得一段代码后,经过词法分析、语法分析等步骤,将源程序翻译成命令(操作码)。然后ZEND虚拟机依次执行这些指令来完成操作。 PHP本身是用C实现的,所以你最终调用的函数都是C函数。事实上,我们可以将PHP视为用C开发的软件。
PHP执行的核心是翻译后的命令,即操作码。
操作码是PHP程序执行的最基本单位。操作码由两个参数(op1、op2)、一个返回值和一个处理函数组成。 PHP程序最终被翻译为顺序执行操作码处理函数。
几个常用的处理函数:
PHP
ZEND_ASSIGN_SPEC_CV_CV_HANDLER:变量分配($a=$b)
ZEND_DO_FCALL_BY_NAME_LER_SPEC:END_CONZAT_CON _CV_CV_HANDLER:字符串拼写取$a.$b
ZEND_ADD_SPEC_CV_CONST_HANDLER:添加$a+ 2
ZEND_IS_EQUAL_SPEC_CV_CONST:判断相等 $a==1
ZEND_IS_IDENTICAL_SPEC_CV_CONST:判断相等 $a===1
5。 HashTable — 基本数据结构
HashTable 是 zend Zend 的基本数据结构,用于实现 PHP 中几乎所有常用功能。我们知道的PHP数组就是一个典型的应用。另外,函数符号表、全局变量等也是基于zendis哈希表实现的。
PHP 哈希表具有以下特性:
支持典型的 key->value 查询
可用作数组
添加和删除节点的复杂度为 O(1)
混合类型 : At同时有关联数字组合数组的索引
Value 支持混合类型:array("string", 2332)
支持线性遍历:例如 foreach
Zend 的哈希表实现了典型的哈希表 hash结构,同时添加双向链表提供正向和反向数组遍历功能。它的结构如下:
可以看到,哈希表既有key->value的哈希结构,又有双向链表模式,这使得它非常方便支持快速查找和线性遍历。
哈希结构:Zend的哈希结构是典型的哈希表模型,通过链表解决冲突。需要注意的是,zendi哈希表是一种自增长的数据结构。当哈希表已满时,它会动态扩展2倍并重新排列元素。默认大小为8。另外,zend本身在进行key->value快速查找时也做了一些优化,通过用空间换取时间来加快处理速度。例如,每个元素中使用 nKeyLength 变量来标识密钥长度,以便快速确定。
双向链表:Zend 哈希表通过链表结构实现元素的线性遍历。理论上,使用单向链表就足以进行遍历。使用双向链表的主要目的是快速删除和避免遍历。 Zend 哈希表是一个复合结构。当用作数组时,它支持常规关联数组,也可以用作顺序索引号,甚至允许混合两者。
PHP 关联数组:关联数组是典型的 hash_table 实现。查询过程经历以下步骤(如代码中所示,这是一个正常的哈希查询过程,添加了一些快速决策以加快搜索速度。):
PHP
getKeyHashValueh;
index=n&nTableMask;
Bucket*p=arBucket[index ];
while(p){
if((p->h==h)&(p->nKeyLength==nKeyLength)){
RETURNp-> data;
}
p=p->next;
}
RETURNFALTURE;
PHP索引数组:索引数组是通过我们的公共子数组访问的。例如$arr[0],Zend HashTable内部进行了规范化,并且将hash值和nKeyLength(0)也赋值给了索引类型key。内部成员变量nNextFreeElement是当前设置的最大id,每次push后都会自动加1。正是这种规范化过程使 PHP 能够实现关联数据和非关联数据的混合。由于push操作的特殊性,PHP数组中索引键的顺序不是由子索引的大小决定的,而是由push的顺序决定的。例如,$arr[1] = 2; $arr[2] = 3;对于 double 类型的键,Zend HashTable 将它们视为索引键
6。 PHP 变量
PHP 是一种弱类型语言。变量本身的类型并没有严格区分。 PHP 在声明变量时不需要指定类型。 PHP可以在程序执行过程中进行变量类型的隐式转换。与其他强类型语言类似,该程序也可以执行显式类型转换。PHP变量可以分为简单类型(int、string、bool)、集合类型(数组资源对象)和常量(const)。上述所有变量在底层都有相同的结构 zval。
Zval 是 zend 中另一个非常重要的数据结构,用于识别和实现 PHP 变量。其数据结构如下:
Zval 主要由三部分组成:
type:定义变量的类型。 (整数、字符串、数组等)
refcount&is_ref:用于实现引用计数(后面详述)
value:存储变量实际数据的主要部分
Zvalue 存储变量的实际数据数据。由于需要存储多种类型,所以zvalue是一个union,从而实现弱类型。
PHP变量类型及其实际存储对应关系为:
PHP
IS_LONG->lvalue
IS_DOUBLE->dvalue
IS_ARRAY->♺♺
❀ IS_RESOURCE ->lvalue
引用计数广泛应用于内存复用、字符串操作等。 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
对于引用变量,要求与非引用类型相反。通过引用定义的变量必须捆绑在一起,更改一个变量会更改所有捆绑的变量。
整数和浮点数是PHP的基本类型之一,也是简单变量。对于整数和浮点数,对应的值直接存储在zvalue中。它们的类型分别是long和double。
Zvalue结构表明,与C等强类型语言不同,PHP对于整数类型并不区分int、unsigned int、long、long long等类型。为此只有一种整数类型。也就是说,长。这说明PHP中整数的取值范围是由编译器的位数决定的,并不是固定的。
和整数一样,浮点数不区分float和double,只区分double。
在 PHP 中如果整数范围越界该怎么办?在这种情况下,它会自动转换为 double 类型。你必须小心这一点,因为它会导致很多技巧。
与整数一样,字符变量是 PHP 中的基本类型和简单变量。从 zvalue 结构中可以看出,PHP 中的字符串由指向实际数据的指针和类似于 C++ 字符串的长度结构组成。由于length是用实际变量表示的,与c不同,它的字符串可以是二进制数据(含)。同时,在 PHP 中查找字符串的长度 strlen 是一个 O(1) 操作。
当添加、更改或添加字符串操作时,PHP 会重新分配内存以生成新字符串。最后,出于安全考虑,PHP在生成字符串时仍然在末尾添加
。常见字符串拼接方式及速度对比:
假设有以下4个变量:$strA='123'; $strB = '456'; $intA=123; intB=456;
现在比较解释以下字符串拼接方法:
PHP
$res=$strA.$strB 和 $res=“$strA$strB”
本例中 zend会再次删除一块内存并进行相应的处理。速度一般是
$strA=$strA.$strB
这个是最快的了,zend直接根据当前strA进行移动,避免重复复制。
$res=$intA.$intB
这会比较慢,因为它需要隐式格式转换,这也应该在实际编程中完成。小心避免
$strA=sprintf(“%s%s” , $strA.$strB);
这应该是最慢的方式,因为sprintf不是PHP中的语言结构,但其本身的格式检测和处理需要花费大量的时间,而其机制本身就是malloc。不过可读性最好的还是sprintf方法,实际中可以根据具体情况灵活选择。
PHP 数组自然是通过 Zend HashTable 实现的。
如何实现foreach操作?数组Foreach是通过遍历哈希表的双向链表来完成的。对于索引数组来说, foreach 遍历比 for 高效得多,无需查找 key->value 。 count操作直接调用HashTable->NumOfElements,O(1)操作。对于像 '123' 这样的字符串,zend 将其转换为整数。 $arr[‘123’]和$arr[123]是等价的
资源类型变量是PHP中最复杂的变量,也是复合结构。
PHP zval 可以表示大量的数据类型,但是自定义的数据类型很难完全描述。由于没有有效的方法来表示这些复合结构,因此无法使用传统运算符。要解决这个问题,您只需要通过称为资源的本质上任意的标识符(标签)来引用光标。
在zval中,lval用于资源作为直接指向资源地址的指针。资源可以是任何复合结构。我们熟悉的mysqli、fsock、memcached等都是资源。
如何使用资源:
注册:对于自定义数据类型,您希望将其用作资源。首先,您需要注册它,zend 将为它分配一个全局唯一标识符。
获取资源变量。对于资源,zend 管理 id->实际数据 hash_tale。对于资源,zval 中仅存储其 ID。拉取时,通过id在哈希表中查找特定值并返回。
资源破坏:资源具有不同的数据类型。 Zend 本身没有办法销毁它。因此,用户在注册资源时必须提供销毁函数。当资源被销毁时,zend调用相应的函数来完成销毁。同时将其从全局资源表中删除。
资源可以持续很长时间,不仅是在引用它们的所有变量超出范围之后,而且甚至在一个请求完成并生成新请求之后也是如此。这些资源称为持久资源,因为它们在整个 SAPI 生命周期中持续存在,除非专门销毁。很多情况下,持久化资源可以在一定程度上提高性能。例如,在我们的标准 mysql_pconnect 中,持久资源通过 pemalloc 分配内存,因此查询完成时它们不会被释放。
对于 Zend 而言,两者没有区别。
PHP 中局部变量和全局变量是如何实现的?对于查询,PHP 随时会看到两个符号表(符号表和 active_symbol 表),第一个用于存储全局变量。后者是指向当前活动变量符号表的指针。当程序进入一个函数时,zend为其分配符号表x,并将active_symbol表指向a。这样就实现了全局变量和局部变量的区分。
获取变量值:PHP符号表是通过hash_table实现的。每个变量都被分配了一个唯一的标识符。收到时,根据标识符在表中找到对应的zval并返回。
在函数中使用全局变量:我们可以通过显式声明全局变量来在函数中使用全局变量。创建对 active_symbol_table 表中同名变量的引用。如果符号表中没有同名变量,则先创建它。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。