王帅:深入PHP内核



深入PHP内核


关于作者:王帅,腾讯企业QQ SaaS团队Leader。转载https://blog.csdn.net/heiyeshuwu/article/details/42262951


深入PHP内核(一)——弱类型变量原理探究


摘要:PHP作为一门简单而强大的语言,能够提供很多Web适用的语言特性,而从本期《问底》开始,王帅将从实践出发,带你弄清PHP内核中一些常用的部分,比如这里的“弱类型变量原理”。

PHP是一门简单而强大的语言,提供了很多Web适用的语言特性,其中就包括了变量弱类型,在弱类型机制下,你能够给一个变量赋任意类型的值。 

PHP的执行是通过Zend Engine(下面简称ZE),ZE是使用C编写,在底层实现了一套弱类型机制。ZE的内存管理使用写时拷贝、引用计数等优化策略,减少再变量赋值时候的内存拷贝。

下面不光带你探索PHP弱类型的原理,也会在写PHP扩展角度,介绍如何操作PHP的变量。 

1. PHP的变量类型

PHP的变量类型有8种:

  • 标准类型:布尔boolen,整型integer,浮点float,字符string
  • 复杂类型:数组array,对象object
  • 特殊类型:资源resource  

PHP不会严格检验变量类型,变量可以不显示的声明其类型,而在运行期间直接赋值。也可以将变量自由的转换类型。如下例,没有实现声明的情况下,$i可以赋任意类型的值。

[php]  view plain copy
  1. <? php  $i = 1;   //int $i = 'show me the money';  //string $i = 0.02;  // float $i = array(1, 2, 3);  // array $i = new Exception('test', 123); // object $i = fopen('/tmp/aaa.txt', 'a') // resource ?>   

如果你对弱类型原理理解不深刻,在变量比较时候,会出现“超出预期”的惊喜。

[php]  view plain copy
  1. <? PHP $str1 = null;  $str2 = false;  echo $str1==$str2 ? '相等' : '不相等';  $str3 = '';  $str4 = 0;  echo $str3==$str4 ? '相等' : '不相等';  $str5 = 0;  $str6 = '0';  echo $str5==$str6 ? '相等' : '不相等';  ?>   

以上三个结果全部是相等,因为在变量比较的时候,PHP内部做了变量转换。如果希望值和类型同时判断,请使用三个=(如,$a===0)来判断。也许你会觉得司空见惯,也许你会觉得很神奇,那么请跟我一起深入PHP内核,探索PHP变量原理。

2. 变量的存储及标准类型介绍

PHP的所有变量,都是以结构体zval来实现,在Zend/zend.h中我们能看到zval的定义:

[php]  view plain copy
  1. typedef union _zvalue_value {     long lval;                 /* long value */     double dval;               /* double value */     struct {                            char *val;         int len;               /* this will always be set for strings */     } str;                     /* string (always has length) */     HashTable *ht;             /* an array */     zend_object_value obj;     /* stores an object store handle, and handlers */  } zvalue_value;   

属性名 含义 默认值
refcount__gc 表示引用计数 1
is_ref__gc 表示是否为引用 0
value 存储变量的值  
type 变量具体的类型  

其中refcount__gc和is_ref__gc表示变量是否是一个引用。type字段标识变量的类型,type的值可以是:IS_NULL,IS_BOOL,IS_LONG,IS_FLOAT,IS_STRING,IS_ARRAY,IS_OBJECT,IS_RESOURCE。PHP根据type的类型,来选择如何存储到zvalue_value。 

zvalue_value能够实现变量弱类型的核心,定义如下:

[php]  view plain copy
  1. typedef union _zvalue_value {     long lval;                 /* long value */     double dval;               /* double value */     struct {                            char *val;         int len;               /* this will always be set for strings */     } str;                     /* string (always has length) */     HashTable *ht;             /* an array */     zend_object_value obj;     /* stores an object store handle, and handlers */  } zvalue_value;   

布尔型,zval.type=IS_BOOL,会读取zval.value.lval字段,值为1/0。如果是字符串,zval.type=IS_STRING,会读取zval.value.str,这是一个结构体,存储了字符串指针和长度。

C语言中,用"\0"作为字符串结束符。也就是说一个字符串"Hello\0World"在C语言中,用printf来输出的话,只能输出hello,因为"\0"会认为字符已经结束。PHP中是通过结构体的_zval_value.str.len来控制字符串长度,相关函数不会遇到"\0"结束。所以PHP的字符串是二进制安全的。

如果是NULL,只需要zval.type=IS_NULL,不需要读取值。

通过对zval的封装,PHP实现了弱类型,对于ZE来说,通过zval可以存取任何类型。

3. 高级类型Array和Object数组Array

数组是PHP语言中非常强大的一个数据结构,分为索引数组和关联数组,zval.type=IS_ARRAY。在关联数组中每个key可以存储任意类型的数据。PHP的数组是用Hash Table实现的,数组的值存在zval.value.ht中。 

后面会专门讲到PHP哈希表的实现。

对象类型的zval.type=IS_OBJECT,值存在zval.value.obj中。

4. 特殊类型——资源类型(Resource)介绍

资源类型是个很特殊的类型,zval.type=IS_RESOURCE,在PHP中有一些很难用常规类型描述的数据结构,比如文件句柄,对于C语言来说是一个指针,不过PHP中没有指针的概念,也不能用常规类型来约束,因此PHP通过资源类型概念,把C语言中类似文件指针的变量,用zval结构来封装。资源类型值是一个整数,ZE会根据这个值去资源的哈希表中获取。  

资源类型的定义:

[php]  view plain copy
  1. typedefstruct_zend_rsrc_list_entry {     void *ptr;     int type;     int refcount;  }zend_rsrc_list_entry;   

其中,ptr是一个指向资源的最终实现的指针,例如一个文件句柄,或者一个数据库连接结构。type是一个类型标记,用于区分不同的资源类型。refcount用于资源的引用计数。

内核中,资源类型是通过函数ZEND_FETCH_RESOURCE获取的。

[php]  view plain copy
  1. ZEND_FETCH_RESOURCE(con, type, zval *, default, resource_name, resource_type);   

5. 变量类型的转换

按照现在我们对PHP语言的了解,变量的类型依赖于zval.type字段指示,变量的内容按照zval.type存储到zval.value。当PHP中需要变量的时候,只需要两个步骤:把zval.value的值或指针改变,再改变zval.type的类型。不过对于PHP的一些高级变量Array/Object/Resource,变量转换要进行更多操作。

变量转换原理分为3种:

5.1 标准类型相互转换

比较简单,按照上述的步骤转化即可。

5.2 标准类型与资源类型转换

资源类型可以理解为是int,比较方便转换标准类型。转换后资源会被close或回收。

[php]  view plain copy
  1. <? php $var = fopen('/tmp/aaa.txt''a'); // 资源 #1 $var = (int) $var; var_dump($var);  // 输出1 ?>  

5.3 标准类型与复杂类型转换

Array转换整型int/浮点型float会返回元素个数;转换bool返回Array中是否有元素;转换成string返回'Array',并抛出warning。 
详细内容取决于经验,请阅读PHP手册: http://php.net/manual/en/language.types.type-juggling.php 

5.4 复杂类型相互转换

array和object可以互转。如果其它任何类型的值被转换成对象,将会创建一个内置类stdClass的实例。

在我们写PHP扩展的时候,PHP内核提供了一组函数用于类型转换:  

void convert_to_long(zval* pzval)
void convert_to_double(zval* pzval)
void convert_to_long_base(zval* pzval, int base)
void convert_to_null(zval* pzval)
void convert_to_boolean(zval* pzval)
void convert_to_array(zval* pzval)
void convert_to_object(zval* pzval)
void convert_object_to_type(zval* pzval, convert_func_t converter)

PHP内核提供的一组宏来方便的访问zval,用于更细粒度的获取zval的值:

内核访问zval容器的API
访问变量
Z_LVAL(zval) (zval).value.lval
Z_DVAL(zval) (zval).value.dval
Z_STRVAL(zval) (zval).value.str.val
Z_STRLEN(zval) (zval).value.str.len
Z_ARRVAL(zval) (zval). value.ht
Z_TYPE(zval) (zval).type
Z_LVAL_P(zval) (*zval).value.lval
Z_DVAL_P(zval) (*zval).value.dval
Z_STRVAL_P(zval_p) (*zval).value.str.val
Z_STRLEN_P(zval_p) (*zval).value.str.len
Z_ARRVAL_P(zval_p) (*zval). value.ht
Z_OBJ_HT_P(zval_p) (*zval).value.obj.handlers
Z_LVAL_PP(zval_pp) (**zval).value.lval
Z_DVAL_PP(zval_pp) (**zval).value.dval
Z_STRVAL_PP(zval_pp) (**zval).value.str.val
Z_STRLEN_PP(zval_pp) (**zval).value.str.len
Z_ARRVAL_PP(zval_pp) (**zval). value.ht

6. 变量的符号表与作用域

PHP的变量符号表与zval值的映射,是通过HashTable(哈希表,又叫做散列表,下面简称HT),HashTable在ZE中广泛使用,包括常量、变量、函数等语言特性都是HT来组织,在PHP的数组类型也是通过HashTable来实现。 
举个例子:

[php]  view plain copy
  1. <? php $var = 'Hello World'; ?>   

$var的变量名会存储在变量符号表中,代表$var的类型和值的zval结构存储在哈希表中。内核通过变量符号表与zval地址的哈希映射,来实现PHP变量的存取。

