php 源码阅读

4,912 阅读11分钟
原文链接: ghost.icosplay.cc
目录 描述
ext 这是存放动态内建模块的目录, 在这里可以找到所有的PHP官方扩展, 并且以后也会在这里编写扩展
main 这里包含PHP的主要宏定义
pear 该目录就是 "PHP扩展与应用库" 目录, 包含 PEAR 核心文件
sapi 包含不同服务器抽象层的代码
TSRM Zend 和 PHP 的 "线程安全资源管理器" 目录
Zend 包含 Zend 引擎的所有文件。在这里你可以找到所有的 Zend API 定义和宏等

PHP 源代码的一些主要目录及描述

还有几个重要的头文件, 在编写扩展的过程中, 一般要把这些文件包含进来:

  • main/php.h: 位于 main 目录下。包含了绝大部分PHP 宏及PHP API定义。
  • Zend/zend.h: 位于 Zend 目录下。包含了绝大部分Zend宏及Zend API定义。
  • Zend/zend_API.h: 位于Zend 目录下。包含Zend API的定义。

    ext_skel 工具

    PHP 为编写PHP扩展提供的一个很好用的 自动构建系统 工具 extskel。使用 extskel 可以很方便的搭建一个扩展框架。

    extskel 工具位于 ext 目录下。ext 目录下有两个文件, extskel 和 extskelwin32.php。第一个在 Linux 系统中使用, 第二个则在 windows 系统下使用

    PHP 的生命周期

    一个php实例, 无论是从 init 脚本中调用, 还是从命令行启动, 都会依次jingguo Module initRequest initRequest shutdownModule shutdown 四个过程。

    最常见的四种启动 PHP 的方式如下:

    • 直接以 CLI/CGI 模式调用
    • 多进程模块
    • 多线程模块
    • Embedded(嵌入式, 在自己的C程序中调用 Zend Engine)

SAPI (Server abstraction API, 服务器抽象化程序接口) 提供一个接口, 使得 PHP 可以和其他应用进行交互数据。也就是说, PHP 能够跟其他程序 (如 Apache) 交互就是这个接口起的作用。

在命令行模式运行一个 PHP 的主要流程如下:

$ php -f test.php

  1. Call each extension's MINIT

    这个过程在扩展被载入时调用

 PHP_MINIT_FUNCTION(myext)
 {
     // 注册常量或者类等初始化操作
     return SUCCESS;
 }
  1. Request test.php

    请求 test.php 文件。当请求到达后, PHP 会自动初始化执行脚本的基本环境,例如创建一个执行环境, 包括保存 PHP 运行过程中变量名称和变量值内容的符号表, 以及当前所有的函数以及类等信息的符号表。然后 PHP 会调用所有模块 RINIT 函数, 在这个阶段各个模块也可以执行一些相关的操作, 模块的 RINIT 函数和 MINIT 函数类是:

 PHP_RINIT_FUNCTION(myext)
 {
     /* 例如记录请求开始时间 */
     /* 随后在请求结束的时候记录结束时间 */
     /* 这样就能够记录下处理请求所花费的时间 */
     return SUCCESS;
 }
  1. Excute test.php

    执行 test.php 阶段, 主要把 PHP 文件编译成 Opcods, 然后在 PHP 虚拟机下执行。

  2. Call exch extension's RSHUTDOWN

    请求处理完成后, 一般脚本执行到行末尾或者通过调用 exit() 或者 die() 函数, PHP 都将进入结束阶段。和开始阶对应, 结束阶段也分为两个环节, 一个在请求结束后(RSHUTDOWN), 一个在 SAPI 生命周期结束时(MSHUTDOWN)。

    RSHUTDOWN 类似如下:

 PHP_RSHUTDOWN_FUNCTION(myext)
 {
     /* 例如记录请求结束时间, 并把相应的信息写入到日志文件中 */

     return SUCCESS; 
 }

MSHUTDOWN 类似如下:

 PHP_MSHUTDOWN_FUNCTION(myext)
 {
     /* 注销一些持久化的资源 */
     return SUCCESS;
 }

PHP 内核中的变量

