3. PHP中哈希表实现与数组的顺序遍历

PHP中哈希表实现

哈希表的相关概念可以看TIPI中哈希表介绍,也可以直接看算法导论中哈希表的介绍。

哈希表

PHP中的哈希表实现在Zend/zend_hash.c中。HashTable结构体用于保存整个哈希表需要的基本信息,Bucket结构体用于保存具体的数据内容,如下:

typedef struct _hashtable { 
    uint nTableSize;        // hash Bucket的大小,最小为8,以2x增长。
    uint nTableMask;        // nTableSize-1 , 索引取值的优化
    uint nNumOfElements;    // hash Bucket中当前存在的元素个数,count()函数会直接返回此值 
    ulong nNextFreeElement; // 下一个数字索引的位置
    Bucket *pInternalPointer;   // 当前遍历的指针(foreach比for快的原因之一)pInternalPointer指向当前的内部指针的位置, 在对数组进行顺序遍历的时候, 这个指针指明了当前的元素.当在线性(顺序)遍历的时候, 就会从pListHead开始, 顺着Bucket中的pListNext/pListLast, 根据移动pInternalPointer, 来实现对所有元素的线性遍历.


    Bucket *pListHead;          // 存储数组头元素指针
    Bucket *pListTail;          // 存储数组尾元素指针
    Bucket **arBuckets;         // 存储hash数组
    dtor_func_t pDestructor;    // 在删除元素时执行的回调函数,用于资源的释放
    zend_bool persistent;       //指出了Bucket内存分配的方式。如果persisient为TRUE,则使用操作系统本身的内存分配函数为Bucket分配内存,否则使用PHP的内存分配函数。
    unsigned char nApplyCount; // 标记当前hash Bucket被递归访问的次数(防止多次递归)
    zend_bool bApplyProtection;// 标记当前hash桶允许不允许多次访问,不允许时,最多只能递归3次
#if ZEND_DEBUG
    int inconsistent;
#endif
} HashTable;

数据容器:槽位

typedef struct bucket {
    ulong h;            // 对char *key进行hash后的值,或者是用户指定的数字索引值
    uint nKeyLength;    // hash关键字的长度,如果数组索引为数字,此值为0
    void *pData;        // 指向value,一般是用户数据的副本,如果是指针数据,则指向pDataPtr
    void *pDataPtr;     //如果是指针数据,此值会指向真正的value,同时上面pData会指向此值
    struct bucket *pListNext;   // 整个hash表的下一元素
    struct bucket *pListLast;   // 整个哈希表该元素的上一个元素
    struct bucket *pNext;       // 存放在同一个hash Bucket内的下一个元素
    struct bucket *pLast;       // 同一个哈希bucket的上一个元素

    char arKey[1];   // 保存当前值所对于的key字符串,这个字段只能定义在最后,实现变长结构体            
} Bucket;

在PHP中可以使用字符串或者数字作为数组的索引,数字索引直接就可以作为哈希表的索引,数字无需进行哈希处理。h字段后面的nKeyLength字段是作为key长度的标示,如果索引是数字的话,则nKeyLength为0.在PHP数组中如果索引字符串可以被转换成数字也会被转换成数字索引。所以在php中’19’ ‘20’这样的字符索引和数字索引19,20没有区别。

上面结构体的最后一个字段用来保存key的字符串,而这个字段却申明为只有一个字符的数组, 其实这里是一种长见的变长结构体,主要的目的是增加灵活性。

一张图来解释Zend引擎哈希表结构和关系 来自tipi

详细解释:

  • PHP的Zend引擎中哈希表处理冲突使用的是拉链
  • HashTable结构体中存储的是哈希表的整体信息,Bucket中存储的是真正的数据
  • Bucket结构体维护了两个双向链表,pNext和pLast指针分别指向本槽位所在的链表关系。比如相同hash值的两个key分别是k1,k2,先插入k1再插入k2,那么k2->pNext = k1; k1->pLast = k2;
  • pListHead和pListTail维护了整个哈希表的头元素指针和最后一个元素的指针。
  • 关于pListNext和pListLast
    • 当在线性(顺序)遍历的时候, 就会从pListHead开始, 顺着Bucket中的pListNext/pListLast, 根据移动pInternalPointer, 来实现对所有元素的线性遍历。
    • 数组添加元素的时候,依次添加元素。比如两个key,先添加k1再添加k2,则k1->pListNext=k2, k2->pListLast=k1。
    • 也就是说PHP中遍历数组的顺序是和元素的添加先后顺序相关的。
    • 哈希表的Bucket结构通过pListNext和pListLast维护了哈希表插入元素的先后顺序。
  • pListNext和pListLast指针指向的是整个哈希表所有的数据之间的链接关系。

参考文章:

  1. 深入理解PHP之数组(遍历顺序)
  2. 深入理解PHP原理之foreach
  3. 变量的结构和类型
  4. PHP的哈希表实现

5. PHP预定义变量REQUEST/GET/POST等处理过程

1. 基础环境

  • php版本:php.5.6.25
  • php.ini中一个配置 variables_order = "GPCS"

2. 处理流程