为什么要提作用域呢?因为函数内部变量保护。按照作用域PHP的变量分为全局变量和局部变量,每种作用域PHP都会维护一个符号表的HashTable。当在PHP中创建一个函数或类的时候,ZE会创建一个新的符号表,表明函数或类中的变量是局部变量,这样就实现了局部变量的保护--外部无法访问函数内部的变量。当创建一个PHP变量的时候,ZE会分配一个zval,并设置相应type和初始值,把这个变量加入当前作用域的符号表,这样用户才能使用这个变量。 
内核中使用ZEND_SET_SYMBOL来设置变量:

[php]  view plain copy
  1. ZEND_SET_SYMBOL( EG(active_symbol_table), "foo", foo);  

查看_zend_executor_globals结构

[php]  view plain copy
  1. Zend/zend_globals.h  
  2.  struct _zend_executor_globals {          //略        HashTable symbol_table;//全局变量的符号表        HashTable *active_symbol_table;//局部变量的符号表        //略  };   

在写PHP扩展时候,可以通过EG宏来访问PHP的变量符号表。EG(symbol_table)访问全局作用域的变量符号表,EG(active_symbol_table)访问当前作用域的变量符号表,局部变量存储的是指针,在对HashTable进行操作的时候传递给相应函数。

为了更好的理解变量的哈希表与作用域,举个简单的例子:

[php]  view plain copy
  1. <? php $temp = 'global'function test() {     $temp = 'active'; } test(); var_dump($temp); ?>   

创建函数外的变量$temp,会把这个它加入全局符号表,同时在全局符号表的HashTable中,分配一个字符类型的zval,值为‘global‘。创建函数test内部变量$temp,会把它加入属于函数test的符号表,分配字符型zval,值为’active' 。

7. PHP扩展中变量操作

创建PHP变量

我们可以在扩展中调用函数MAKE_STD_ZVAL(pzv)来创建一个PHP可调用的变量,MAKE_STD_ZVAL应用到的宏有:

[php]  view plain copy
  1. #define     MAKE_STD_ZVAL(zv)               ALLOC_ZVAL(zv);INIT_PZVAL(zv)   #define     ALLOC_ZVAL(z)                   ZEND_FAST_ALLOC(z, zval, ZVAL_CACHE_LIST)   #define     ZEND_FAST_ALLOC(p, type, fc_type)       (p) = (type *) emalloc(sizeof(type))   #define     INIT_PZVAL(z)                       (z)->refcount__gc = 1;(z)->is_ref__gc = 0;   

MAKE_STD_ZVAL(foo)展开后得到:

[php]  view plain copy
  1. (foo) = (zval *) emalloc(sizeof(zval));   (foo)->refcount__gc = 1;   (foo)->is_ref__gc = 0;   

可以看出,MAKE_STD_ZVAL做了三件事:分配内存、初始化zval结构中的refcount、is_ref。 

内核中提供一些宏来简化我们的操作,可以只用一步便设置好zval的类型和值。

API Macros for Accessing zval 
实现方法
ZVAL_NULL(pvz) Z_TYPE_P(pzv) = IS_NULL
ZVAL_BOOL(pvz) Z_TYPE_P(pzv) = IS_BOOL; 
Z_BVAL_P(pzv) = b ? 1 : 0;
ZVAL_TRUE(pvz) ZVAL_BOOL(pzv, 1);
ZVAL_FALSE(pvz) ZVAL_BOOL(pzv, 0);
ZVAL_LONG(pvz, l)(l 是值) Z_TYPE_P(pzv) = IS_LONG;Z_LVAL_P(pzv) = l;
ZVAL_DOUBLE(pvz, d) Z_TYPE_P(pzv) = IS_DOUBLE;Z_LVAL_P(pzv) = d;
ZVAL_STRINGL(pvz, str, len, dup) Z_TYPE_P(pzv) = IS_STRING;Z_STRLEN_P(pzv) = len; 
if (dup) { 
    {Z_STRVAL_P(pzv) =estrndup(str, len + 1);}  
}else { 
    {Z_STRVAL_P(pzv) = str;} 
}
ZVAL_STRING(pvz, str, len) ZVAL_STRINGL(pzv, str,strlen(str), dup);
ZVAL_RESOURCE(pvz, res) Z_TYPE_P(pzv) = IS_RESOURCE;Z_RESVAL_P(pzv) = res; 


ZVAL_STRINGL(pzv,str,len,dup)中的dup参数

先阐述一下ZVAL_STRINGL(pzv,str,len,dup); str和len两个参数很好理解,因为我们知道内核中保存了字符串的地址和它的长度,后面的dup的意思其实很简单,它指明了该字符串是否需要被复制。值为 1 将先申请一块新内存并赋值该字符串,然后把新内存的地址复制给pzv,为 0 时则是直接把str的地址赋值给zval。

ZVAL_STRINGL与ZVAL_STRING的区别

如果你想在某一位置截取该字符串或已经知道了这个字符串的长度,那么可以使用宏 ZVAL_STRINGL(zval, string, length, duplicate) ,它显式的指定字符串长度,而不是使用strlen()。这个宏该字符串长度作为参数。但它是二进制安全的,而且速度也比ZVAL_STRING快,因为少了个strlen。 

ZVAL_RESOURCE约等于ZVAL_LONG

在章节4中我们说过,PHP中的资源类型的值是一个整数,所以ZVAL_RESOURCE和ZVAL_LONG的工作差不多,只不过它会把zval的类型设置为 IS_RESOURCE。

8. 总结

PHP的弱类型是通过ZE的zval容器转换完成,通过哈希表来存储变量名和zval数据,在运行效率方面有一定牺牲。另外因为变量类型的隐性转换,在开发过程中对变量类型检测力度不够,可能会导致问题出现。 

不过PHP的弱类型、数组、内存托管、扩展等语言特性,非常适合Web开发场景,开发效率很高,能够加快产品迭代周期。在海量服务中,通常瓶颈存在于数据访问层,而不是语言本身。在实际使用PHP不仅担任逻辑层和展现层的任务,我们甚至用PHP开发的UDPServer/TCPServer作为数据和cache的中间层。

关于作者:王帅,腾讯企业QQ SaaS团队Leader。


深入PHP内核(二)——SAPI探究



摘要:PHP作为一门简单而强大的语言,能够提供很多Web适用的语言特性。从实践出发,继弱类型变量原理探究后,王帅将继续带大家弄清PHP内核中的一些常用部分,本期则是SAPI的深入理解。

SAPI是Server Application Programming Interface(服务器应用编程接口)的缩写。PHP通过SAPI提供了一组接口,供应用和PHP内核之间进行数据交互。

简单的讲,就像函数的输入和输出一样,我们通过Linux命令行执行一段PHP代码,本质是Linux的Shell通过PHP的SAPI传入一组参数,Zend引擎执行后,返回给shell,由shell显示出来的过程。同样的,通过Apache调用PHP,通过Web服务器给SAPI传入数据,Zend引擎执行后,返回给Apache,由Apache显示在页面上。

 

图1. PHP架构图 

PHP提供很多种形式的接口,包括apache、apache2filter、apache2handler、caudium、cgi 、cgi-fcgi、cli、cli-server、continuity、embed、isapi、litespeed、milter、nsapi、phttpd pi3web、roxen、thttpd、tux和webjames。但是常用的只有5种形式,CLI/CGI(命令行)、Multiprocess(多进程)、Multithreaded(多线程)、FastCGI和Embedded(内嵌)。

PHP提供了一个函数查看当前SAPI接口类型:

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. string php_sapi_name ( void )  

PHP的运行和加载

无论使用哪种SAPI,在PHP执行脚本前后,都包含一系列事件:Module的Init(MINT)和Shutdown(MSHUTDOWN),Request 的Init(RINT)和Shutdown(RSHUTDOWN)。 第一阶段是PHP模块初始化阶段(MINT),可以初始化扩展内部变量、分配资源和注册资源处理器,在整个PHP实例生命周期内,该过程只执行一次。

什么是PHP模块?通过上面的PHP架构图,在PHP中可以使用get_loaded_extensions 函数来查看所有编译并加载的模块/扩展,相当于CLI模式下的php -m。

以PHP的Memcached扩展源代码为例:

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. PHP_MINIT_FUNCTION(memcached) {       
  2.     zend_class_entry ce;      
  3.     memcpy(&memcached_object_handlers,zend_get_std_object_handlers(), sizeof(zend_object_handlers));      
  4. memcached_object_handlers.clone_obj = NULL;     /* 执行了一些类似的初始化操作 */       
  5. return SUCCESS;   
  6. }  
第二阶段是请求初始化阶段(RINT),在模块初始化并激活后,会创建PHP运行环境,同时调用所有模块注册的RINT函数,调用每个扩展的请求初始化函数 ,设定特定的环境变量、分配资源或执行其他任务,如审核。

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. PHP_RINIT_FUNCTION(memcached) {       
  2.     /* 执行一些关于请求的初始化 */        
  3.     return SUCCESS;   
  4. }  

第三阶段,请求处理完成后,会调用PHP_RSHUTDOWN_FUNCTION进行回收,这是每个扩展的请求关闭函数,执行最后的清理工作。Zend引擎执行清理过程、垃圾收集、对之前的请求期间用到的每个变量执行unset。请求完成可能是执行到脚本完成,也可能是调用die()或exit()函数完成

第四阶段,当PHP生命周期结束时候,PHP_MSHUTDOWN_FUNCTION对模块进行回收处理,这是每个扩展的模块关闭函数,用于关闭自己的内核子系统。

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. PHP_MSHUTDOWN_FUNCTION(memcached) { /* 执行关于模块的销毁工作 */ UNREGISTER_INI_ENTRIES(); return SUCCESS; }  

常见的运行模式

常见的SAPI模式有五种:

  • CLI和CGI模式(单进程模式)
  • 多进程模式
  • 多线程模式
  • FastCGI模式
  • 嵌入式

1. CLI/CGI模式

CLI和CGI都属于单进程模式,PHP的生命周期在一次请求中完成。也就是说每次执行PHP脚本,都会执行第二部分讲的四个INT和Shutdown事件。

图2. CGI/CLI生命周期 

2. 多进程模式(Multiprocess)

多进程模式可以将PHP内置到Web Server中,PHP可以编译成Apache下的prefork MPM模式和APXS模块,当Apache启动后,会fork很多子进程,每个子进程拥有自己独立的进程地址空间。

 

图3. 多进程模式生命周期 

在一个子进程中,PHP的生命周期是调用MINT启动后,执行多次请求(RINT/RSHUTDOWN),在Apache关闭或进程结束后,才会调用MSHUTDOWN进行回收阶段。 

 

图4. 多进程的生命周期 

多进程模型中,每个子进程都是独立运行,没有代码和数据共享,因此一个子进程终止退出和重新生成,不会影响其他子进程的稳定。

3. 多线程模式(Multithreaded)

Apache2的Worker MPM采用了多线程模型,在一个进程下创建多个线程,在同一个进程地址空间执行。


图5. 多线程生命周期

4. FastCGI模式

在我们用的Nginx+PHP-FPM用的就是FastCGI模式,Fastcgi是一种特殊的CGI模式,是一种常驻进程类型的CGI,运行后可以Fork多个进程,不用花费时间动态的Fork子进程,也不需要每次请求都调用MINT/MSHUTDOWN。PHP通过PHP-FPM来管理和调度FastCGI的进程池。Nginx和PHP-FPM通过本地的TCP Socket和Unix Socket 进行通信。

 

图6. FastCGI模式生命周期

PHP-FPM进程管理器自身初始化,启动多个CGI解释器进程等待来自Nginx的请求。当客户端请求达到PHP-FPM,管理器选择到一个CGI进程进行处理,Nginx将CGI环境变量和标准输入发送到一个PHP-CIG子进程。PHP-CGI子进程处理完成后,将标准输出和错误信息返回给Nginx,当PHP-CGI子进程关闭连接时,请求处理完成。PHP-CGI子进程等待着下一个连接。

可以想象CGI的系统开销有多大。每一个Web 请求PHP都必须重新解析php.ini、载入全部扩展并始化全部数据结构。使用FastCGI,所有这些都只在进程启动时发生一次。另外,对于数据库和Memcache的持续连接可以工作。

5. 内嵌模式(Embedded)

Embed SAPI是一种特殊的SAPI,允许在C/C++语言中调用PHP提供的函数。这种SAPI和CLI模式一样,按照Module Init => Request Init => Request => Request Shutdown => Module Shutdown的模式运行。

Embed SAPI可以调用PHP丰富的类库,也可以实现高级玩法,比如可以查看PHP的OPCODE(PHP执行的中间码,Zend引擎的指令,由PHP代码生成)。

详细请见: http://www.laruence.com/2008/09/23/539.html

SAPI的运行机制

我们以CGI为例,看一下SAPI的运行机制。

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. static sapi_module_struct cgi_sapi_module = {       
  2.     "cgi-fcgi",                     /* 输出给php_info()使用 */     "CGI/FastCGI",                  /* pretty name */        
  3.     php_cgi_startup,                /* startup 当SAPI初始化时,首先会调用该函数 */       
  4.     php_module_shutdown_wrapper,    /* shutdown  关闭函数包装器,它用来释放所有的SAPI的数据结构、内存等,调用php_module_shutdown */        
  5.     sapi_cgi_activate,              /* activate  此函数会在每个请求开始时调用,它会做初始化,资源分配 */       
  6.     sapi_cgi_deactivate,            /* deactivate  此函数会在每个请求结束时调用,它用来确保所有的数据都得到释放 */        
  7.     sapi_cgi_ub_write,              /* unbuffered write  不缓存的写操作(unbuffered write),它是用来向SAPI外部输出数据 */       
  8.     sapi_cgi_flush,                 /* flush  刷新输出,在CLI模式下通过使用C语言的库函数fflush实现*/     NULL,                           /* get uid */       
  9.     sapi_cgi_getenv,                /* getenv 根据name查找环境变量 */        
  10.     php_error,                      /* error handler 注册错误处理函数  */        
  11.     NULL,                           /* header handler PHP调用header()时候被调用 */       
  12.     sapi_cgi_send_headers,          /* send headers handler 发送头部信息*/       
  13.     NULL,                           /* send header handler 发送一个单独的头部信息 */        
  14.     sapi_cgi_read_post,             /* read POST data  当请求的方法是POST时,程序获取POST数据,写入$_POST数组 */       
  15.     sapi_cgi_read_cookies,          /* read Cookies 获取Cookie值  */        
  16.     sapi_cgi_register_variables,    /* register server variables 给$_SERVER添加环境变量 */       
  17.     sapi_cgi_log_message,           /* Log message 输出错误信息 */       
  18.     NULL,                           /* Get request time */       
  19.     NULL,                           /* Child terminate */        
  20.     STANDARD_SAPI_MODULE_PROPERTIES   
  21. };   

由上面代码可见,PHP的SAPI像是面向对象中基类,SAPI.h和SAPI.c包含的函数是抽象基类的声明和定义,各个服务器用的SAPI模式,则是继承了这个基类,并重新定义基类方法的子类。

总结

PHP的SAPI是Zend引擎提供的一组标准交互接口,通过注册初始化、析构、输入、输出等接口,我们可以将应用程序运行在Zend引擎上,也可以把PHP嵌入到类似Apache的Web Server中。PHP常见的SAPI模式有五种,CGI/CLI模式、多进程模式、多线程模式、FastCGI模式和内嵌模式。

了解PHP的SAPI机制意义重大,帮助我们理解PHP的生命周期,并了解如何更好的通过C/C++为PHP编写扩展,并在生命周期中找到提高系统性能的方式。




深入PHP内核(三)——内核利器哈希表与哈希碰撞攻击



摘要:PHP作为一门简单而强大的语言,能够提供很多Web适用的语言特性。从实践出发,继弱类型变量原理探究后,王帅将继续带大家弄清PHP内核中的一些常用部分,本期则是内核利器哈希表与哈希碰撞攻击。

【导读】王帅在海量分布式Web系统有超过8年沉淀,主导过多个大型系统的架构设计,目前在腾讯企业SaaS团队。

PHP内核系列文章,是作者在PHP领域实践中,把相关原理性的知识,通过更便于理解的方式,系统整理出来分享给读者。希望通过PHP原理性的轻量解读,对这门Web领域最热门技术的优秀架构分析解构,让更多的人不断的深入了解语言的原理本身,更容易定位、理解一些问题背后原因,更游刃有余的做基础架构设计。同时希望影响更多的人才投入Web开源领域,不仅是应用和学习一门技术、组件,同样能够贡献更多高质量组件,像战国时期的百家争鸣一样,PHP开源界花开遍地。

作者一直倡导技术的深入学习就像职业篮球训练,80%的时间都是基本功的训练,球场上实际战术的练习只是基本功的应用。同样的,学习PHP语言本身的特性,应当是每个PHP领域工程师所掌握、理解的,至于系统的架构设计也是基于对Linux、Mysql、Nginx等原理机制足够理解后,战术性的使用。

深入PHP内核(三)——内核利器哈希表与哈希碰撞攻击

在PHP的Zend Engine(下面简称ZE)中,有一个非常重要的数据结构——哈希表(HashTable)。哈希表在ZE中有非常广泛的应用,PHP的复杂数据结构中数组和类的存储和访问就是用哈希表来组织,PHP语言结构中的常量、变量、函数等符号表也是用它来组织。

1. 哈希表的基本概念

什么是哈希表呢?哈希表在数据结构中也叫散列表。是根据键名经过hash函数计算后,映射到表中的一个位置,来直接访问记录,加快了访问速度。在理想情况下,哈希表的操作时间复杂度为O(1)。数据项可以在一个与哈希表长度无关的时间内,计算出一个值hash(key),在固定时间内定位到一个桶(bucket,表示哈希表的一个位置),主要时间消耗在于哈希函数计算和桶的定位。

在分析PHP中HashTable实现原理之前,先介绍一下相关的基本概念:

如下图例子,希望通过人名检索一个数据,键名通过哈希函数,得到指向bucket的指针,最后访问真实的bucket。

 

键名(Key):在哈希函数转换前,数据的标识。

桶(Bucket):在哈希表中,真正保存数据的容器。

哈希函数(Hash Function):将Key通过哈希函数,得到一个指向bucket的指针。MD5,SHA-1是我们在业务中常用的哈希函数。

哈希冲突(Hash Collision):两个不同的Key,经过哈希函数,得到同一个bucket的指针。

2. PHP的哈希表实现原理

哈希表的结构:

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. Zend/zend_hash.h  
  2.  typedef struct _hashtable {  
  3.         uint nTableSize;                    //哈希表的长度,不是元素个数  
  4.         uint nTableMask;                  //哈希表的掩码,设置为nTableSize-1  
  5.         uint nNumOfElements;          //哈希表实际元素个数  
  6.         ulong nNextFreeElement;      //指向下一个空元素位置  
  7.         Bucket *pInternalPointer;       //用于遍历哈希表的内部指针  
  8.         Bucket *pListHead;               //哈希表队列的头部  
  9.         Bucket *pListTail;                 //哈希表队列的尾部  
  10.         Bucket **arBuckets;               //哈希表存储的元素数组  
  11.         dtor_func_t pDestructor;          //哈希表的元素析构函数指针  
  12.         zend_bool persistent;              //是否是持久保存,用于pmalloc的参数,可以持久存储在内存中  
  13.         unsigned char nApplyCount;     // zend_hash_apply的次数,用来限制嵌套遍历的层数,限制为3层  
  14.         zend_bool bApplyProtection;     //是否开启嵌套遍历保护  
  15. #if ZEND_DEBUG  
  16.         int inconsistent;  
  17. #endif  
  18. } HashTable;  
1)  nTableSize 哈希表的大小。最小容量是2^3(8),最大容量是2^31(2147483648)。当如果进行一次操作后发现元素个数大于nTableSize,会申请当前nTableSize * 2的空间。假设当前nTableSize为8,当插入元素达到9个的时候,会申请nTableSize=16的空间。

