php源码-扩展是如何加载注册的

字数 564阅读 109

看过PHP源码或者扩展开发相关资料的都知道PHP扩展的几个关键函数,或者叫生命周期
PHP_MINIT
PHP_RINIT
PHP_RSHUTDOWN
PHP_MSHUTDOWN

其中PHP_MINIT 是php启动的时候加载扩展的时候会调用的函数 , 这个宏展开后其实真的就是定义了一个这样的C函数

zm_startup_##module(...){...}

那么这个在扩展的代码里定义的C函数是如何被执行的呢? 接下来,我们还会发现,每个扩展都有一个zend_module_entry 的结构体定义

例如swoole的扩展 zend_module_entry 定义如下

zend_module_entry swoole_module_entry =
{
#if ZEND_MODULE_API_NO >= 20050922
    STANDARD_MODULE_HEADER_EX,
    NULL,
    NULL,
#else
    STANDARD_MODULE_HEADER,
#endif
    "swoole",
    swoole_functions,
    PHP_MINIT(swoole),
    PHP_MSHUTDOWN(swoole),
    PHP_RINIT(swoole),     //RINIT
    PHP_RSHUTDOWN(swoole), //RSHUTDOWN
    PHP_MINFO(swoole),
    PHP_SWOOLE_VERSION,
    STANDARD_MODULE_PROPERTIES
};

定义这么一个结构体就加载了吗? 显然是不能的,php源码在编写的时候又不知道你要定义什么扩展。 关键点就在下面的代码,每个扩展还会有这么一句代码

ZEND_GET_MODULE(swoole)

//上面这个宏展开后其实是个函数 , 以swoole为例,这个函数就是返回上面定义的 swoole_module_entry  这个结构体
zend_module_entry *get_module(void) { return &name##_module_entry; }

大概总结下流程
1.扩展会提供一个 get_module(void)的方法拿到扩展的 zend_module_entry 结构体的定义

  1. 扩展被编译成so文件后,在php.ini文件中配置 xxx.so, 表示加载扩展
  2. php 启动的时候会读php.ini 文件,并做解析
    4.在linux下 通过 dlopen()打开扩展的xxx.so库文件
  3. 通过系统的 dlsym()获取动态库中get_module()函数的地址,执行每个扩展的get_module方法拿到 zend_module_entry 结构体
  4. 把zend_module_entry 结构体注册到php的 extension_lists 扩展列表中
    7.在php的生生命周期中执行各个扩展定义的PHP_MINIT

上面是个执行流程的总结,下面进行源码求证

看源码很容易在php启动的时候找到这个函数执行 php_module_startup()

int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_modules, uint num_additional_modules)
{
    ...
    //根据php.ini注册扩展
    php_ini_register_extensions();

    ...
}

动态库就是在php_ini_register_extensions()这个函数中完成的注册:

//main/php_ini.c
void php_ini_register_extensions(void)
{
    //注册zend扩展
    zend_llist_apply(&extension_lists.engine, php_load_zend_extension_cb);
    //注册php扩展
    zend_llist_apply(&extension_lists.functions, php_load_php_extension_cb);

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

extension_lists是一个链表,保存着根据php.ini中定义的extension=xxx.so取到的全部扩展名称,其中engine是zend扩展,functions为php扩展,依次遍历这两个数组然后调用php_load_php_extension_cb()或php_load_zend_extension_cb()进行各个扩展的加载:

static void php_load_php_extension_cb(void *arg)
{
#ifdef HAVE_LIBDL
    php_load_extension(*((char **) arg), MODULE_PERSISTENT, 0);
#endif
}
HAVE_LIBDL这个宏根据dlopen()函数是否存在设置的:

#Zend/Zend.m4
AC_DEFUN([LIBZEND_LIBDL_CHECKS],[
AC_CHECK_LIB(dl, dlopen, [LIBS="-ldl $LIBS"])
AC_CHECK_FUNC(dlopen,[AC_DEFINE(HAVE_LIBDL, 1,[ ])])
])

接着就是最关键的操作了,php_load_extension():

//ext/standard/dl.c
PHPAPI int php_load_extension(char *filename, int type, int start_now)
{
    void *handle;
    char *libpath;
    zend_module_entry *module_entry;
    zend_module_entry *(*get_module)(void);
    ...
    //调用dlopen打开指定的动态连接库文件:xx.so
    handle = DL_LOAD(libpath); 
    ...
    //调用dlsym获取get_module的函数指针
    get_module = (zend_module_entry *(*)(void)) DL_FETCH_SYMBOL(handle, "get_module"); 
    ...
    //调用扩展的get_module()函数
    module_entry = get_module();
    ...
    //检查扩展使用的zend api是否与当前php版本一致
    if (module_entry->zend_api != ZEND_MODULE_API_NO) {
        DL_UNLOAD(handle);
        return FAILURE;
    }
    ...
    module_entry->type = type;
    //为扩展编号
    module_entry->module_number = zend_next_free_module();
    module_entry->handle = handle;

    if ((module_entry = zend_register_module_ex(module_entry)) == NULL) {
        DL_UNLOAD(handle);
        return FAILURE;
    }
    ...
}

DL_LOAD()、DL_FETCH_SYMBOL()这两个宏在linux下展开后就是:dlopen()、dlsym(),所以上面过程的实现就比较直观了:

推荐阅读更多精彩内容