10 July 2016

本文参考自PHP Wiki: Extensions

数据结构区别


通常在php.ini中,通过extension=*加载的扩展我们称为PHP扩展,通过zend_extension=*加载的扩展我们称为Zend扩展,但从源码的角度来讲,PHP扩展应该称为“模块”(源码中以module命名),而Zend扩展称为“扩展”(源码中以extension命名)。

两者最大的区别在于向引擎注册的钩子。少数的扩展,例如xdebug、opcache,既是PHP扩展,也是Zend扩展,但它们在php.ini中的加载方式得用zend_extension=*,具体原因下文会说明。

先来看看两种类型扩展主要的数据结构:

PHP扩展的结构体:

typedef struct _zend_module_entry zend_module_entry;
typedef struct _zend_module_dep zend_module_dep;
 
struct _zend_module_entry {
    unsigned short size;
    unsigned int zend_api;
    unsigned char zend_debug;
    unsigned char zts;
    const struct _zend_ini_entry *ini_entry;
    const struct _zend_module_dep *deps;
    const char *name;
    const struct _zend_function_entry *functions;          /* PHP Functions */
    int (*module_startup_func)(INIT_FUNC_ARGS);             /* MINIT */
    int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);        /* MSHUTDOWN */
    int (*request_startup_func)(INIT_FUNC_ARGS);            /* RINIT */
    int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);       /* RSHUTDOWN */
    void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);
    const char *version;
    size_t globals_size;
#ifdef ZTS
    ts_rsrc_id* globals_id_ptr;
#else
    void* globals_ptr;
#endif
    void (*globals_ctor)(void *global TSRMLS_DC);
    void (*globals_dtor)(void *global TSRMLS_DC);
    int (*post_deactivate_func)(void);
    int module_started;
    unsigned char type;
    void *handle;
    int module_number;
    const char *build_id;
};
 
struct _zend_module_dep {
    const char *name;       /* module name */
    const char *rel;        /* version relationship: NULL (exists), lt|le|eq|ge|gt (to given version) */
    const char *version;    /* version */
    unsigned char type;     /* dependency type */
};

Zend扩展的结构体:

/* Typedef's for zend_extension function pointers */
typedef int (*startup_func_t)(zend_extension *extension);
typedef void (*shutdown_func_t)(zend_extension *extension);
typedef void (*activate_func_t)(void);
typedef void (*deactivate_func_t)(void);
 
typedef void (*message_handler_func_t)(int message, void *arg);
 
typedef void (*op_array_handler_func_t)(zend_op_array *op_array);
 
typedef void (*statement_handler_func_t)(zend_op_array *op_array);
typedef void (*fcall_begin_handler_func_t)(zend_op_array *op_array);
typedef void (*fcall_end_handler_func_t)(zend_op_array *op_array);
 
typedef void (*op_array_ctor_func_t)(zend_op_array *op_array);
typedef void (*op_array_dtor_func_t)(zend_op_array *op_array);
 
typedef struct _zend_extension {
    char *name;
    char *version;
    char *author;
    char *URL;
    char *copyright;
 
    startup_func_t startup;
    shutdown_func_t shutdown;
    activate_func_t activate;
    deactivate_func_t deactivate;
 
    message_handler_func_t message_handler;
 
    op_array_handler_func_t op_array_handler;
 
    statement_handler_func_t statement_handler;
    fcall_begin_handler_func_t fcall_begin_handler;
    fcall_end_handler_func_t fcall_end_handler;
 
    op_array_ctor_func_t op_array_ctor;
    op_array_dtor_func_t op_array_dtor;
 
    int (*api_no_check)(int api_no);
    int (*build_id_check)(const char* build_id);
    void *reserved3;
    void *reserved4;
    void *reserved5;
    void *reserved6;
    void *reserved7;
    void *reserved8;
 
    DL_HANDLE handle;
    int resource_number;
} zend_extension;
 
typedef struct _zend_extension_version_info {
    int zend_extension_api_no;
    char *build_id;
} zend_extension_version_info;