PHP 是弱类型语言, 也就是一个 PHP 变量可以保存任何数据类型, 但是 PHP 是使用C语言编写的, 而 C语言是强类型语言, 每个变量都有固定类型, 不能随意改变变量的类型(可以通过强类型转换改变, 不过可能出现问题)。

打开 Zend/zend.h头文件, 会发现以下一些结构体:

 typedef struct _zval_struct zval;

 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;

 struct _zval_struct {
     /* Variable information */
     zvalue_value value;    /* value */
     zend_uint refcount;
     zend_uchar type;       /* active type */
     zend_uchar is_ref;
 }

zval 结构体就是通常用到的 PHP 变量在内核中的表示方式, 在zval 结构体中, 可以看到 4 个成员变量, 分别是:

  • zend_value value: 变量的值
  • zend_uint refcount: 变量引用数, 变量引用计算器
  • zend_uchar type: 变量的类型
  • zenduchar isref: 变量是否被引用

    zval 结构体的 value 成员变量是一个 zvaluevalue 联合体, PHP 能够保持任何的结构类型就是因为这个联合体。从 zvaluevalue 联合体的成员变量可以看到, 不同的类型会保存到不同的成员变量中, 这样就实现了 PHP 变量可以存储任何数据类型。

不同类型对应的成员变量
PHP 语言层类型 保存在 zvalue_value 的成员变量
long, bool, resource lval
double dval
string str (len 保存字符串的长度, val 保存字符串的值)
array ht
object obj

zval 结构体的type成员变量保存了一个php变量的类型

Zend 引擎定义了几种变量类型

  • #define IS_NULL 0
  • #define IS_LONG 1
  • #define IS_DOUBLE 2
  • #define IS_STRING 3
  • #define IS_ARRAY 4
  • #define IS_OBJECT 5
  • #define IS_BOOL 6
  • #define IS_RESOURCE 7
宏定义对应的 PHP 类型
宏定义 表示类型
IS_NULL NULL 类型(null)
IS_LONG 整数类型(int)
IS_DOUBLE 浮点型类型(float)
IS_STRING 字符串类型(string)
IS_ARRAY 数组类型(array)
IS_OBJECT 对象类型(object)
IS_BOOL 布尔类型(bool)
IS_RESOURCE 资源类型(resource)
// 打印一个zval 的类型

switch(zval.type) {  
    case IS_NULL:
        php_printf("zval type is null\n");
        break;
    case IS_STRING:
        php_print("zval type is string\n");
        break;
    case IS_LONG:
        php_print("zval type is long\n");
        break;
    case IS_ARRAY:
        php_print("zval type is array\n");
        break;
    ...
}

使用 zval.type 可以访问或者设置一个变量的类型, 不过这样做不合适, 因为不能预测 PHP 以后的版本是否会发生改变, 可能以后的版本中, type 成员变量变成了 type_gc 或者其它名字。

PHP 内核提供了一个访问和设置变量类型的方法:

Z_TYPE(zval)            对应 zval 结构体的实体  
ZTYPE_P(&zval)          对应 zval 结构体的指针  
Z_TYPE_PP(&&zval)       对应 zval 结构体的二级指针

设置变量类型:

// 方式1(不推荐)
zval.type = IS_LONG;

//方式2(推荐使用)
Z_TYPE(zval) = IS_LONG;  

访问变量类型:

if (Z_TYPE(zval) == IS_LONG) {  
    printf("is long\n");
}

与变量类型一样, 变量的值也有相应的访问宏定义:

类型 访问宏
整数类型 Z_LVAL(zval)
- Z_LVAL_P(&zval)
- Z_LVAL_PP(&&zval)
浮点类型 | Z_DVAL(zval)
- Z_DVAL_P(&zval)
- Z_DVAL_PP(&&zval)
布尔类型 | Z_BVAL(zval)
- Z_BVAL_P(&zval)
- Z_BVAL_PP(&&zval)
字符串类型 | 取得值:
- Z_STRVAL(zval)
- Z_STRVAL_P(&zval)
- Z_STRVAL_PP(&&zval)
- 取得长度:
- Z_STRLEN(zval)
- Z_STRLEN_P(&zval)
- Z_STRLEN_PP(&&zval)
数组类型 Z_ARRVAL(zval)
- Z_ARRVAL_P(&zval)
- Z_ARRVAL_PP(&&zval)
资源类型 Z_RESVAL(zval)
- Z_RESVAL_P(&zval)
- Z_RESVAL_PP(&&zval)
// 创建一个值为10的整数变量
zval lvar;  
Z_TYPE(lvar) = IS_LOGIN;  
Z_LVAL(lval) = 10;