2)  nTableMask 为nTableSize-1,用于调整最大索引值。当哈希后值大于索引值时候,把这个值映射到索引值范围内。

3)  nNumOfElements HashTable中的个数。数组操作中,sizeof和count函数获取的是这个值。

4)  nNextFreeElement 下一个空元素的地址。

5)  pInternalPointer 存储了HashTable当前指向的元素的指针,当我们使用一些内部循环函数的时候会用到这个指针比如reset(), current(), prev(), next(), foreach(), end()。相当于游标。

6)  pListHead和pListTail则具体指向了该哈希表的第一个和最后一个元素,对应就是数组的起始和结束元素。哈希表的pListHead、pListTail与Bucket的pListNext、pListLast维护了一个哈希表中Bucket的双向链表,按照插入的先后顺序,用于哈希表的遍历。

7)  arBuckets 实际存储Buckets的数组。

8)  pDestructor 是一个析构函数,当某个值被从哈希表删除的时候会触发此函数。他还有一个主要作用是用于变量的GC回收。在PHP里面GC是通过引用计数实现的,当一个变量的引用计数变为0,就会被PHP的GC回收。

9)  persistent 定义了hashtable是否能在多次request中获得持久存在。

10)  nApplyCount 和 bApplyProtection 是用来防止嵌套遍历的。