以xdebug 2.4.0为例,来说明为何其既是PHP扩展,也是Zend扩展。

首先看xdebug.c的2710行处,定义了一个Zend扩展的结构体:

ZEND_DLEXPORT zend_extension zend_extension_entry = {
    XDEBUG_NAME,
    XDEBUG_VERSION,
    XDEBUG_AUTHOR,
    XDEBUG_URL_FAQ,
    XDEBUG_COPYRIGHT_SHORT,
    xdebug_zend_startup,
    xdebug_zend_shutdown,
    NULL,           /* activate_func_t */
    NULL,           /* deactivate_func_t */
    NULL,           /* message_handler_func_t */
    NULL,           /* op_array_handler_func_t */
    xdebug_statement_call, /* statement_handler_func_t */
    NULL,           /* fcall_begin_handler_func_t */
    NULL,           /* fcall_end_handler_func_t */
    xdebug_init_oparray,   /* op_array_ctor_func_t */
    NULL,           /* op_array_dtor_func_t */
    STANDARD_ZEND_EXTENSION_PROPERTIES
};

因为xdebug提供了单步调试、性能优化等高级功能,这些是需要hook到Zend引擎才能做到的,而zend_extension这个结构体就提供了hook到Zend引擎的钩子,例如xdebug_statement_call会在每一条PHP语句执行之后调用。

再看xdebug.c的160行处,定义了一个PHP扩展的结构体:

zend_module_entry xdebug_module_entry = {
    STANDARD_MODULE_HEADER,
    "xdebug",
    xdebug_functions,
    PHP_MINIT(xdebug),
    PHP_MSHUTDOWN(xdebug),
    PHP_RINIT(xdebug),
    PHP_RSHUTDOWN(xdebug),
    PHP_MINFO(xdebug),
    XDEBUG_VERSION,
    NO_MODULE_GLOBALS,
    ZEND_MODULE_POST_ZEND_DEACTIVATE_N(xdebug),
    STANDARD_MODULE_PROPERTIES_EX
}

xdebug提供了许多函数xdebug_enable()、xdebug_disable()、xdebug_call_class()……,这些函数都是定义在xdebug_module_entry中的xdebug_functions。

所以,以我的理解,向用户层面提供一些C实现的PHP函数,需要用到zend_module_entry(即作为PHP扩展),而需要hook到Zend引擎的话,就得用到zend_extension(即作为Zend扩展),xdebug在这里两种都需要。

但是xdebug是通过Zend扩展加载的(zend_extension=*),其PHP扩展部分是如何被加载的呢?其实在Zend扩展加载的时候会调用zend_extension中的startup,即xdebug的xdebug_zend_startup函数,在该函数中:

ZEND_DLEXPORT int xdebug_zend_startup(zend_extension *extension)
{
    /* Hook output handlers (header and output writer) */
    xdebug_hook_output_handlers();

    zend_xdebug_initialised = 1;
                                                                                                                                                                                                               
    return zend_startup_module(&xdebug_module_entry);//对xdebug PHP扩展的加载
}

加载顺序区别


分清PHP扩展和Zend扩展的差异后,接着看看扩展是如何加载的,其加载顺序和依赖是怎么处理的:

相关的主要代码片段在main/main.c中:

int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_modules, uint num_additional_modules)
{
    . . . . . .

    /* this will read in php.ini, set up the configuration parameters,
       load zend extensions and register php function extensions
       to be loaded later */
    if (php_init_config(TSRMLS_C) == FAILURE) {
        return FAILURE;
    }

    . . . . . .

    /* startup extensions statically compiled in */
    if (php_register_internal_extensions_func(TSRMLS_C) == FAILURE) {
        php_printf("Unable to start builtin modules\n");
        return FAILURE;
    }

    /* start additional PHP extensions */
    php_register_extensions_bc(additional_modules, num_additional_modules TSRMLS_CC);

    /* load and startup extensions compiled as shared objects (aka DLLs)
       as requested by php.ini entries
       theese are loaded after initialization of internal extensions
       as extensions *might* rely on things from ext/standard
       which is always an internal extension and to be initialized
       ahead of all other internals
     */
    php_ini_register_extensions(TSRMLS_C);
    zend_startup_modules(TSRMLS_C);

    /* start Zend extensions */
    zend_startup_extensions();

    . . . . . .
}