相当与php脚本:

$lvar = 10;

zval 结构中以下两个成员变量用于引用计数器:

  • is_ref: BOOL 值, 标识变量是否是引用集合
  • refcount: 计算指向引用集合的变量个数

一个 zval 结构的实体称为 zval 容器。在 PHP 语言层创建一个变量就会相应地在 PHP 内核中创建一个 zval 容器。

$a = "this is variable";

由于 $a 不是一个引用, 所以 zval 容器的 is_ref 等于 FALSE, 并且 refcount 等于1

<?php  
$a = "this is variable";
$b = $a;

上面代码创建了两个变量, 所以 PHP 内核会创建两个 zval 容器来保存它们。

由于变量 $b 并不是引用变量 $a, 所以变量 $a 的 is_ref 字段的值为 FALSE, 但是使用 xdebug 打印变量 $a, 会发现 refcount 也等于2, 关于为什么是 2, 要先了解 PHP 的写时复制(copy on write) 机制。

写时复制是一个解决内存复用的方法

当变量的值改变时才进行内存的复制, 就是写时复制

<?php

$a = "this is a variable";
xdebug_debug_zval('a');

$b = $a;
xdebug_debug_zval('a');

$a = "changed value";
xdebug_debug_zval('a');

// 输出

a:  
(refcount=1, is_ref=0), string 'this is variable' (length=16)

a:  
(refcount=2, is_ref=0), string 'this is variable' (length=16)

a:  
(refcount=1, is_ref=0), string 'changed value' (length=13)

当将变量 $a 的值赋予变量 $b 时, 变量 $a 的 refcount 增加1, 所以这时候变量 $a 和 变量 $b 指向同一内存块。当改变变量 $a 的值时, $a 的 refcount 变回1, 所以这时候两个变量指向不同的内存块, 这就是写时复制机制

$a = 1;
xdebug_debug_zval('a');

$b = & $a;
xdebug_debug_zval('a');

$b += 5;
xdebug_debug_zval('a');

// 输出

a:  
(refcount=1, is_ref=0), int 1

a:  
(refcount=2, is_ref=1), int 1

a:  
(refcount=2, is_ref=1), int 6

当显示地让一个变量引用另外一个变量时, 变量的 is_ref 字段会设置成1, 表示此变量被引用, 另外引用计数器(refcount) 也增加1。

php 内核通过以下代码判断是否复制变量:

if ((*varval)->is_ref || (*varval)->refcount<2) {  
    return *varval;
}

PHP 内核中的 HashTable 分析

Zend 引擎中大量使用了 HashTable, 如变量表、常量表、函数表等, 这些都是在 HashTable 中保存的, PHP 的数组也是使用 HashTable 实现的。了解 PHP 的 HashTable 才是真正的了解 PHP。

PHP 内涵 HashTable 的数据结构

PHP 中的 HashTable 实现代码保存在 Zend/zend_hash.hZend/zend_hash.c 两个文件中。

HashTable 数据结构定义(Zend/zend_hash.h):

typedef struct bucket {  
    ulong h;                    /* 用于存储 key 的 Hash 值 */
    uint nKeyLength;
    void *pDat;
    void *pDataPtr;
    struct bucket *pListNext;
    struct bucket *pListLast;
    struct bucket *pNext;
    struct bucket *pLast;
    char arKey[1];              /* Must be last element */

} Bucket;

typedf struct _hashtable {  
    uint nTableSize;
    uint nTableMask;
    uint nNumOfElements;
    ulong nNextFreeElement;
    Bucket *pInternalPointer;   /* Used for element traversal */
    Bucket *pListHead;
    Bucket *pListTail;
    Bucket **arBuckets;
    dtor_func_t pDestructor;
    zend_bool persistent;
    unsigned char nApplyCount;
    zend_bool bApplyProtection;
}

Bucket(桶) 和 HashTable 两个结构体构成一个完整的 HashTable。