11)  inconsistent 是在调试模式下捕获对HT不正确的使用。

Bucket的结构:

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1.  typedef struct bucket {  
  2.         ulong h;                               //数组索引的哈希值  
  3.         uint nKeyLength;                  //索引数组为0,关联数组为key的长度  
  4.         void *pData;                         //元素内容的指针  
  5.         void *pDataPtr;                    // 如果是指针大小的数据,用pDataPtr直接存储,pData指向pDataPtr  
  6.         struct bucket *pListNext;     //哈希链表中下一个元素  
  7.         struct bucket *pListLast;     //哈希链表中上一个元素  
  8.         struct bucket *pNext;          //解决哈希冲突,变为双向链表,双向链表的下一个元素  
  9.         struct bucket *pLast;          //解决哈希冲突,变为双向链表,双向链表的上一个元素  
  10.         const char *arKey;             //最后一个元素key的名称  
  11. } Bucket;  

通过下图来表示HashTable的原理:

 

我们先来看一下,ZE是如何创建一个hash表的。创建并初始化一个Hash比较容易,调用_zend_hash_init函数。PHP的哈希表最小容量8(2^3),最大容量是0x80000000(2^31,即2147483648)。nTableSize会按照2的整数次幂圆整来增加,直到超过预设值的nSize。

Zend/zend_hash.c

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. ZEND_API int _zend_hash_init(HashTable *ht, uint nSize, hash_func_t pHashFunction, dtor_func_t pDestructor, zend_bool persistent ZEND_FILE_LINE_DC)  
  2. {  
  3.         uint i = 3;  
  4.   
  5.         SET_INCONSISTENT(HT_OK);  
  6.   
  7.         if (nSize >= 0x80000000) {  
  8.                 /* prevent overflow */  
  9.                 ht->nTableSize = 0x80000000;  
  10.         } else {  
  11.                 while ((1U << i) < nSize) {  
  12.                         i++;  
  13.                 }  
  14.                 ht->nTableSize = 1 << i;  
  15.         }  
  16.   
  17.         /* 省略哈希表初始化步骤 */  
  18.   
  19.         return SUCCESS;  
  20. }  

1)  *ht 是哈希表的指针,这里既可以传入一个已存在的HashTable, 也可以通过内核宏ALLOC_HASHTABLE(ht)来自动申请一块HashTable内存。ALLOC_HASHTABLE(ht)相当于ht=emalloc(sizeof(HashTable))

2)  nSize 哈希表能拥有的最大数量。通过预先申请好内存的方式,减少哈希表rehash操作。

3)  pHashFunction 自定义哈希函数的钩子

4)  pDesctructor 哈希表析构的回调函数,当删除一个哈希表的时候,会调用。

5)  persistent 对应HashTable.persistent,当设置为true的时候,不会在RSHUTDOWN阶段自动销毁。

我们通过更新哈希表的操作方式,来分析哈希表的操作机制: 

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. h = zend_inline_hash_func(arKey, nKeyLength);  
  2. nIndex = h & ht->nTableMask;  
  3.   
  4. p = ht->arBuckets[nIndex];  
  5. while (p != NULL) {  
  6.     if (p->arKey == arKey ||  
  7.     ((p->h == h) && (p->nKeyLength == nKeyLength) && !memcmp(p->arKey, arKey,         nKeyLength))) {  
  8.     if (flag & HASH_ADD) {  
  9.         return FAILURE;  
  10.     }  
  11.   
  12.     /* 省略 */  
  13.   
  14.     UPDATE_DATA(ht, p, pData, nDataSize);   // 找到h 和 Key都相等的Buckets,说明需要更新  
  15.     /* 省略 */  
  16.     }  
  17.     p = p->pNext;   // 这里说明有哈希冲突,按照Buckets[nIndex]的链表找下去  
  18. }  
  19.   
  20. /* 省略 */  
  21. p->nKeyLength = nKeyLength;  
  22. INIT_DATA(ht, p, pData, nDataSize);    // 把Bucket.pData数据更新  
  23. p->h = h;  
  24. CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex]);    // 挂到  
  25. if (pDest) {  
  26.     *pDest = p->pData;  
  27. }  
  28.   
  29. HANDLE_BLOCK_INTERRUPTIONS();  
  30. CONNECT_TO_GLOBAL_DLLIST(p, ht);  
  31. ht->arBuckets[nIndex] = p;      
  32. HANDLE_UNBLOCK_INTERRUPTIONS();  
  33.   
  34. ht->nNumOfElements++;  
  35. ZEND_HASH_IF_FULL_DO_RESIZE(ht); /* 如果哈希表满了,重新散列,这里有一定开销   */  
1) 通过哈希算法  times33(Key) & (nTableSize-1) ,生成Key对应的哈希值A,获取arBuckets[A]的值

2) 判断arBuckets[A]是否存在,如果存在而且没有哈希冲突,进行数据update(UPDATE_DATA)。如果存在但是Key不相同说明有哈希冲突,在arBuckets[A]链表中寻找Key是否存在,如果存在,执行update操作(UPDATE_DATA)

3) 如果arBuckets[A]不存在,创建新的arBucket[A](INIT_DATA)。或哈希冲突情况下,在arBuckets[A]的链表中找不到Key。创建新的bucket(INIT_DATA),并把新的buckets放在arBucket[A]链表头

4) 维护哈希表的逻辑链表(CONNECT_TO_GLOBAL_DLLIST)。

5) 如果发现新插入元素已经超过HashTable的nTableSize,自动扩容至2倍nTableSize,重新哈希后维护新的HashTable。

3. PHP使用的哈希函数