1、php_init_config解析php.ini文件,获取需要加载的PHP扩展和Zend扩展

2、php_register_internal_extensions_func加载静态编译的扩展,静态编译进PHP中的扩展,例如date、ereg、pcre等,这些扩展包含在main/internal_functions.c中的zend_module_entry *php_builtin_extensions[]中

3、php_register_extensions_bc注册SAPI的扩展模块,即additional_modules中的扩展,例如Apache SAPI会注册一些与Apache功能相关的扩展,CLI模式下additional_modules为NULL

4、php_ini_register_extensions中会先加载Zend扩展,之后再加载PHP扩展

void php_ini_register_extensions(TSRMLS_D)
{
    zend_llist_apply(&extension_lists.engine, php_load_zend_extension_cb TSRMLS_CC);//加载Zend扩展
    zend_llist_apply(&extension_lists.functions, php_load_php_extension_cb TSRMLS_CC);//加载PHP扩展

    zend_llist_destroy(&extension_lists.engine);
    zend_llist_destroy(&extension_lists.functions);
}

Zend扩展的加载(php_load_zend_extension_cb->zend_load_extension):

在zend_load_extension中会判断扩展是否合法(能否加载?版本信息是否符合要求?……),如果合法的话将调用zend_register_extension,向全局变量zend_extensions添加当前Zend扩展,并向其他已加载的Zend扩展广播一条消息(实现了message_handler的Zend扩展将接收到)说明自己被加载了,该功能的用处在于,如果某些扩展存在冲突,则扩展能够发出警告信息,并停止加载。

PHP扩展的加载(php_load_php_extension_cb->php_load_extension):

在php_load_extension中会判断扩展是否合法(能否加载?版本信息是否符合要求?……),如果合法的话将调用zend_register_module_ex,先检查扩展的依赖(_zend_module_entry中的deps),这里只检查当前扩展是否与已加载的扩展冲突,如果没有冲突,则向全局变量module_registry添加当前PHP扩展,接着注册当前PHP扩展的函数(_zend_module_entry中的functions)

这里需要注意的点是deps依赖,该依赖声明了:

  • 当前扩展会与哪些扩展冲突
  • 哪些扩展需要先于当前扩展加载

但是zend_register_module_ex中只对冲突做检查,如果A声明自己与B冲突,但是B却在A之后加载,那么就无法检查到该冲突了。为了解决这个问题,可以在php.ini中排好PHP扩展的顺序(即B放到A之前),加载的时候会按照该顺序加载。

5、扩展初始化阶段:

先激活PHP扩展,在zend_startup_modules中,会先对PHP扩展进行排序(根据每个PHP扩展中deps声明的依赖),然后执行zend_startup_module_ex,调用PHP扩展的MINIT

再激活Zend扩展,在zend_startup_extensions中,对每个Zend扩展调用其startup()

6、请求初始化阶段:

先调用Zend扩展的activate():php_request_startup->zend_activate->init_executor->zend_extension_activator->activate

再调用PHP扩展的RINIT:php_request_startup->zend_activate_modules->request_startup_func

7、请求结束阶段:

先调用PHP扩展的RSHUTDOWN:php_request_shutdown->zend_deactivate_modules->request_shutdown_func

再调用Zend扩展的deactivate:php_request_shutdown->zend_deactivate->shutdown_executor->zend_extension_deactivator->deactivate

8、扩展关闭阶段:

先调用PHP扩展的MSHUTDOWN:zend_shutdown->zend_destroy_modules->zend_hash_graceful_reverse_destroy->module_destructor->module_shutdown_func

再调用Zend扩展的shutdown():zend_shutdown->zend_shutdown_extensions->zend_extension_shutdown->shutdown

一张图了解扩展的生命周期


图片来自参考文档