Bucket 结构体
  • ulong h: 保存经过 hash 函数处理之后的 hash 值
  • uint nKeyLength: 保存索引 (key) 的长度
  • void *pData: 指向要保存的内存地址
  • void *pDataPtr: 保存指针数据
  • struct bucket *pListNext: 指向双向链表的下一个元素
  • struct bucket * pListLast: 指向双向链表的上一个元素
  • struct bucket *pNext: 指向具有同一个 hash 值的下一个元素
  • struct bucket * pLast: 指向具有同一个 hash 值的上一个元素

PHP 的 HashTable 由 pListNext 和 pListLast 这两个成员变量同时维护了两个双向链表。

pData 指向的是想要保存的内存块地址, 一般是通过 malloc 之类的系统调用分配出来。但是有时候只想保存一个指针, 如果也去调用 malloc 分配内存, 就会造成很多细小的内存快,从而导致产生内存碎片。

pDataPtr 的作用就是当想要保存的数据是一个指针类型的数据时, 就直接保存在 pDataPtr 成员变量中, 而不调用 malloc 分配内存, 从而防止内存碎片产生。然后pData 直接指向 pDataPtr, 当相应保存的数据不是指针时, pDataPtr 被设置成 NULL。

arKey 用来保存数据的索引 (key) , 每个元素都有一个索引, 且彼此不同。通过这个索引可以找到这个元素, 并且取得保存在里面的数据, 但是 arKey 只有一个字节, 如果索引不止一个字节, PHP 使用 C 语言的一个常用技巧(flexible array), 通过是 sizeof(Bucket) + nKeyLength 大小的内存(nKeyLength 就是索引的长度), 然后把索引保存到 arKey 成员中, 而 nKeyLength 保存索引的长度。

当索引是一个蒸熟时, PHP 把索引保存到 Bucket 结构提的 h 成员变量中, 然后把 nKeyLength 设置为0, 表示这个索引是一个数字, 而不是一个字符串。

当 nKeyLength 大于0时, 可以在 arKey 中取得索引, 而 nKeyLength 等于0时, 就在 h 中取得索引。当 nKeyLength 大于0时(也就是索引是字符串时), h 成员保存的是索引经过 hash 函数处理后的值

HashTable 结构体

一个 Bucket 只能保存一个数据, 而 HashTable 的目的就是通过索引 (key) 把每一个元素分散到一个唯一的位置 (当没有冲突时), HashTable 通过 hash 算法把索引处理成一个 int 整数, 然后定位到一个 Bucket 数组中的其中一个元素中。

一个字符串的索引经过 hash 函数处理之后会返回一个 int 索引定位到 Bucket 数组中的其中一个元素。这就是 HashTable 的原理和实现方法。

HashTable 结构体主要成员变量的作用:

  • unit nTableSize: 记录 Bucket 数组的大小。
  • unit nNumOfElements: 记录 HashTable 中元素个数
  • unit nNextFreeElements: 下一个可用 Bucket 位置
  • Bucket *pInternalPointer: 遍历 HashTable 元素
  • Bucket *pListHead: 双链表表头
  • Bucket *pListTail: 双链表表尾
  • Bucket **arBuckets: Bucket 数组
// PHP 内核通过一个字符串索引定位到 Bucket 数组
h = hash(key);  
pos = h & nTableSize;  
bucket = arBuckets[po];  

hash 就是一个把字符串处理成整数的函数:

int hash(char *key) {  
    int h =0;
    char *p = key;

    while(*p) {
        h += *p;
        p ++;
    }

    return h;
}

nNumOfElement 记录 HashTable 中保存元素的个数, 和 nTableSize的区别在于, nNumOfElements 只记录有数据的元素, nTableSize 记录不会去管元素中是否有数据保存, 只是单单记录元素的个数。

pListHead 和 pListTail 发双向链表的表头和表尾指针。传统的 HashTable 是不会同时维护双向链表的, PHP 这样做主要是用于有序遍历 HashTable 里的元素。

HashTable 的代码实现

HashTable 的初始化

PHP 为 HashTable 的初始化提供了一个接口 zendhashinit, 这个接口主要是把 HashTable 结构体的成员变量初始化, 并且初始化桶数组(arBuckets)