PHP的哈希表是用Times33哈希算法,又称为DJBX33A。这是一个使用比较广泛的对字符串的哈希算法,计算速度快,散列均匀,Perl和Apache都使用了这个算法。算法原理就是不断的乘以33,其算法原型如下:

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. hash(i) = hash(i-1) * 33 + str[i]  
为什么是33呢?对于33这个数,DJB注释中是说,1到256之间的所有奇数,都能达到一个可接受的哈希分布,平均分布大概是86%。而其中33,17,31,63,127,129这几个数在面对大量的哈希运算时有一个更大的优势,就是这些数字能将乘法用位运算配合加减法替换,这样运算速度会更高。gcc编译器开启优化后会自动将乘法转换为位运算。PHP实际算法如下:

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1.  static inline ulong zend_inline_hash_func(const char *arKey, uint nKeyLength)  
  2. {  
  3.     register ulong hash = 5381;  
  4.   
  5.     /* variant with the hash unrolled eight times */  
  6.     for (; nKeyLength >= 8; nKeyLength -= 8) {  
  7.         hash = ((hash << 5) + hash) + *arKey++;  
  8.         hash = ((hash << 5) + hash) + *arKey++;  
  9.         hash = ((hash << 5) + hash) + *arKey++;  
  10.         hash = ((hash << 5) + hash) + *arKey++;  
  11.         hash = ((hash << 5) + hash) + *arKey++;  
  12.         hash = ((hash << 5) + hash) + *arKey++;  
  13.         hash = ((hash << 5) + hash) + *arKey++;  
  14.         hash = ((hash << 5) + hash) + *arKey++;  
  15.     }  
  16.     switch (nKeyLength) {  
  17.         case 7: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */  



深入PHP内核


关于作者:王帅,腾讯企业QQ SaaS团队Leader。转载https://blog.csdn.net/heiyeshuwu/article/details/42262951


深入PHP内核(一)——弱类型变量原理探究


摘要:PHP作为一门简单而强大的语言,能够提供很多Web适用的语言特性,而从本期《问底》开始,王帅将从实践出发,带你弄清PHP内核中一些常用的部分,比如这里的“弱类型变量原理”。

PHP是一门简单而强大的语言,提供了很多Web适用的语言特性,其中就包括了变量弱类型,在弱类型机制下,你能够给一个变量赋任意类型的值。 

PHP的执行是通过Zend Engine(下面简称ZE),ZE是使用C编写,在底层实现了一套弱类型机制。ZE的内存管理使用写时拷贝、引用计数等优化策略,减少再变量赋值时候的内存拷贝。

下面不光带你探索PHP弱类型的原理,也会在写PHP扩展角度,介绍如何操作PHP的变量。 

1. PHP的变量类型

PHP的变量类型有8种:

  • 标准类型:布尔boolen,整型integer,浮点float,字符string
  • 复杂类型:数组array,对象object
  • 特殊类型:资源resource  

PHP不会严格检验变量类型,变量可以不显示的声明其类型,而在运行期间直接赋值。也可以将变量自由的转换类型。如下例,没有实现声明的情况下,$i可以赋任意类型的值。

[php]  view plain copy
  1. <? php  $i = 1;   //int $i = 'show me the money';  //string $i = 0.02;  // float $i = array(1, 2, 3);  // array $i = new Exception('test', 123); // object $i = fopen('/tmp/aaa.txt', 'a') // resource ?>   

如果你对弱类型原理理解不深刻,在变量比较时候,会出现“超出预期”的惊喜。

[php]  view plain copy
  1. <? PHP $str1 = null;  $str2 = false;  echo $str1==$str2 ? '相等' : '不相等';  $str3 = '';  $str4 = 0;  echo $str3==$str4 ? '相等' : '不相等';  $str5 = 0;  $str6 = '0';  echo $str5==$str6 ? '相等' : '不相等';  ?>   

以上三个结果全部是相等,因为在变量比较的时候,PHP内部做了变量转换。如果希望值和类型同时判断,请使用三个=(如,$a===0)来判断。也许你会觉得司空见惯,也许你会觉得很神奇,那么请跟我一起深入PHP内核,探索PHP变量原理。

2. 变量的存储及标准类型介绍

PHP的所有变量,都是以结构体zval来实现,在Zend/zend.h中我们能看到zval的定义:

[php]  view plain copy
  1. typedef union _zvalue_value {     long lval;                 /* long value */     double dval;               /* double value */     struct {                            char *val;         int len;               /* this will always be set for strings */     } str;                     /* string (always has length) */     HashTable *ht;             /* an array */     zend_object_value obj;     /* stores an object store handle, and handlers */  } zvalue_value;   

属性名 含义 默认值
refcount__gc 表示引用计数 1
is_ref__gc 表示是否为引用 0
value 存储变量的值  
type 变量具体的类型  

其中refcount__gc和is_ref__gc表示变量是否是一个引用。type字段标识变量的类型,type的值可以是:IS_NULL,IS_BOOL,IS_LONG,IS_FLOAT,IS_STRING,IS_ARRAY,IS_OBJECT,IS_RESOURCE。PHP根据type的类型,来选择如何存储到zvalue_value。 

zvalue_value能够实现变量弱类型的核心,定义如下:

[php]  view plain copy
  1. typedef union _zvalue_value {     long lval;                 /* long value */     double dval;               /* double value */     struct {                            char *val;         int len;               /* this will always be set for strings */     } str;                     /* string (always has length) */     HashTable *ht;             /* an array */     zend_object_value obj;     /* stores an object store handle, and handlers */  } zvalue_value;   

布尔型,zval.type=IS_BOOL,会读取zval.value.lval字段,值为1/0。如果是字符串,zval.type=IS_STRING,会读取zval.value.str,这是一个结构体,存储了字符串指针和长度。

C语言中,用"\0"作为字符串结束符。也就是说一个字符串"Hello\0World"在C语言中,用printf来输出的话,只能输出hello,因为"\0"会认为字符已经结束。PHP中是通过结构体的_zval_value.str.len来控制字符串长度,相关函数不会遇到"\0"结束。所以PHP的字符串是二进制安全的。

如果是NULL,只需要zval.type=IS_NULL,不需要读取值。

通过对zval的封装,PHP实现了弱类型,对于ZE来说,通过zval可以存取任何类型。

3. 高级类型Array和Object数组Array

数组是PHP语言中非常强大的一个数据结构,分为索引数组和关联数组,zval.type=IS_ARRAY。在关联数组中每个key可以存储任意类型的数据。PHP的数组是用Hash Table实现的,数组的值存在zval.value.ht中。 

后面会专门讲到PHP哈希表的实现。

对象类型的zval.type=IS_OBJECT,值存在zval.value.obj中。

4. 特殊类型——资源类型(Resource)介绍

资源类型是个很特殊的类型,zval.type=IS_RESOURCE,在PHP中有一些很难用常规类型描述的数据结构,比如文件句柄,对于C语言来说是一个指针,不过PHP中没有指针的概念,也不能用常规类型来约束,因此PHP通过资源类型概念,把C语言中类似文件指针的变量,用zval结构来封装。资源类型值是一个整数,ZE会根据这个值去资源的哈希表中获取。  

资源类型的定义:

[php]  view plain copy
  1. typedefstruct_zend_rsrc_list_entry {     void *ptr;     int type;     int refcount;  }zend_rsrc_list_entry;   

其中,ptr是一个指向资源的最终实现的指针,例如一个文件句柄,或者一个数据库连接结构。type是一个类型标记,用于区分不同的资源类型。refcount用于资源的引用计数。

内核中,资源类型是通过函数ZEND_FETCH_RESOURCE获取的。

[php]  view plain copy
  1. ZEND_FETCH_RESOURCE(con, type, zval *, default, resource_name, resource_type);   

5. 变量类型的转换

按照现在我们对PHP语言的了解,变量的类型依赖于zval.type字段指示,变量的内容按照zval.type存储到zval.value。当PHP中需要变量的时候,只需要两个步骤:把zval.value的值或指针改变,再改变zval.type的类型。不过对于PHP的一些高级变量Array/Object/Resource,变量转换要进行更多操作。

变量转换原理分为3种:

5.1 标准类型相互转换

比较简单,按照上述的步骤转化即可。

5.2 标准类型与资源类型转换

资源类型可以理解为是int,比较方便转换标准类型。转换后资源会被close或回收。

[php]  view plain copy
  1. <? php $var = fopen('/tmp/aaa.txt''a'); // 资源 #1 $var = (int) $var; var_dump($var);  // 输出1 ?>  

5.3 标准类型与复杂类型转换

Array转换整型int/浮点型float会返回元素个数;转换bool返回Array中是否有元素;转换成string返回'Array',并抛出warning。 
详细内容取决于经验,请阅读PHP手册: http://php.net/manual/en/language.types.type-juggling.php 

5.4 复杂类型相互转换

array和object可以互转。如果其它任何类型的值被转换成对象,将会创建一个内置类stdClass的实例。

在我们写PHP扩展的时候,PHP内核提供了一组函数用于类型转换:  

void convert_to_long(zval* pzval)
void convert_to_double(zval* pzval)
void convert_to_long_base(zval* pzval, int base)
void convert_to_null(zval* pzval)
void convert_to_boolean(zval* pzval)
void convert_to_array(zval* pzval)
void convert_to_object(zval* pzval)
void convert_object_to_type(zval* pzval, convert_func_t converter)

PHP内核提供的一组宏来方便的访问zval,用于更细粒度的获取zval的值:

内核访问zval容器的API
访问变量
Z_LVAL(zval) (zval).value.lval
Z_DVAL(zval) (zval).value.dval
Z_STRVAL(zval) (zval).value.str.val
Z_STRLEN(zval) (zval).value.str.len
Z_ARRVAL(zval) (zval). value.ht
Z_TYPE(zval) (zval).type
Z_LVAL_P(zval) (*zval).value.lval
Z_DVAL_P(zval) (*zval).value.dval
Z_STRVAL_P(zval_p) (*zval).value.str.val
Z_STRLEN_P(zval_p) (*zval).value.str.len
Z_ARRVAL_P(zval_p) (*zval). value.ht
Z_OBJ_HT_P(zval_p) (*zval).value.obj.handlers
Z_LVAL_PP(zval_pp) (**zval).value.lval
Z_DVAL_PP(zval_pp) (**zval).value.dval
Z_STRVAL_PP(zval_pp) (**zval).value.str.val
Z_STRLEN_PP(zval_pp) (**zval).value.str.len
Z_ARRVAL_PP(zval_pp) (**zval). value.ht

6. 变量的符号表与作用域

PHP的变量符号表与zval值的映射,是通过HashTable(哈希表,又叫做散列表,下面简称HT),HashTable在ZE中广泛使用,包括常量、变量、函数等语言特性都是HT来组织,在PHP的数组类型也是通过HashTable来实现。 
举个例子:

[php]  view plain copy
  1. <? php $var = 'Hello World'; ?>   

$var的变量名会存储在变量符号表中,代表$var的类型和值的zval结构存储在哈希表中。内核通过变量符号表与zval地址的哈希映射,来实现PHP变量的存取。

为什么要提作用域呢?因为函数内部变量保护。按照作用域PHP的变量分为全局变量和局部变量,每种作用域PHP都会维护一个符号表的HashTable。当在PHP中创建一个函数或类的时候,ZE会创建一个新的符号表,表明函数或类中的变量是局部变量,这样就实现了局部变量的保护--外部无法访问函数内部的变量。当创建一个PHP变量的时候,ZE会分配一个zval,并设置相应type和初始值,把这个变量加入当前作用域的符号表,这样用户才能使用这个变量。 
内核中使用ZEND_SET_SYMBOL来设置变量:

[php]  view plain copy
  1. ZEND_SET_SYMBOL( EG(active_symbol_table), "foo", foo);  

查看_zend_executor_globals结构

[php]  view plain copy
  1. Zend/zend_globals.h  
  2.  struct _zend_executor_globals {          //略        HashTable symbol_table;//全局变量的符号表        HashTable *active_symbol_table;//局部变量的符号表        //略  };   

在写PHP扩展时候,可以通过EG宏来访问PHP的变量符号表。EG(symbol_table)访问全局作用域的变量符号表,EG(active_symbol_table)访问当前作用域的变量符号表,局部变量存储的是指针,在对HashTable进行操作的时候传递给相应函数。

为了更好的理解变量的哈希表与作用域,举个简单的例子:

[php]  view plain copy
  1. <? php $temp = 'global'function test() {     $temp = 'active'; } test(); var_dump($temp); ?>   

创建函数外的变量$temp,会把这个它加入全局符号表,同时在全局符号表的HashTable中,分配一个字符类型的zval,值为‘global‘。创建函数test内部变量$temp,会把它加入属于函数test的符号表,分配字符型zval,值为’active' 。

7. PHP扩展中变量操作

创建PHP变量

我们可以在扩展中调用函数MAKE_STD_ZVAL(pzv)来创建一个PHP可调用的变量,MAKE_STD_ZVAL应用到的宏有:

[php]  view plain copy
  1. #define     MAKE_STD_ZVAL(zv)               ALLOC_ZVAL(zv);INIT_PZVAL(zv)   #define     ALLOC_ZVAL(z)                   ZEND_FAST_ALLOC(z, zval, ZVAL_CACHE_LIST)   #define     ZEND_FAST_ALLOC(p, type, fc_type)       (p) = (type *) emalloc(sizeof(type))   #define     INIT_PZVAL(z)                       (z)->refcount__gc = 1;(z)->is_ref__gc = 0;   

MAKE_STD_ZVAL(foo)展开后得到:

[php]  view plain copy
  1. (foo) = (zval *) emalloc(sizeof(zval));   (foo)->refcount__gc = 1;   (foo)->is_ref__gc = 0;   

可以看出,MAKE_STD_ZVAL做了三件事:分配内存、初始化zval结构中的refcount、is_ref。 

内核中提供一些宏来简化我们的操作,可以只用一步便设置好zval的类型和值。

API Macros for Accessing zval 
实现方法
ZVAL_NULL(pvz) Z_TYPE_P(pzv) = IS_NULL
ZVAL_BOOL(pvz) Z_TYPE_P(pzv) = IS_BOOL; 
Z_BVAL_P(pzv) = b ? 1 : 0;
ZVAL_TRUE(pvz) ZVAL_BOOL(pzv, 1);
ZVAL_FALSE(pvz) ZVAL_BOOL(pzv, 0);
ZVAL_LONG(pvz, l)(l 是值) Z_TYPE_P(pzv) = IS_LONG;Z_LVAL_P(pzv) = l;
ZVAL_DOUBLE(pvz, d) Z_TYPE_P(pzv) = IS_DOUBLE;Z_LVAL_P(pzv) = d;
ZVAL_STRINGL(pvz, str, len, dup) Z_TYPE_P(pzv) = IS_STRING;Z_STRLEN_P(pzv) = len; 
if (dup) { 
    {Z_STRVAL_P(pzv) =estrndup(str, len + 1);}  
}else { 
    {Z_STRVAL_P(pzv) = str;} 
}
ZVAL_STRING(pvz, str, len) ZVAL_STRINGL(pzv, str,strlen(str), dup);
ZVAL_RESOURCE(pvz, res) Z_TYPE_P(pzv) = IS_RESOURCE;Z_RESVAL_P(pzv) = res; 


ZVAL_STRINGL(pzv,str,len,dup)中的dup参数

先阐述一下ZVAL_STRINGL(pzv,str,len,dup); str和len两个参数很好理解,因为我们知道内核中保存了字符串的地址和它的长度,后面的dup的意思其实很简单,它指明了该字符串是否需要被复制。值为 1 将先申请一块新内存并赋值该字符串,然后把新内存的地址复制给pzv,为 0 时则是直接把str的地址赋值给zval。

ZVAL_STRINGL与ZVAL_STRING的区别

如果你想在某一位置截取该字符串或已经知道了这个字符串的长度,那么可以使用宏 ZVAL_STRINGL(zval, string, length, duplicate) ,它显式的指定字符串长度,而不是使用strlen()。这个宏该字符串长度作为参数。但它是二进制安全的,而且速度也比ZVAL_STRING快,因为少了个strlen。 

ZVAL_RESOURCE约等于ZVAL_LONG

在章节4中我们说过,PHP中的资源类型的值是一个整数,所以ZVAL_RESOURCE和ZVAL_LONG的工作差不多,只不过它会把zval的类型设置为 IS_RESOURCE。

8. 总结

PHP的弱类型是通过ZE的zval容器转换完成,通过哈希表来存储变量名和zval数据,在运行效率方面有一定牺牲。另外因为变量类型的隐性转换,在开发过程中对变量类型检测力度不够,可能会导致问题出现。 

不过PHP的弱类型、数组、内存托管、扩展等语言特性,非常适合Web开发场景,开发效率很高,能够加快产品迭代周期。在海量服务中,通常瓶颈存在于数据访问层,而不是语言本身。在实际使用PHP不仅担任逻辑层和展现层的任务,我们甚至用PHP开发的UDPServer/TCPServer作为数据和cache的中间层。

关于作者:王帅,腾讯企业QQ SaaS团队Leader。


深入PHP内核(二)——SAPI探究



摘要:PHP作为一门简单而强大的语言,能够提供很多Web适用的语言特性。从实践出发,继弱类型变量原理探究后,王帅将继续带大家弄清PHP内核中的一些常用部分,本期则是SAPI的深入理解。

SAPI是Server Application Programming Interface(服务器应用编程接口)的缩写。PHP通过SAPI提供了一组接口,供应用和PHP内核之间进行数据交互。

简单的讲,就像函数的输入和输出一样,我们通过Linux命令行执行一段PHP代码,本质是Linux的Shell通过PHP的SAPI传入一组参数,Zend引擎执行后,返回给shell,由shell显示出来的过程。同样的,通过Apache调用PHP,通过Web服务器给SAPI传入数据,Zend引擎执行后,返回给Apache,由Apache显示在页面上。

 

图1. PHP架构图 

PHP提供很多种形式的接口,包括apache、apache2filter、apache2handler、caudium、cgi 、cgi-fcgi、cli、cli-server、continuity、embed、isapi、litespeed、milter、nsapi、phttpd pi3web、roxen、thttpd、tux和webjames。但是常用的只有5种形式,CLI/CGI(命令行)、Multiprocess(多进程)、Multithreaded(多线程)、FastCGI和Embedded(内嵌)。

PHP提供了一个函数查看当前SAPI接口类型:

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. string php_sapi_name ( void )  

PHP的运行和加载

无论使用哪种SAPI,在PHP执行脚本前后,都包含一系列事件:Module的Init(MINT)和Shutdown(MSHUTDOWN),Request 的Init(RINT)和Shutdown(RSHUTDOWN)。 第一阶段是PHP模块初始化阶段(MINT),可以初始化扩展内部变量、分配资源和注册资源处理器,在整个PHP实例生命周期内,该过程只执行一次。

什么是PHP模块?通过上面的PHP架构图,在PHP中可以使用get_loaded_extensions 函数来查看所有编译并加载的模块/扩展,相当于CLI模式下的php -m。

以PHP的Memcached扩展源代码为例:

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. PHP_MINIT_FUNCTION(memcached) {       
  2.     zend_class_entry ce;      
  3.     memcpy(&memcached_object_handlers,zend_get_std_object_handlers(), sizeof(zend_object_handlers));      
  4. memcached_object_handlers.clone_obj = NULL;     /* 执行了一些类似的初始化操作 */       
  5. return SUCCESS;   
  6. }  
第二阶段是请求初始化阶段(RINT),在模块初始化并激活后,会创建PHP运行环境,同时调用所有模块注册的RINT函数,调用每个扩展的请求初始化函数 ,设定特定的环境变量、分配资源或执行其他任务,如审核。

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. PHP_RINIT_FUNCTION(memcached) {       
  2.     /* 执行一些关于请求的初始化 */        
  3.     return SUCCESS;   
  4. }  

第三阶段,请求处理完成后,会调用PHP_RSHUTDOWN_FUNCTION进行回收,这是每个扩展的请求关闭函数,执行最后的清理工作。Zend引擎执行清理过程、垃圾收集、对之前的请求期间用到的每个变量执行unset。请求完成可能是执行到脚本完成,也可能是调用die()或exit()函数完成

第四阶段,当PHP生命周期结束时候,PHP_MSHUTDOWN_FUNCTION对模块进行回收处理,这是每个扩展的模块关闭函数,用于关闭自己的内核子系统。

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. PHP_MSHUTDOWN_FUNCTION(memcached) { /* 执行关于模块的销毁工作 */ UNREGISTER_INI_ENTRIES(); return SUCCESS; }  

常见的运行模式

常见的SAPI模式有五种:

  • CLI和CGI模式(单进程模式)
  • 多进程模式
  • 多线程模式
  • FastCGI模式
  • 嵌入式

1. CLI/CGI模式

CLI和CGI都属于单进程模式,PHP的生命周期在一次请求中完成。也就是说每次执行PHP脚本,都会执行第二部分讲的四个INT和Shutdown事件。

图2. CGI/CLI生命周期 

2. 多进程模式(Multiprocess)

多进程模式可以将PHP内置到Web Server中,PHP可以编译成Apache下的prefork MPM模式和APXS模块,当Apache启动后,会fork很多子进程,每个子进程拥有自己独立的进程地址空间。

 

图3. 多进程模式生命周期 

在一个子进程中,PHP的生命周期是调用MINT启动后,执行多次请求(RINT/RSHUTDOWN),在Apache关闭或进程结束后,才会调用MSHUTDOWN进行回收阶段。 

 

图4. 多进程的生命周期 

多进程模型中,每个子进程都是独立运行,没有代码和数据共享,因此一个子进程终止退出和重新生成,不会影响其他子进程的稳定。

3. 多线程模式(Multithreaded)

Apache2的Worker MPM采用了多线程模型,在一个进程下创建多个线程,在同一个进程地址空间执行。


图5. 多线程生命周期

4. FastCGI模式

在我们用的Nginx+PHP-FPM用的就是FastCGI模式,Fastcgi是一种特殊的CGI模式,是一种常驻进程类型的CGI,运行后可以Fork多个进程,不用花费时间动态的Fork子进程,也不需要每次请求都调用MINT/MSHUTDOWN。PHP通过PHP-FPM来管理和调度FastCGI的进程池。Nginx和PHP-FPM通过本地的TCP Socket和Unix Socket 进行通信。

 

图6. FastCGI模式生命周期

PHP-FPM进程管理器自身初始化,启动多个CGI解释器进程等待来自Nginx的请求。当客户端请求达到PHP-FPM,管理器选择到一个CGI进程进行处理,Nginx将CGI环境变量和标准输入发送到一个PHP-CIG子进程。PHP-CGI子进程处理完成后,将标准输出和错误信息返回给Nginx,当PHP-CGI子进程关闭连接时,请求处理完成。PHP-CGI子进程等待着下一个连接。

可以想象CGI的系统开销有多大。每一个Web 请求PHP都必须重新解析php.ini、载入全部扩展并始化全部数据结构。使用FastCGI,所有这些都只在进程启动时发生一次。另外,对于数据库和Memcache的持续连接可以工作。

5. 内嵌模式(Embedded)

Embed SAPI是一种特殊的SAPI,允许在C/C++语言中调用PHP提供的函数。这种SAPI和CLI模式一样,按照Module Init => Request Init => Request => Request Shutdown => Module Shutdown的模式运行。

Embed SAPI可以调用PHP丰富的类库,也可以实现高级玩法,比如可以查看PHP的OPCODE(PHP执行的中间码,Zend引擎的指令,由PHP代码生成)。

详细请见: http://www.laruence.com/2008/09/23/539.html

SAPI的运行机制

我们以CGI为例,看一下SAPI的运行机制。

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. static sapi_module_struct cgi_sapi_module = {       
  2.     "cgi-fcgi",                     /* 输出给php_info()使用 */     "CGI/FastCGI",                  /* pretty name */        
  3.     php_cgi_startup,                /* startup 当SAPI初始化时,首先会调用该函数 */       
  4.     php_module_shutdown_wrapper,    /* shutdown  关闭函数包装器,它用来释放所有的SAPI的数据结构、内存等,调用php_module_shutdown */        
  5.     sapi_cgi_activate,              /* activate  此函数会在每个请求开始时调用,它会做初始化,资源分配 */       
  6.     sapi_cgi_deactivate,            /* deactivate  此函数会在每个请求结束时调用,它用来确保所有的数据都得到释放 */        
  7.     sapi_cgi_ub_write,              /* unbuffered write  不缓存的写操作(unbuffered write),它是用来向SAPI外部输出数据 */       
  8.     sapi_cgi_flush,                 /* flush  刷新输出,在CLI模式下通过使用C语言的库函数fflush实现*/     NULL,                           /* get uid */       
  9.     sapi_cgi_getenv,                /* getenv 根据name查找环境变量 */        
  10.     php_error,                      /* error handler 注册错误处理函数  */        
  11.     NULL,                           /* header handler PHP调用header()时候被调用 */       
  12.     sapi_cgi_send_headers,          /* send headers handler 发送头部信息*/       
  13.     NULL,                           /* send header handler 发送一个单独的头部信息 */        
  14.     sapi_cgi_read_post,             /* read POST data  当请求的方法是POST时,程序获取POST数据,写入$_POST数组 */       
  15.     sapi_cgi_read_cookies,          /* read Cookies 获取Cookie值  */        
  16.     sapi_cgi_register_variables,    /* register server variables 给$_SERVER添加环境变量 */       
  17.     sapi_cgi_log_message,           /* Log message 输出错误信息 */       
  18.     NULL,                           /* Get request time */       
  19.     NULL,                           /* Child terminate */        
  20.     STANDARD_SAPI_MODULE_PROPERTIES   
  21. };   

由上面代码可见,PHP的SAPI像是面向对象中基类,SAPI.h和SAPI.c包含的函数是抽象基类的声明和定义,各个服务器用的SAPI模式,则是继承了这个基类,并重新定义基类方法的子类。

总结

PHP的SAPI是Zend引擎提供的一组标准交互接口,通过注册初始化、析构、输入、输出等接口,我们可以将应用程序运行在Zend引擎上,也可以把PHP嵌入到类似Apache的Web Server中。PHP常见的SAPI模式有五种,CGI/CLI模式、多进程模式、多线程模式、FastCGI模式和内嵌模式。

了解PHP的SAPI机制意义重大,帮助我们理解PHP的生命周期,并了解如何更好的通过C/C++为PHP编写扩展,并在生命周期中找到提高系统性能的方式。




深入PHP内核(三)——内核利器哈希表与哈希碰撞攻击



摘要:PHP作为一门简单而强大的语言,能够提供很多Web适用的语言特性。从实践出发,继弱类型变量原理探究后,王帅将继续带大家弄清PHP内核中的一些常用部分,本期则是内核利器哈希表与哈希碰撞攻击。

【导读】王帅在海量分布式Web系统有超过8年沉淀,主导过多个大型系统的架构设计,目前在腾讯企业SaaS团队。

PHP内核系列文章,是作者在PHP领域实践中,把相关原理性的知识,通过更便于理解的方式,系统整理出来分享给读者。希望通过PHP原理性的轻量解读,对这门Web领域最热门技术的优秀架构分析解构,让更多的人不断的深入了解语言的原理本身,更容易定位、理解一些问题背后原因,更游刃有余的做基础架构设计。同时希望影响更多的人才投入Web开源领域,不仅是应用和学习一门技术、组件,同样能够贡献更多高质量组件,像战国时期的百家争鸣一样,PHP开源界花开遍地。

作者一直倡导技术的深入学习就像职业篮球训练,80%的时间都是基本功的训练,球场上实际战术的练习只是基本功的应用。同样的,学习PHP语言本身的特性,应当是每个PHP领域工程师所掌握、理解的,至于系统的架构设计也是基于对Linux、Mysql、Nginx等原理机制足够理解后,战术性的使用。

深入PHP内核(三)——内核利器哈希表与哈希碰撞攻击

在PHP的Zend Engine(下面简称ZE)中,有一个非常重要的数据结构——哈希表(HashTable)。哈希表在ZE中有非常广泛的应用,PHP的复杂数据结构中数组和类的存储和访问就是用哈希表来组织,PHP语言结构中的常量、变量、函数等符号表也是用它来组织。

1. 哈希表的基本概念

什么是哈希表呢?哈希表在数据结构中也叫散列表。是根据键名经过hash函数计算后,映射到表中的一个位置,来直接访问记录,加快了访问速度。在理想情况下,哈希表的操作时间复杂度为O(1)。数据项可以在一个与哈希表长度无关的时间内,计算出一个值hash(key),在固定时间内定位到一个桶(bucket,表示哈希表的一个位置),主要时间消耗在于哈希函数计算和桶的定位。

在分析PHP中HashTable实现原理之前,先介绍一下相关的基本概念:

如下图例子,希望通过人名检索一个数据,键名通过哈希函数,得到指向bucket的指针,最后访问真实的bucket。

 

键名(Key):在哈希函数转换前,数据的标识。

桶(Bucket):在哈希表中,真正保存数据的容器。

哈希函数(Hash Function):将Key通过哈希函数,得到一个指向bucket的指针。MD5,SHA-1是我们在业务中常用的哈希函数。

哈希冲突(Hash Collision):两个不同的Key,经过哈希函数,得到同一个bucket的指针。

2. PHP的哈希表实现原理

哈希表的结构:

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. Zend/zend_hash.h  
  2.  typedef struct _hashtable {  
  3.         uint nTableSize;                    //哈希表的长度,不是元素个数  
  4.         uint nTableMask;                  //哈希表的掩码,设置为nTableSize-1  
  5.         uint nNumOfElements;          //哈希表实际元素个数  
  6.         ulong nNextFreeElement;      //指向下一个空元素位置  
  7.         Bucket *pInternalPointer;       //用于遍历哈希表的内部指针  
  8.         Bucket *pListHead;               //哈希表队列的头部  
  9.         Bucket *pListTail;                 //哈希表队列的尾部  
  10.         Bucket **arBuckets;               //哈希表存储的元素数组  
  11.         dtor_func_t pDestructor;          //哈希表的元素析构函数指针  
  12.         zend_bool persistent;              //是否是持久保存,用于pmalloc的参数,可以持久存储在内存中  
  13.         unsigned char nApplyCount;     // zend_hash_apply的次数,用来限制嵌套遍历的层数,限制为3层  
  14.         zend_bool bApplyProtection;     //是否开启嵌套遍历保护  
  15. #if ZEND_DEBUG  
  16.         int inconsistent;  
  17. #endif  
  18. } HashTable;  
1)  nTableSize 哈希表的大小。最小容量是2^3(8),最大容量是2^31(2147483648)。当如果进行一次操作后发现元素个数大于nTableSize,会申请当前nTableSize * 2的空间。假设当前nTableSize为8,当插入元素达到9个的时候,会申请nTableSize=16的空间。

2)  nTableMask 为nTableSize-1,用于调整最大索引值。当哈希后值大于索引值时候,把这个值映射到索引值范围内。

3)  nNumOfElements HashTable中的个数。数组操作中,sizeof和count函数获取的是这个值。