a. 初始化

  1. 对于_GET,_POST,_COOKIE,_SERVER, _ENV, _REQUEST, _FILES处理
    fpm启动时会对这些预定义常量进行处理初始化,设置其回调函数。

    php_cgi_startup(sapi/fpm/fpm/fpm_main.c)->php_startup_auto_globals中进行初始化

    void php_startup_auto_globals(TSRMLS_D)
    {
        zend_register_auto_global(ZEND_STRL("_GET"), 0, php_auto_globals_create_get TSRMLS_CC);
        zend_register_auto_global(ZEND_STRL("_POST"), 0, php_auto_globals_create_post TSRMLS_CC);
        zend_register_auto_global(ZEND_STRL("_COOKIE"), 0, php_auto_globals_create_cookie TSRMLS_CC);
        zend_register_auto_global(ZEND_STRL("_SERVER"), PG(auto_globals_jit), php_auto_globals_create_server TSRMLS_CC);
        zend_register_auto_global(ZEND_STRL("_ENV"), PG(auto_globals_jit), php_auto_globals_create_env TSRMLS_CC);
        zend_register_auto_global(ZEND_STRL("_REQUEST"), PG(auto_globals_jit), php_auto_globals_create_request TSRMLS_CC);
        zend_register_auto_global(ZEND_STRL("_FILES"), 0, php_auto_globals_create_files TSRMLS_CC);
    }
    
  2. 对于GLOBALS处理
    php_cgi_startup->php_module_startup->zend_startup中会对$GLOBALS的处理进行初始化

    zend_register_auto_global("GLOBALS", sizeof("GLOBALS") - 1, 1, php_auto_globals_create_globals TSRMLS_CC);

  3. zend_register_auto_global解释
    该函数在Zend/zend_compile.c中定义,主要是将这些添加到CG(auto_globals)这个全局的hash表中。

b. 请求到来时

php_request_startup->php_hash_environment-> zend_activate_auto_globals ->zend_hash_apply其中zend_activate_auto_globals的核心是调用

zend_hash_apply(CG(auto_globals),(apply_func_t) zend_auto_global_init TSRMLS_CC)

zend_auto_global_init中会对CG(auto_globals)进行处理,执行初始化时设置的回调函数。

对于回调函数的解释

首先介绍php.ini中的配置variables_ordervariables_order目的是设置the EGPCS (Environment, Get, Post, Cookie, and Server)变量的解析顺序。比如,variables_order设置为PG,那么只会设置全局变量$POST$_GET,并且对于$_REQUEST,如果$POST['a']$_GET['a'],那么在$_REQUEST$_GET['a']会覆盖$_POST['a']的值。

明白了variables_order的含义,接下来就看一下回调函数。

  • 数据处理:对于GET,POST,COOKIE三者的回调函数都是sapi_module.treate_data。对于SERVER,ENV,FILES,REQUEST则走的其他逻辑。
  • 数据保存:得到数据以后,会经过zend_hash_update(&EG(symbol_table), 变量名..)存入符号表中。

一些细节的地方:

  • 其中sapi_module.treate_data的初始化是在php_startup_sapi_content_types中设置的, sapi_module.treate_data = php_default_treate_data。大多数sapi都是使用的默认的处理函数php_default_treate_data

  • php_default_treat_data中,对于变量,都调用php_register_variable_safe来注册变量, 而php_register_variable_safe最终会调用php_register_variable_ex:

PHPAPI void php_register_variable_ex(char *var, zval *val, zval *track_vars_array TSRMLS_DC)
{
        ... 省略
  for (p = var; *p; p++) {
  if (*p == ' ' || *p == '.') {
      *p='_';
  } else if (*p == '[') {
      is_array = 1;
      ip = p;
      *p = 0;
      break;
  }
        ....以下省略
}

就是在php_register_variable的时候,会将(.)转换成(_)

3. 预定义变量的获取

在某个局部函数中使用类似于$GLOBALS变量这样的预定义变量, 如果在此函数中有改变的它们的值的话,这些变量在其它局部函数调用时会发现也会同步变化。 为什么呢?是否是这些变量存放在一个集中存储的地方? 从PHP中间代码的执行来看,这些变量是存储在一个集中的地方:EG(symbol_table)。

在通过$获取变量时,PHP内核都会通过这些变量名区分是否为全局变量(ZEND_FETCH_GLOBAL), 其调用的判断函数为zend_is_auto_global,这个过程是在生成中间代码过程中实现的。 如果是ZEND_FETCH_GLOBALZEND_FETCH_GLOBAL_LOCK(global语句后的效果), 则在获取获取变量表时(zend_get_target_symbol_table), 直接返回EG(symbol_table)。则这些变量的所有操作都会在全局变量表进行。

4. 参考文章

说明:
在参考文章中使用的php版本应该是5.3.x版本。我特意下载了php-5.3.0看了一下php_hash_environment的实现,确实是下面两篇文章所介绍的。而在php-5.6中,采用的则是先初始化CG(auto_globals)的方式。

  1. 预定义常量
  2. PHP的GET/POST等大变量生成过程

1. PHP变量的存储结构

基础介绍

变量具有三个基本组成部分:

  • 名称
  • 类型
  • 值内容

数据类型:
从类型的维度来看,编程语言可以分为三大类:

  • 静态类型语言:比如C/Java等,类型的检查是在编译期(compile-time)确定的。
  • 动态语言类型:比如PHP,pythone等各种脚本语言,这类语言的类型在运行时确定的。
  • 无类型语言:比如汇编语言,汇编语言操作的是底层存储。

变量的结构

在官方的PHP实现内部,所有变量使用同一种数据结构zval来保存,而这个结构同时表示PHP中各种数据类型。它不仅仅包含变量的值,也包含变量的类型。这就是PHP弱类型的核心。

php变量类型及存储结构

PHP是弱类型语言,但这并不表示PHP没有类型,在PHP中,存在8种变量类型,可以分为三类:
标量类型: boolean, integer, float(double), string
复合类型: array, object,
特殊类型: resource, NULL

变量存储结构

// Zend/zend.h
typedef struct _zval_struct zval;
...
struct _zval_struct {
    /* Variable information */
    zvalue_value value;     /* value */
    zend_uint refcount__gc;
    zend_uchar type;    /* active type */
    zend_uchar is_ref__gc;
};

变量类型

type的值可以是IS_NULL, IS_BOOL, iS_LONG, IS_DOUBLE, IS_STRING, IS_ARRAY, IS_OBJECT, IS_RESOURCE之一。

变量的值存储

前面提到变量的值存储在zvalue_value结构体中,结构体定义如下:之所以是联合体是因为一个变量同时只能属于一种类型。

typedef union _zvalue_value {
    long lval;                  /* long value */
    double dval;                /* double value */
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;              /* hash table value */
    zend_object_value obj;
} zvalue_value;

字符串string

字符串的类型标示和其他数据类型一样,不过在存储字符串时多了一个字符串长度的字段。

struct {
    char *val;
    int len;
}str;

数组Array

数组是PHP中最常用的,也是最强大的变量类型,可以存储其他类型的数据,而且提供各种内置操作函数。数组的值存储在zvalue_value.ht字段中,是一个HashTable类型的数据。

对象object

在面向对象语言中,我们能自己定义自己需要的数据类型,包括类的属性,方法等数据。而对象则是类的一个具体实现。对象有自身的状态和所能完成的操作。
PHP的对象是一种复合型的数据,使用一种zend_object_value的结构体来存放。其定义如下:

typedef struct _zend_object_value {
    zend_object_handle handle;  //  unsigned int类型,EG(objects_store).object_buckets的索引
    zend_object_handlers *handlers;
} zend_object_value;

PHP的对象只有在运行时才会被创建,前面的章节介绍了EG宏,这是一个全局结构体用于保存在运行时的数据。 其中就包括了用来保存所有被创建的对象的对象池,EG(objects_store)即全局变量executor_globals.objects_store,而object对象值内容的zend_object_handle域就是当前 对象在对象池中所在的索引,handlers字段则是将对象进行操作时的处理函数保存起来。 这个结构体及对象相关的类的结构_zend_class_entry,将在第五章作详细介绍。

#define EG(v) (executor_globals.v)
extern ZEND_API zend_executor_globals executor_globals;

参考文章:

  1. 变量的结构和类型

PHP源码之用户代码的执行

二.用户代码的执行

一切的开始:SAPI接口

SAPI(Server Application Programming Interface) 指的是PHP具体应用的编程接口。
脚本执行的开始都是以SAPI接口实现开始的。只是不同的SAPI接口实现会完成他们特定的工作。

开始和结束

PHP开始执行以后会经过两个主要阶段:

  • 处理请求之前的开始阶段:又分为两个过程
    • 第一个过程:模块初始化阶段(MINIT),在整个SAPI生命周期内只进行一次。比如sudo /etc/init.d/php-fpm start时。例如PHP在MINIT阶段会回调所有模块的MINIT函数,进行一些初始化工作,如注册常量,定义模块使用的类等等
    • 第二个过程:模块激活阶段(RINIT),该过程发生在请求阶段,在每个请求到来之前都会进行模块激活。其目的是请求到达以后PHP初始化执行脚本的基本环境,例如创建一个执行环境,包含保存PHP允许过程中变量名称和值内容的符号表,以及当前所有的函数和类的符号表。然后PHP调用所有模块的RINIT函数。
  • 处理请求之后的结束阶段

    • 第一个过程:请求结束后停用模块(RSHUTDOWN,对应RINIT)
    • 第二个过程:关闭模块,在SAPI生命周期结束(WEB服务器退出或者命令行脚本执行完毕退出)MSHUTDOWN,对应MINIT。
  • 处理流程图(引用TIPI图)
    图片

fastcgi详细执行流程

初始化:即 /etc/init.d/php-fpm start

  1. 入口函数:在sapi/fpm/fpm/fpm_main.c中的main函数
  2. 调用cgi_sapi_module.startup(函数指针startup的取值是php_cgi_startup),php_cgi_startup做了以下几件事情:

    • 初始化若干全局变量。如zuf.printf_function = php_printfPHP_VERSION
    • 初始化zend引擎和核心组件。php_module_startup会调用zend_startup(zend/zend.c)函数。zend_startup函数的作用就是初始化zend引擎。这里的初始化操作包括内存管理初始化、 全局使用的函数指针初始化(如前面所说的zend_printf等),对PHP源文件进行词法分析、语法分析、 中间代码执行的函数指针的赋值,初始化若干HashTable(比如函数表,常量表等等),为ini文件解析做准备, 为PHP源文件解析做准备,注册内置函数(如strlen、define等),注册标准常量(如E_ALL、TRUE、NULL等)、注册GLOBALS全局变量(zend_register_auto_global("GLOBALS",..., php_auto_globals_create_globals))等。
      • zend_startup->zend_startup_builtin_functions->zend_register_module_ex注册core模块,core模块的module_number是0。core模块中的函数分别是zend_version, func_num_argsgc_disable
      • 注册E_ERRORE_WARNING这些常量。zend_startup->zend_register_standard_constants
    • 解析php.ini:php_init_config函数的作用就是读取php.ini文件,设置配置参数,加载zend扩展并注册php扩展函数。
    • 全局操作函数的初始化:
      • php_startup_auto_globals(main/php_variables.c)函数会初始化在用户空间使用频率很高的一些全局变量,如$_GET,$_POST,$_FILES等。其调用的zend_register_auto_global函数会将这些变量名添加到CG(auto_globals)这个变量表。
      • php_startup_sapi_content_types函数用来初始化SAPI对于不同类型内容的处理函数。这里的处理函数包括POST数据默认处理函数
    • 初始化静态构建的模块和共享模块MINIT
      • php_register_internal_extensions_func函数用来注册静态构建的模块,也就是默认加载的模块。
      • 模块初始化会执行两个操作:1. 将这些模块注册到已注册的模块列表(module_registry) 2. 将每个模块中包含的注册到函数表CG(function_table).可以看到各个模块是按照首字母排序的(date模块的module_number是2, ereg模块是3,libxml是4,xsl是46,zip是47。特别是cgi-fcgi是48)
      • 在所有的模块都注册有,PHP会马上执行模块初始化操作(zend_startup_modules)。它的整个过程就是依次遍历每个模块,调用每个模块的模块初始化函数(PHP_MINIT_FUNCTION)。
    • 禁用函数和类 php_disable_functionsphp_disable_classes
  3. 补充一段 进程初始化部分

    • fpm_init
    • fpm_run中会调用fpm_children_create_initial(wp)进行worker进程(子进程)的初始化,
      • fpm_children_create_initial函数返回:0表示是子进程、1表示父进程、2表示错误。
      • fpm_run函数对于父进程不返回,执行fpm_event_loop(0); 永远进行事件循环
      • fpm_run函数对于子进程返回listen_fd
  4. 此时调用fcgi_accept_request(fpm/fpm/fastcgi.c)。因为没有请求,因此listen_socket加锁,卡在accept处,等待请求的到来

  5. 当有请求到来时,会从accept后续开始执行。

接受请求

在处理了文件相关的内容,PHP会调用php_request_startup做请求初始化操作。 请求初始化操作,除了图中显示的调用每个模块的请求初始化函数外,还做了较多的其它工作,其主要内容如下

  1. 用户空间中需要用到的一些环境变化的初始化,包括服务器环境、请求数据环境等。实际用到的就是$_POST, $_GET, $_COOKIE, $_SERVER, $_ENV, $_FILES

    • php_request_startup->php_hash_environment,调用CG(auto_globals)中的回调来处理。到了这里说个题外话, 就是在php.ini中, 可以使用variables_order来控制PHP是否生成某个大变量,已经大变量的生成顺序。关于顺序,就是说, 如果打开了auto_register_globals的情况下, 如果先处理p,后处理g,那么$_GET[‘a’],就会覆盖$_POST[‘a’];
    • php.ini设置中variables_order string
      设置了EGPCS(Environment, Get, Post, Cookie and Server)变量的解析顺序。比如,variables_order顺序设置为SP,那么PHP将会创建超级全局变量$_SERVER和$_POST,而不会创建$_ENV, $_GET, $_COOKIE。

    • sapi_module.default_post_reader一样,sapi_module.treat_data的值也是在模块初始化时, 通过php_startup_sapi_content_types函数注册了默认数据处理函数为main/php_variables.c文件中php_default_treat_data函数。

    • 以$_COOKIE为例,php_default_treat_data函数会对依据分隔符,将所有的cookie拆分并赋值给对应的变量。
  2. 模块请求的初始化。PHP通过zend_activate_modules函数实现模块请求的初始化。会调用各个模块的RINIT函数。这儿的调用并没有按照字母排序的顺序调用模块。比如libxml,zlib,intl,mbstring…

  3. 运行。
    • php_execute_script函数包含了运行php脚本的全部过程。当一个php文件需要解析执行时,它可能会需要执行三个文件,其中包括一个前置执行文件、当前需要执行文件和一个后置文件。非当前的两个文件可以在php.ini文件通过auto_prepend_file参数和auto_append_file参数设置。如果这两个参数设置为空,则禁用对应执行文件。
    • 对于需要解析执行的文件,通过zend_compile_file(compile_file函数)做词法分析、语法分析和中间代码生成操作,返回此文件的所有中间代码。 如果解析的文件有生成有效的中间代码,则调用zend_execute(execute函数)执行中间代码。 如果在执行过程中出现异常并且用户有定义对这些异常的处理,则调用这些异常处理函数。 在所有的操作都处理完后,PHP通过EG(return_value_ptr_ptr)返回结果

PHP关闭请求

PHP关闭请求的过程是一个若干个关闭操作的集合,这个集合存在于php_request_shutdown函数中。 这个集合包括如下内容:

  • 调用所有通过register_shutdown_function()注册的函数。这些在关闭时调用的函数是在用户空间添加加来的。我们可以在脚本出错时调用一个统一的函数,给用户一个友好一些的页面,这个有点类似于网页中的404页面。
  • 执行所有可用的__destruct函数。 这里的析构函数包括在对象池(EG(objects_store)中的所有对象的析构函数以及EG(symbol_table)中各个元素的析构方法。
  • 将所有的输出刷出去
  • 发送HTTP应答头。也是一个输出字符串的过程,只是这个字符串可能符合某些规范
  • 遍历每个模块的关闭请求方法,执行模块的请求关闭操作,即RSHUTDOWN
  • 销毁全局变量表(PG(http_globals))的变量。
  • 通过zend_deactivate函数,关闭词法分析器、语法分析器和中间代码执行器
  • 调用每个扩展的post-RSHUTDOWN函数。只是基本每个扩展的post_deactivate_func函数指针都是NULL
  • 关闭SAPI,通过sapi_deactivate销毁SG(sapi_headers)、SG(request_info)等的内容。
  • 关闭流的包装器、关闭流的过滤器
  • 关闭内存管理
  • 重新设置最大执行时间

结束

  1. flush

    sapi_flush将最后的内容刷回去。调用的是sapi_module.flush。

  2. 关闭zend引擎

    此时对应图中的流程,我们应该是执行每个模块的关闭模块操作。 在这里只有一个zend_hash_graceful_reverse_destroy函数将module_registry销毁了。 当然,它最终也是调用了关闭模块的方法的,其根源在于在初始化module_registry时就设置了这个hash表析构时调用ZEND_MODULE_DTOR宏。 而ZEND_MODULE_DTOR宏对应的是module_destructor函数。 在此函数中会调用模块的module_shutdown_func方法,即PHP_RSHUTDOWN_FUNCTION宏产生的那个函数。

    在关闭所有的模块后,PHP继续销毁全局函数表,销毁全局类表、销售全局变量表等。 通过zend_shutdown_extensions遍历zend_extensions所有元素,调用每个扩展的shutdown函数。

【todo】目前需要看的地方有:

  1. redismemcache的持久化连接是如何处理的
  2. 全局变量表这些如何做的呢
  3. 最后关闭这儿是如何处理的呢,还没有详细看代码
  4. opcode:一个代码构造出来的完整opcode是什么样子呢
    【todo】opcode这部分还是没有理解请求

2. 深入理解PHP内核之SAPI概述(讲述FASTCGI)

在各个服务器抽象层之间遵守着相同的约定,这里我们称之为SAPI接口。 每个SAPI实现都是一个_sapi_module_struct结构体变量。(SAPI接口)。 在PHP的源码中,当需要调用服务器相关信息时,全部通过SAPI接口中对应方法调用实现, 而这对应的方法在各个服务器抽象层实现时都会有各自的实现。如下图所示,为SAPI的简单示意图(引用TIPI图)

SAPI简单示意图

因为平时使用fastcgi,因此这儿参考TIPI中讲述apache2的方式来讲讲fastcgi
它的启动方法如下:

cgi_sapi_module.startup(&cgi_sapi_module) // fastcgi模式 sapi/fpm/fpm/fpm_main.c

这儿的cgi_sapi_module.startup是sapi_module_struct结构体的静态变量。静态变量的详细解释如下(引用TIPI内容):

struct _sapi_module_struct {
char *name;         //  名字(标识用)
char *pretty_name;  //  更好理解的名字(自己翻译的)

int (*startup)(struct _sapi_module_struct *sapi_module);    //  启动函数
int (*shutdown)(struct _sapi_module_struct *sapi_module);   //  关闭方法

int (*activate)(TSRMLS_D);  // 激活
int (*deactivate)(TSRMLS_D);    //  停用

int (*ub_write)(const char *str, unsigned int str_length TSRMLS_DC);
 //  不缓存的写操作(unbuffered write)
void (*flush)(void *server_context);    //  flush
struct stat *(*get_stat)(TSRMLS_D);     //  get uid
char *(*getenv)(char *name, size_t name_len TSRMLS_DC); //  getenv

void (*sapi_error)(int type, const char *error_msg, ...);   /* error handler */

int (*header_handler)(sapi_header_struct *sapi_header, sapi_header_op_enum op,
    sapi_headers_struct *sapi_headers TSRMLS_DC);   /* header handler */

 /* send headers handler */
int (*send_headers)(sapi_headers_struct *sapi_headers TSRMLS_DC);

void (*send_header)(sapi_header_struct *sapi_header,
        void *server_context TSRMLS_DC);   /* send header handler */

int (*read_post)(char *buffer, uint count_bytes TSRMLS_DC); /* read POST data */
char *(*read_cookies)(TSRMLS_D);    /* read Cookies */

/* register server variables */
void (*register_server_variables)(zval *track_vars_array TSRMLS_DC);

void (*log_message)(char *message);     /* Log message */
time_t (*get_request_time)(TSRMLS_D);   /* Request Time */
void (*terminate_process)(TSRMLS_D);    /* Child Terminate */

char *php_ini_path_override;    //  覆盖的ini路径

...
...
};

其中一些函数指针的说明如下:

  • startup:当sapi初始化时,首先会调用该函数。 startup函数只在父进程中创建一次,在其fork的子进程中不会调用。在fastcgi的
  • activate:在每个请求开始时调用,他会再次初始化每个请求前的数据结构。

其中fastcgi的sapi_module_struct的定义在fpm/fpm/fpm_main.c中,定义如下:

static sapi_module_struct cgi_sapi_module = {
"fpm-fcgi",                        /* name */
"FPM/FastCGI",                    /* pretty name */

php_cgi_startup,                /* startup */
php_module_shutdown_wrapper,    /* shutdown */

sapi_cgi_activate,                /* activate */
sapi_cgi_deactivate,            /* deactivate */

sapi_cgibin_ub_write,            /* unbuffered write */
sapi_cgibin_flush,                /* flush */
NULL,                            /* get uid */
sapi_cgibin_getenv,                /* getenv */

php_error,                        /* error handler */

NULL,                            /* header handler */
sapi_cgi_send_headers,            /* send headers handler */
NULL,                            /* send header handler */

sapi_cgi_read_post,                /* read POST data */
sapi_cgi_read_cookies,            /* read Cookies */

sapi_cgi_register_variables,    /* register server variables */
sapi_cgi_log_message,            /* Log message */
NULL,                            /* Get request time */
NULL,                            /* Child terminate */

STANDARD_SAPI_MODULE_PROPERTIES
};

参考文章

SAPI概述

fastcgi概述

PHP源码之准备工作和背景知识

深入理解PHP内核–学习版
说明:深入理解PHP内核是非常非常好的书,当然书中内容只有你详细尝试过,走一遍代码才能理解的更深刻,这几篇文章,当做自己的一个学习笔记。

一、学习环境搭建

  1. php源代码: 我下载的是php5.6.25。
  2. 编译环境:由于会涉及到nginx,php,mysql等等,为了快速搭建lnmp的环境,建议使用lnmp先安装所需要的各种软件包。
  3. 编译语句:
    因为我是在一个全新的centos虚拟机上进行学习,因此安装目录是/usr/local/php。

    './configure' '--prefix=/usr/local/php' '--with-config-file-path=/usr/local/php/etc' '--enable-fpm' '--with-fpm-user=www' '--with-fpm-group=www' '--with-mysql=mysqlnd' '--with-mysqli=mysqlnd' '--with-pdo-mysql=mysqlnd' '--with-iconv-dir=libiconv' '--with-freetype-dir=/usr/local/freetype' '--with-jpeg-dir' '--with-png-dir' '--with-zlib' '--with-libxml-dir=/usr' '--enable-xml' '--disable-rpath' '--enable-bcmath' '--enable-shmop' '--enable-sysvsem' '--enable-inline-optimization' '--with-curl' '--enable-mbregex' '--enable-mbstring' '--with-mcrypt' '--enable-ftp' '--with-openssl' '--with-mhash' '--enable-pcntl' '--enable-sockets' '--enable-zip' '--enable-soap' '--with-gettext' '--disable-fileinfo' '--enable-opcache' '--enable-intl' '--with-xsl'

    此时试一下php是否可以运行

  4. php的源码目录结构

    • build:放置一些和源码编译相关的一些文件。比如开始构建之前的buildconf脚本等文件
    • ext 官方扩展目录,包括了绝大多数PHP的函数定义和实现,如array系列,pdo系列,spl系列等函数实现
    • main:这里存放的是PHP最为核心的文件了,主要实现php的基本设施。
    • zend:zend引擎的实现目录,比如脚本的词法语法解析,opcode的执行和扩展机制的实现等等
    • pear:php扩展与应用仓库
    • sapi:包含了各种服务器抽象层的代码,如apache的mod_php,cgi,fastcgi以及fpm等接口
    • TSRM:
    • tests
    • win32
  1. php源码阅读工具

    使用vim+ctags阅读

    • 安装ctags: yum install ctags
    • 生成tags: cd /your/php/source/directory && ctags -R
    • 在.vimrc中添加ctags路径:set tags+=/your/php/source/directory/tags
    • 使用:“使用 Ctrl+] 就可以自动跳转至定义,Ctrl+t 可以返回上一次查看位置。这样就可以快速的在代码之间“游动”了。
  1. PHP源码中的常用代码

    • 双井号(##):”##”被称为 连接符(concatenator),它是一种预处理运算符, 用来把两个语言符号(Token)组合成单个语言符号。 这里的语言符号不一定是宏的变量。并且双井号不能作为第一个或最后一个元素存在
    • 单井号(#):”#”是一种预处理运算符,它的功能是将其后面的宏参数进行 字符串化操作 , 简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号, 用比较官方的话说就是将语言符号(Token)转化为字符串
    • 宏定义中的do-while循环
    • #line预处理:用于改变当前的行号(LINE)和文件名(FILE
    • PHP中的全局变量宏,如PG(), EG()之类的函数,他们都是PHP中定义的宏,这系列宏主要的作用是解决线程安全所写的全局变量包裹宏。在PHP代码的其他地方也存在很多类似的宏,这些宏和PG宏一样,都是为了将线程安全进行封装,同时通过约定的 G 命名来表明这是全局的, 一般都是个缩写,因为这些全局变量在代码的各处都会使用到,这也算是减少了键盘输入。

参考文章

第一章 准备工作和背景知识

php原理-鸟哥的文章列表

  1. 概述

    深入浅出PHP

  2. 变量

    深入理解PHP原理之变量
    深入理解PHP原理之变量作用域(Scope in PHP)
    深入理解PHP原理之变量分离/引用
    深入理解php内核之写时复制
    到了引用这儿还是有点懵逼
    PHP的GET/POST等大变量生成过程
    深入理解PHP原理之变量生命期(一)
    Array dereferencing
    如何获取一个变量的名字
    如何获取一个变量的名字中提到了活动符号表
    而在PHP中, 所有的变量都存储在称为”符号表”的HastTable结构中. 在解析执行的过程中, 依旧保留着着”符号”信息, 所以, 肯定是可以获取到的.

    变量的使用

    而在PHP中, 符号的作用域是和活动符号表相关联的. 同一时间, 只有一个活动符号表.

    那么怎么理解活动符号表和符号表呢?

    对于PHP来说, 当前活动的符号表是保存在全局变量EG(active_symbol_table)中的, 而于此同时, 还有个全局符号表保存在EG(symbol_table)中, 在进入一个函数调用的执行体之前, 会生成一个新的active_symbol_table, 并且会保持一个调用栈式样的符号表栈:EG(symtable_cache), 以便在退出函数调用的时候, 恢复之前的活动符号表(作用域).

    同时在PHP中, 不能实现作用域继承, 也就是不能直接访问作用域外层的符号(需要加上golbal声明), 而如果加上global的声明的话, 也会在当前的活动作用域生成一个copy, 也就是说, 不存在在当前作用域可见的符号是保存在全局符号表的

  3. 函数

    深入理解PHP原理之函数(Introspecting PHP Function)
    函数分为两种zend_internal_function(对应结构体_zend_internal_function)和用户自定义函数(对应结构体_zend_op_array)。还有一个结构体zend_function.
    首先你要理解他的设计目标: zend_internal_function, zend_function,zend_op_array可以安全的互相转换(The are not identical structs, but all the elements that are in “common” they hold in common, thus the can safely be casted to each other);
    具体来说,当在op code中通过ZEND_DO_FCALL调用一个函数的时候,ZE会在函数表中,根据名字(其实是lowercase的函数名字,这也就是为什么PHP的函数名是大小写不敏感的)查找函数, 如果找到,返回一个zend_function结构的指针(仔细看这个上面的zend_function结构), 然后判断type,如果是ZEND_INTERNAL_FUNCTION, 那么ZE就调用zend_execute_internal,通过zend_internal_function.handler来执行这个函数, 如果不是,就调用zend_execute来执行这个函数包含的zend_op_array.

    【todo】现在不明白的是zend_op_array里面到底存的是什么?
    函数类型提示(Callable typehint)
    深入理解PHP之匿名函数

  4. opcode

    深入理解PHP原理之opcodes
    使用PHP embed sapi实现opcodes查看器
    关于PHP的编译和执行分离
    深入理解PHP原理之异常机制
    PHP源码分析之Global关键字
    也就是说, 如果你global了一个变量,那么Zend就会去全局symbol_table去寻找,如果找不到,就会在全局symbol_table中分配相应的变量。通过这样的机制,实现了全局变量

  5. 对象

    深入理解PHP原理之对象(一)
    PHP5多重继承顺序的bug

  6. 数组

    关于一笔试题(Iterator模式)
    深入理解PHP原理之foreach
    PHP中的Hash算法
    深入理解PHP之数组(遍历顺序)

  7. 扩展

    扩展PHP(一)
    关于PHP扩展开发的一些资源
    用C/C++扩展你的PHP
    保证PHP扩展的依赖关系
    深入理解PHP原理之扩展载入过程

  8. 内存管理

    PHP原理之内存管理中难懂的几个点
    深入理解PHP内存管理之谁动了我的内存

  9. SAPI
    深入理解ZendSAPIs
    垃圾回收机制
    第二节 SAPI概述
    fastcgi
    嵌入式sapi

  10. 其他
    深入理解PHP原理之错误抑制与内嵌HTML
    PHP Performance Optimization
    关于PHP浮点数你应该知道的
    PHP浮点数的一个场景问题的解答
    深入理解PHP原理之文件上传
    PHP受locale影响的函数
    字符编码详解
    isset与is_null的不同
    isset是语句 is_null是函数,判断是否为null时可以使用===
    深入理解ob_flush和flush的区别
    在php中使用协程实现多任务调度
    使用fsock实现异步调用php
    更简单的重现PHP core调用栈
    Zend Signal in PHP5.4
    一些PHP Coding Tips

  11. Dtrace
    Dtrace
    php骇客指南

php一些bug列表

在读Laruence的文章时,有很多好的文章。
有一些不会出现或者出现概率很低的文章,现在将读过的文章,自己记录一下,方便查阅

  1. PHP5.2x + APC的一个bug定位

    出现原因是:session模块和apc模块加载顺序和模块关闭顺序导致。
    模块载入顺序和模块关闭函数很有关系了. 总体来说, 就是PHP会根据模块载入的顺序的反序来在每次请求处理结束后依次调用各个扩展的请求关闭函数.

    因为我们环境的Session是静态编译进PHP的, 所以Session模块一定先于动态编译进PHP的APC被载入, 也就是说, 在请求关闭时期, APC的请求关闭函数, 一定会先于Session的请求关闭函数被调用.

    APC在模块请求关闭函数时期, 清空了执行全局标量中的类定义表EG(classs_table),当Session的请求关闭函数调用的时候, 执行环境的Class Table已经为空, 当然也就会抛出类找不到的fatalerror了。

  2. 一个低概率的PHP CoreDump

    出现原因:php正在出错处理函数中,这个时候php execute limit time信号到来被响应,再次载入php_error_cb函数,就会出现。

    自己尝试:在php5.5环境下复现

  3. PHP stream未能及时清理现场导致Core的bug

php中程序加载一个不停变化的文件出现bus error

出现原因

程序加载一个不停在变化的文件,会出现bus error

原因

通过gdb发现在进行语法解析时,内存越界

php环境

Configure Command => ‘./configure’ ‘–prefix=/tmp/php56’ ‘–enable-fpm’ ‘-enable-debug’
没有opcache,apc等缓存。即bus error的出现于opcode缓存没有关系

出现bus error的代码

//文件名 parse.php
<?php

if ($argv[1] > 0) {
    while ($argv[1]--) {
        file_put_contents('test.tpl', "<?php #".str_repeat('A', mt_rand(4000, 5000))." ?>\n", LOCK_EX);
    }
} else {
    $p2 = popen("php parse.php 100", "r");
    while (1) {
        include 'test.tpl';
    }
}

执行 $php parse.php 会出现:

[1]    17776 bus error (core dumped)  php parse.php

coredump调试

命令:gdb php core-php.15285
现象:

balabala...

Core was generated by `php parse.php'.
Program terminated with signal 7, Bus error

(gdb) bt

#0  0x00000000007f39a7 in lex_scan (zendlval=0x7fffc794b888) at Zend/zend_language_scanner.l:1863
#1  0x00000000008309a7 in zendlex (zendlval=0x7fffc794b880) at /home/yankai-c/php-5.6.25/Zend/zend_compile.c:6913
#2  0x00000000007e45ce in zendparse () at /home/yankai-c/php-5.6.25/Zend/zend_language_parser.c:3732
#3  0x00000000007ed208 in compile_file (file_handle=0x7fffc794bca0, type=2) at Zend/zend_language_scanner.l:586
#4  0x000000000067f21c in phar_compile_file (file_handle=0x7fffc794bca0, type=2)
    at /home/yankai-c/php-5.6.25/ext/phar/phar.c:3371
#5  0x00000000007ed3bb in compile_filename (type=2, filename=0x7f3ea56146a8) at Zend/zend_language_scanner.l:629
#6  0x000000000089a120 in ZEND_INCLUDE_OR_EVAL_SPEC_CONST_HANDLER (execute_data=0x7f3ea55db330)
    at /home/yankai-c/php-5.6.25/Zend/zend_vm_execute.h:2988
#7  0x0000000000893367 in execute_ex (execute_data=0x7f3ea55db330)
    at /home/yankai-c/php-5.6.25/Zend/zend_vm_execute.h:363
#8  0x00000000008933ea in zend_execute (op_array=0x7f3ea5611d88)
    at /home/yankai-c/php-5.6.25/Zend/zend_vm_execute.h:388
#9  0x000000000084d76c in zend_execute_scripts (type=8, retval=0x0, file_count=3)
    at /home/yankai-c/php-5.6.25/Zend/zend.c:1341
#10 0x00000000007b2a02 in php_execute_script (primary_file=0x7fffc794f3d0)
    at /home/yankai-c/php-5.6.25/main/main.c:2613
#11 0x0000000000904f71 in do_cli (argc=2, argv=0x2d9ecf0) at /home/yankai-c/php-5.6.25/sapi/cli/php_cli.c:994
#12 0x000000000090602e in main (argc=2, argv=0x2d9ecf0) at /home/yankai-c/php-5.6.25/sapi/cli/php_cli.c:1378

(gdb) p (*(struct _zval_struct*)0x7fffc794b888)

$1 = {value = {lval = 139907039412224, dval = 6.9123261784937185e-310, str = {
      val = 0x7f3ea562c000 <Address 0x7f3ea562c000 out of bounds>, len = 6}, ht = 0x7f3ea562c000, obj = {
      handle = 2774712320, handlers = 0x6}, ast = 0x7f3ea562c000}, refcount__gc = 2673624600, type = 1 '\001',
  is_ref__gc = 127 '\177'}

(gdb) p (*(znode*)0x7fffc794b880).u.constant.value.str.val

$26 = 0x7f3ea562c000 <Address 0x7f3ea562c000 out of bounds>

说明:最后对zendlex的参数进行打印,可以看到提示内存越界。

bus error文章中提到,出现bus error的原因是加载了损坏的文件(broken file)。对于平时编程而言,加载损坏的文件是不能接受的。

参考文章

bus error

php如何生成coredump

PHP如何生成GDB的backtrace

使用lnmp环境时,可能会出现502。502的原因很多,其中一种是php-fpm出现了段错误,可以通过fpm日志(在php-fpm.conf中的error_log设置)查看。

重要

要想使用backtrace获得正确消息,编译php的时候使用参数 --enable-debug

如何设置,才能生成coredump文件

  1. 第一步设置linux,使linux能够生成core

    $ su -
    $ echo '/tmp/core-%e.%p' > /proc/sys/kernel/core_pattern
    $ echo 0 > /proc/sys/kernel/core_uses_pid
    $ ulimit -c unlimited
    
  2. 第二步设置php-fpm,使php-fpm生成core

    $ vim /usr/local/php/etc/php-fpm.conf
    

    修改配置

    ``rlimit_core = unlimited```
    

    重启php-fpm sudo /etc/init.d/php-fpm restart

  3. 确认coredump

    如果在php-fpm的error_log日志中看到类似下面的日志就代表生成了coredump,那么在/tmp/目录下就会有coredump文件
    [05-Jun-2014 06:21:12] WARNING: [pool www] child 631273 exited on signal 11 (SIGSEGV - core dumped) after 20.263546 seconds from start

  4. 读取backtrace

    使用下面语法来运行gdb

    ``gdb /usr/local/php/sbin/php-fpm /tmp/core-php-fpm.1230``
    

    可以运行bt命令获得更加详细的输出

    ``(gdb) bt``
    

参考文章

Generating a gdb backtrace
geberating core-dump for php5-fpm
如何调试PHP的Core之获取基本信息