// zend_hash_init 代码实现, 摘自 Zend/zend_hash.c文件
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)  
{
    init i = 3;
    Bucket **tmp;

    SET_INCONSISTENT(HT_OK);

    while ((1U << i) < nSize) {
        i++;
    }

    ht->nTableSize = 1 << i;
    ht->nTableMask = ht -> nTableSize -1;
    ht->pDestructor = pDestructor;
    ht->arBuckets = NULL;
    ht->pListHead = NULL;
    ht->pListTail = null;
    ht->nNumOfElements = 0;
    ht->nNextFreeElement = 0;
    ht->pInternalPointer = NULL;
    ht->persistent = persistent;
    ht->nApplyCount = 0;
    ht->bApplyProtection = 1;

    /* Uses ecalloc() so that Buckets* == NULL */

    if (persistent) {
        tmp = (Bucket **) calloc(ht->nTableSize, sizeof(Bucket *));
        if (!tmp) {
            return FAILURE;
        }
        ht->arBuckets = tmp;
    } else {
        tmp = (Bucket **) ecalloc_rel(ht->nTableSize, sizeof(Buckets *));

        if (tmp) {
            ht->arBuckets = tmp;
        }
    }

    return SUCCESS;
}

参数 nSize 是要申请的桶数组的大小, 但这不是 PHP 实际申请的大小, 因为 PHP 内部会计算出一个不小于 nSize 并且是 2 的 n 次方的数作为实际申请的大小。

参数 persistent 判断是否使用 PHP 内存管理, 如果作为 FALSE 就是使用操作系统的内存管理, 否则就是使用 PHP 的内存管理, 一般设置为 TRUE, 因为使用操作系统内存管理需要自己释放内存, 容易造成内存泄漏, 所以最好是交给 PHP 去管理内存。

HashTable 的插入操作

插入数据的第一步, 就是找到这个元素应该存放到 arBuckets 数组的哪个位置, 因为要确保索引在 HashTable 中是唯一的, 所以插入前, 需要先比较相同 hash 值的链表上的所有元素是否已经存在此索引, 不存在才插入到 HashTable 中。

ZEND_API int _zend_hash_add_or_update(HashTable *ht, char *arKey, uint nKeyLength, void *pData, uint nDataSize, void **pDest, int flag ZEND_FILE_LINE_DC)  
{
    ulong h;
    uint nIndex;
    Bucket *p;

    IS_CONSISTENT(ht);

    if (nKeyLength <=0) {
        return FAILURE;
    }

    h = zend_inline_hash_func(arKey, nKeyLength);
    nIndex = h & ht->nTableMask;

    p = ht->arBuckets[nIndex];
    while (p != NULL) {
        if ((p->h == h) && (p->nKeyLength == nKeyLength)) {
            if (flag & HASH_ADD) {
                return FAILURE;
            }

            HANDLE_BLOCK_INTERRUPTIONS();
            if (ht->pDestructor) {
                ht->pDestructor(p->pData);
            }

            UPDATE_DATA(ht, p, pData, nDataSize);
            if (pDest) {
                *pDest = p->pData;
            }

            HANDLE_UNBLOCK_INTERRUPTIONS();
            return SUCCESS;
        }
    }
    p = p->pNext;
}

p = (Bucket *) pemalloc (sizeof(Blucket) -1 + nKeyLength, ht->persistent);

if (!p) {  
    return FAILURE;
}

memcpy(p->arKey, arKey, nKeyLength);  
p->nKeyLength = nKeyLength;  
INIT_DATA(ht, p, pData, nDataSize);  
p->h = h;  
CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex]);

if (pDest) {  
    *pDest = p->pData;
}

HANDLE_BLOCK_INTERRUPTIONS();  
CONNECT_TO_GLOBAL_DLLIST(p, ht);  
ht->arBuckets[nIndex] = p;  
HANDLE_UNBLOCK_INTERRUPTIONS();

ht->nNumOfElements ++;  
ZEND_HASH_IF_FULL_DO_RESIZE(ht);        /* 如果 Hash Table 的桶(buckets) 满了, 扩大桶的大小 */  
return SUCCESS;

Zend 引擎内存管理