4)  nNextFreeElement 下一个空元素的地址。

5)  pInternalPointer 存储了HashTable当前指向的元素的指针,当我们使用一些内部循环函数的时候会用到这个指针比如reset(), current(), prev(), next(), foreach(), end()。相当于游标。

6)  pListHead和pListTail则具体指向了该哈希表的第一个和最后一个元素,对应就是数组的起始和结束元素。哈希表的pListHead、pListTail与Bucket的pListNext、pListLast维护了一个哈希表中Bucket的双向链表,按照插入的先后顺序,用于哈希表的遍历。

7)  arBuckets 实际存储Buckets的数组。

8)  pDestructor 是一个析构函数,当某个值被从哈希表删除的时候会触发此函数。他还有一个主要作用是用于变量的GC回收。在PHP里面GC是通过引用计数实现的,当一个变量的引用计数变为0,就会被PHP的GC回收。

9)  persistent 定义了hashtable是否能在多次request中获得持久存在。

10)  nApplyCount 和 bApplyProtection 是用来防止嵌套遍历的。

11)  inconsistent 是在调试模式下捕获对HT不正确的使用。

Bucket的结构:

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1.  typedef struct bucket {  
  2.         ulong h;                               //数组索引的哈希值  
  3.         uint nKeyLength;                  //索引数组为0,关联数组为key的长度  
  4.         void *pData;                         //元素内容的指针  
  5.         void *pDataPtr;                    // 如果是指针大小的数据,用pDataPtr直接存储,pData指向pDataPtr  
  6.         struct bucket *pListNext;     //哈希链表中下一个元素  
  7.         struct bucket *pListLast;     //哈希链表中上一个元素  
  8.         struct bucket *pNext;          //解决哈希冲突,变为双向链表,双向链表的下一个元素  
  9.         struct bucket *pLast;          //解决哈希冲突,变为双向链表,双向链表的上一个元素  
  10.         const char *arKey;             //最后一个元素key的名称  
  11. } Bucket;  

通过下图来表示HashTable的原理:

 

我们先来看一下,ZE是如何创建一个hash表的。创建并初始化一个Hash比较容易,调用_zend_hash_init函数。PHP的哈希表最小容量8(2^3),最大容量是0x80000000(2^31,即2147483648)。nTableSize会按照2的整数次幂圆整来增加,直到超过预设值的nSize。

Zend/zend_hash.c

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. ZEND_API int _zend_hash_init(HashTable *ht, uint nSize, hash_func_t pHashFunction, dtor_func_t pDestructor, zend_bool persistent ZEND_FILE_LINE_DC)  
  2. {  
  3.         uint i = 3;  
  4.   
  5.         SET_INCONSISTENT(HT_OK);  
  6.   
  7.         if (nSize >= 0x80000000) {  
  8.                 /* prevent overflow */  
  9.                 ht->nTableSize = 0x80000000;  
  10.         } else {  
  11.                 while ((1U << i) < nSize) {  
  12.                         i++;  
  13.                 }  
  14.                 ht->nTableSize = 1 << i;  
  15.         }  
  16.   
  17.         /* 省略哈希表初始化步骤 */  
  18.   
  19.         return SUCCESS;  
  20. }  

1)  *ht 是哈希表的指针,这里既可以传入一个已存在的HashTable, 也可以通过内核宏ALLOC_HASHTABLE(ht)来自动申请一块HashTable内存。ALLOC_HASHTABLE(ht)相当于ht=emalloc(sizeof(HashTable))