接口 描述
emalloc(size_t size) 替代 malloc, 申请一块大小为 size 的内存快
efree(void *ptr) 替代 free, 释放 ptr 所指向的内存块
estrdup(char *str) 替代 strdup, 申请一块跟 str 字符串一样大小的内存块, 并复制 str 字符串到新的内存块中
estrndup(char *str, int slen) 替代 strndup, 跟 estrdup 差不多, 不过此函数需要指定字符串的长度。此外, estrndup 函数比 estrdup 要快并且是二进制安全的
ecalloc (size_t numOfElem, size_t sizeOfElem) 替代 calloc, 申请 numOfElem 份大小为 sizeOfElem 的内存块(也就是申请大小为 numOfElem * sizeOfElem 的内存块), 并把所有位置为0
erealloc(void *ptr, size_t nsize) 替代 realloc, 把ptr 指向的内存块的大小扩展到nsize
常用内存管理接口

上面提到的内存管理函数所有申请的内存仅对当前本地的请求有效, 并且会在脚本执行完毕, 处理请求终止时释放。

PHP 扩展的架构

#include "php.h"
#include "php_myext.h"

static int le_myext;

PHP_FUNCTION(confirm_myext_compiled);

zend_function_entry myext_functions[] = {  
    PHP_FE(confirm_myext_compiled, NULL) {
        NULL, NULL, NULL
    }
};

zend_module_entry myext_module_entry = {  
    STANDARD_MODULE_HEADER,
    "myext",
    myext_functions,
    PHP_MINIT(myext),
    PHP_MSHUTDOWN(myext),
    PHP_RINIT(myext),
    PHP_RSHUTDOWN(myext),
    PHP_MINFO(myext),
    "0.1",
    STANDARD_MODULE_PROPERTIES
};

#ifdef COMPILE_DL_MYEXT
ZEND_GET_MODULE(myext)  
#endif

PHP_MINIT_FUNCTION(myext)  
{
    return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(myext)  
{
    return SUCCESS;
}

PHP_RINIT_FUNCTION(myext)  
{
    return SUCCESS;
}

PHP_RSHUTDOWN_FUNCTION(myext)  
{
    return SUCCESS;
}

PHP_MINFO_FUNCTION(myext)  
{
    php_info_print_table_start();
    php_info_print_table_header(2, "myext support", "enable");
    php_info_print_table_end();
}

PHP_FUNCTION(confirm_myext_compiled)  
{
    char *arg = NULL;
    int arg_len, len;
    char *strg;

    if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) == FAILURE 
    ) {
        return;
    }

    len = spprintf(&strg, 0, "Hello %s, %s", "myext", arg);
    RETURN_STRINGL(strg, len, 0);
}

编写扩展的目的是能够让 PHP 调用扩展中的函数和类, 声明和实现导出函数才能让PHP脚本调用。

导出函数的参数及作用
参数 描述
ht 保存扩展函数参数的个数。但不应该直接访问这个值, 而是通过 ZEND_NUM_ARGS() 宏获取参数的个数。如 PHP 脚本代码 showmessage($arg1, $arg2, $arg3) 有三个参数, ht 等于3
return_value 用来保存扩展函数向 PHP 脚本返回的值 (访问这个变量最佳的方式也是使用一些列宏)
this_ptr 根据这个参数可以访问该函数所在对象( 换句话说, 此时这个函数应该是一个类的 "方法" )。推荐使用函数 getThis() 得到这个值
Executor_globals 指向 Zend 引擎的全局设置, 在创建新变量时很有用, 在函数中使用 TSRMLS_FETCH() 引用这个值
需要通过zendfunctionentry结构体来把编写的函数引入到 Zend 引擎
typedef struct _zend_function_entry {  
    char *fname;
    void (*handler) (INTERNAL_FUNCTION_PARAMETERS);
}
zendfunctionentry 结构体成员变量及作用
字段 描述
fname 制订在php脚本里调用的函数名(比如fopen、mysql_connect、fsocketopen等)
handler 指向对应 c 函数的句柄, 就是声明的导出函数句柄
func_arg_types 标识一些参数是否要强制性地按引用方式进行传递。通常应将其设定为 NULL

参考资料

缓存

文件缓存系统: Secache

eAccelerator: opcode 缓存工具

VLD 扩展: 检测php脚本的执行情况