2)  nSize 哈希表能拥有的最大数量。通过预先申请好内存的方式,减少哈希表rehash操作。

3)  pHashFunction 自定义哈希函数的钩子

4)  pDesctructor 哈希表析构的回调函数,当删除一个哈希表的时候,会调用。

5)  persistent 对应HashTable.persistent,当设置为true的时候,不会在RSHUTDOWN阶段自动销毁。

我们通过更新哈希表的操作方式,来分析哈希表的操作机制: 

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. h = zend_inline_hash_func(arKey, nKeyLength);  
  2. nIndex = h & ht->nTableMask;  
  3.   
  4. p = ht->arBuckets[nIndex];  
  5. while (p != NULL) {  
  6.     if (p->arKey == arKey ||  
  7.     ((p->h == h) && (p->nKeyLength == nKeyLength) && !memcmp(p->arKey, arKey,         nKeyLength))) {  
  8.     if (flag & HASH_ADD) {  
  9.         return FAILURE;  
  10.     }  
  11.   
  12.     /* 省略 */  
  13.   
  14.     UPDATE_DATA(ht, p, pData, nDataSize);   // 找到h 和 Key都相等的Buckets,说明需要更新  
  15.     /* 省略 */  
  16.     }  
  17.     p = p->pNext;   // 这里说明有哈希冲突,按照Buckets[nIndex]的链表找下去  
  18. }  
  19.   
  20. /* 省略 */  
  21. p->nKeyLength = nKeyLength;  
  22. INIT_DATA(ht, p, pData, nDataSize);    // 把Bucket.pData数据更新  
  23. p->h = h;  
  24. CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex]);    // 挂到  
  25. if (pDest) {  
  26.     *pDest = p->pData;  
  27. }  
  28.   
  29. HANDLE_BLOCK_INTERRUPTIONS();  
  30. CONNECT_TO_GLOBAL_DLLIST(p, ht);  
  31. ht->arBuckets[nIndex] = p;      
  32. HANDLE_UNBLOCK_INTERRUPTIONS();  
  33.   
  34. ht->nNumOfElements++;  
  35. ZEND_HASH_IF_FULL_DO_RESIZE(ht); /* 如果哈希表满了,重新散列,这里有一定开销   */  
1) 通过哈希算法  times33(Key) & (nTableSize-1) ,生成Key对应的哈希值A,获取arBuckets[A]的值

2) 判断arBuckets[A]是否存在,如果存在而且没有哈希冲突,进行数据update(UPDATE_DATA)。如果存在但是Key不相同说明有哈希冲突,在arBuckets[A]链表中寻找Key是否存在,如果存在,执行update操作(UPDATE_DATA)

3) 如果arBuckets[A]不存在,创建新的arBucket[A](INIT_DATA)。或哈希冲突情况下,在arBuckets[A]的链表中找不到Key。创建新的bucket(INIT_DATA),并把新的buckets放在arBucket[A]链表头

4) 维护哈希表的逻辑链表(CONNECT_TO_GLOBAL_DLLIST)。

5) 如果发现新插入元素已经超过HashTable的nTableSize,自动扩容至2倍nTableSize,重新哈希后维护新的HashTable。

3. PHP使用的哈希函数

PHP的哈希表是用Times33哈希算法,又称为DJBX33A。这是一个使用比较广泛的对字符串的哈希算法,计算速度快,散列均匀,Perl和Apache都使用了这个算法。算法原理就是不断的乘以33,其算法原型如下:

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. hash(i) = hash(i-1) * 33 + str[i]  
为什么是33呢?对于33这个数,DJB注释中是说,1到256之间的所有奇数,都能达到一个可接受的哈希分布,平均分布大概是86%。而其中33,17,31,63,127,129这几个数在面对大量的哈希运算时有一个更大的优势,就是这些数字能将乘法用位运算配合加减法替换,这样运算速度会更高。gcc编译器开启优化后会自动将乘法转换为位运算。PHP实际算法如下:

[php]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1.  static inline ulong zend_inline_hash_func(const char *arKey, uint nKeyLength)  
  2. {  
  3.     register ulong hash = 5381;  
  4.   
  5.     /* variant with the hash unrolled eight times */  
  6.     for (; nKeyLength >= 8; nKeyLength -= 8) {  
  7.         hash = ((hash << 5) + hash) + *arKey++;  
  8.         hash = ((hash << 5) + hash) + *arKey++;  
  9.         hash = ((hash << 5) + hash) + *arKey++;  
  10.         hash = ((hash << 5) + hash) + *arKey++;  
  11.         hash = ((hash << 5) + hash) + *arKey++;  
  12.         hash = ((hash << 5) + hash) + *arKey++;  
  13.         hash = ((hash << 5) + hash) + *arKey++;  
  14.         hash = ((hash << 5) + hash) + *arKey++;  
  15.     }  
  16.     switch (nKeyLength) {  
  17.         case 7: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */  

猜你喜欢

转载自blog.csdn.net/ahaotata/article/details/85015555