PHP7内核剖析 学习笔记 第十章 扩展开发(1)

开发者可使用C/C++实现自定义的功能,通过扩展嵌入PHP中。扩展可扩展PHP的以下方面:

1.介入PHP的编译、执行阶段,如opcache重定义了编译函数。

2.提供内部函数。

3.提供内部类。

4.实现RPC客户端,实现与Redis、MySQL等外部服务器的交互。

5.提升性能,PHP是解释型语言,性能方面不及C语言,可将耗CPU的操作用C语言代替。

10.1 扩展的内部实现

PHP中扩展用zend_module_entry结构表示,用于保存扩展的基本信息,包括扩展名、扩展版本、扩展提供的函数列表、PHP四个执行阶段的hook函数等。每个扩展都定义了一个此结构变量,且变量的名称必须是{module_name}_module_entry,内核通过该结构获取扩展提供的功能。

PHP扩展分为PHP扩展和Zend扩展,对内核而言分别称之为模块(module)、扩展(extension),本章主要介绍模块。

扩展可以在编译PHP时一起编译(即静态编译),也可以单独编译为共享库。共享库形式需要将库名加入php.ini文件中,在php_module_startup()阶段会将php.ini中的对应的扩展共享库加载到PHP中。

PHP解析php.ini时,会把其中的extension=xxx.sozend_extension=xxx.so解析到全局变量extension_lists中,该变量是一个php_extension_lists结构,其engine成员和functions成员均为链表,其中engine成员保存所有的Zend扩展,functions成员保存所有的PHP扩展:

php_ini_register_extensions()中将遍历php_extension_lists.engine和php_extension_lists.functions,然后分别调用php_load_zend_extension_cb、php_load_php_extension_cb完成Zend扩展、PHP扩展的加载。

在调用php_load_php_extension加载PHP扩展时,输入的参数是扩展名的地址:

c 复制代码
// file: main/php_ini.c
static void php_load_php_extension(void *arg) {
#ifdef HAVE_LIBDL
    php_load_extension(*((char **)arg), MODULE_PERSISTENT, 0);
#endif
}

只有支持dlopen函数时,才定义HAVE_LIBDL宏,因为扩展的共享库就是通过该函数加载的。

接着由php_load_extension()完成扩展的注册,该函数定义在standard扩展中,具体步骤如下:

1.dlopen()打开扩展的共享库文件。

2.dlsym()获取动态库中get_module函数的地址,get_module()是每个扩展都必须提供的接口,用于返回扩展zend_module_entry结构的地址。

3.调用扩展的get_module(),会返回扩展定义的zend_module_entry结构。

4.检查扩展版本,如PHP7的扩展在PHP5下是无法使用的。

5.注册扩展,将扩展添加到module_registry中,这是一个全局HashTable,用于保存全部扩展的zend_module_entry结构。

6.如果扩展提供了内部函数,将这些函数注册到EG(function_table)符号表中。

完成扩展的注册后,PHP将在不同执行阶段,依次调用每个扩展注册的当前阶段的hook函数。

10.2 扩展的构成及编译

PHP扩展的zend_module_entry结构作为全局变量分配即可,变量名必须是{extensionname}_module_entry

上图中主要成员含义:

1.name:扩展名,不能与其他扩展相同。

2.functions:扩展定义的内部函数。

3.module_startup_func:PHP模块初始化阶段的回调函数,可使扩展介入模块初始化阶段。

4.module_shutdown_func:模块关闭阶段的回调函数。

5.request_startup_func:请求初始化阶段的回调函数。

6.request_shutdown_func:请求结束阶段的回调函数。

7.info_func:php_info函数会调用,展示配置、运行信息。

8.version:扩展版本。

以上成员需手动设置,剩余成员可通过STANDARD_MODULE_HEADER、STANDARD_MODULE_PROPERTIES宏进行填充:

我们需要提供一个获取zend_module_entry结构地址的接口给内核,为了提供这个接口,只需在扩展中加入以下ZEND_GET_MODULE(extension_name)宏即可:

以上宏实际上定义了一个get_module函数,返回扩展的zend_module_entry结构的地址,这就是为什么扩展的zend_module_entry结构的变量名必须是{extensionname}_module_entry。get_module()会在php_load_extension()中调用。

定义完zend_module_entry结构,然后定义并以当前扩展名调用ZEND_GET_MODULE()宏,一个扩展就编写完成了,如果要定义的扩展名为mytest:

然后php -m就可看到mytest扩展了。

10.2.1 脚本工具

PHP提供了几个用于简化扩展开发的脚本工具:ext_skel、phpize、php-config。

PHP的主要目录结构如下,假设PHP的安装路径为/usr/local/php7:

10.2.1.1 ext_skel

该脚本位于PHP源码的/ext目录下,用来生成扩展的基本骨架,可用以下命令生成一个扩展结构:

bash 复制代码
./ext_skel --extname=扩展名

执行完后会在ext目录下生成一个扩展目录,如extname是mytest,则生成以下文件:

该脚本初步生成的这个扩展可以成功编译、安装、使用,我们可以根据需求来完善它。

10.2.1.2 php-config

在PHP安装前,该脚本位于PHP源码的/script/php-config.in;PHP安装后,该脚本位于PHP目录的bin目录下,且重命名为php-config,其中保存了PHP的安装信息,如:

1.PHP安装路径。

2.PHP版本。

3.PHP源码的头文件目录(如main、Zend、ext、TSRM目录,它们都位于PHP安装目录的/include/php目录中),编写扩展时会用到这些头文件。

4.LDFLAGS:外部库路径。

5.外部依赖库,告诉编译器要链接哪些文件。

6.扩展的存放目录,即.so文件的保存位置,安装扩展make install时将安装到此目录下。

7.编译的SAPI,如Cli、Fpm、Cgi等。

8.PHP编译参数,即编译PHP时执行./configure时的参数。

使用命令./configure --with-php-config=xxx生成PHP扩展的Makefile时,将php-config文件作为参数传入即可,用来生成Makefile。如果没有--with-php-config参数时,将在默认的PHP安装路径下搜索。

10.2.1.3 phpize

该脚本用于操作autoconf/automake/autoheader/autolocal等系列命令(这些命令是GNU auto系列的工具,用于自动生成可移植的软件构建系统),因为这系列命令比较繁琐。最后生成扩展的configure文件。

简单项目我们可以手动编写Makefile,但大型软件需适配不同系统,难以手动编写,此时可使用GNU auto系列工具,这些工具需配合使用,步骤如图10-2:

以下是各个工具的介绍:

1.autoscan:扫描源码目录,生成configure.scan,它是configure.in的初始模板,开发者手动完善后将其改名为configure.in,其中定义了系统环境检测逻辑、项目依赖、编译参数。

2.aclocal:可根据configure.in自动生成aclocal.m4。.m4文件使用M4宏处理器语言编写,是GNU Autotools生态中的宏定义文件。.m4文件可将复杂的环境监测(如编译器特性、库兼容性)封装为可调用宏。此外,acinclude.m4文件中可定义用户自定义宏,aclocal会将该文件中的宏加载到aclocal.m4文件。

3.autoheader:可根据configure.in、aclocal.m4生成一个C语言头文件config.h.in供configure执行时使用,该头文件中包含很多undef声明,如#undef USE_GPU,在configure执行时,会检测环境从而生成这些宏的实际值,如configure检测到有GPU或用户执行configure脚本时加上了类似--enable-gpu的参数,则生成的config.h文件中有#define USE_GPU 1

4.autoconf:将configure.in中的宏展开,生成configure,此过程会用到aclocal.m4中定义的宏。

5.automake:将Makefile.am中定义的结构建立Makefile.in,然后configure脚本将生成的Makefile.in文件转换为Makefile。

编写PHP扩展时,PHP提供了两个配置:configure.in、acinclude.m4,这两个配置是从PHP安装路径/lib/php/build下的phpize.m4、acinclude.m4复制来的。configure.in中定义了一些PHP内核相关的配置检查项,这个文件会include每个扩展各自的config.m4,因此我们只需在config.m4中定义扩展自己的配置即可。在扩展所在目录执行phpize即可自动生成扩展的configure、config.h文件。

phpize脚本中的处理:

1.phpize_check_configm4:检查扩展的config.m4是否存在。

2.phpize_check_build_files:检查PHP安装路径下的lib/php/build目录,其中包含PHP自定义的autoconf宏文件acinclude.m4以及libtool。

3.phpize_print_api_numbers:输出PHP API Version、Zend Module API No、Zend Extension API No信息。

4.phpize_copy_files:复制一些文件到扩展的build目录和扩展的根目录中;将acinclude.m4、build/libtool.m4合并到扩展目录下的aclocal.m4文件中。

5.phpize_replace_prefix:将PHP安装路径/lib/php/build下的phpize.m4复制到扩展目录下,将文件中的prefix替换为PHP安装路径,然后将其重命名为configure.in

6.phpize_check_shtool:检查PHP安装路径的/build/shtool。

7.phpize_check_autotools:检查autoconf、autoheader是否安装。

8.phpize_autotools:执行autoconf生成configure,然后执行autoheader生成config.h。

10.2.2 扩展的编写步骤

1.通过ext目录下的ext_skel脚本生成扩展基本框架。

2.修改config.m4配置,设置编译配置参数、依赖库/函数检查等。

3.按PHP扩展的格式,使用PHP提供的API编写要实现的功能。

4.扩展编写完成后,执行phpize生成configure和其他配置文件。

5.编译安装,./configuremakemake install,然后将扩展的.so文件路径加到php.ini中。

扩展开发过程中,如有文件增加/删除或编译参数变更,则需重新执行phpize;如只是修改现有文件,则只需重新makemake install

10.2.3 config.m4

它是autoconf格式的配置文件。编译时config.m4被include到configure.in文件中,最终被autoconf编译为configure。一个简单的扩展的config.m4文件只需包含以下内容:

PHP在acinclude.m4文件中基于autoconf/automake的宏封装了很多可直接使用的宏,使用时可在该文件中直接搜索。其中常见的宏如下:

1.PHP_ARG_WITH(arg_name, check message, help info):定义一个--with-feature[=arg]这样的编译参数,其调用的是autoconf的AC_ARG_WITH宏。该宏有5个参数,其中有两个参数有默认值。前三个参数分别表示参数名、执行./configure时的展示信息、执行--help时的展示信息。通过该宏定义的参数可在config.m4中通过$PHP_{大写参数名}来访问。

2.PHP_ARG_ENABLE(arg_name, check message, help info):定义一个--enable-feature[=arg]--disable-feature参数,后者等价于--enable-feature=no。该宏定义的参数是一个开关,如需要开关二值之外的其他值,需使用PHP_ARG_WITH宏。

3.AC_MSG_CHECKING()/AC_MSG_RESULT()/AC_MSG_ERROR():执行./configure时的输出,其中AC_MSG_ERROR()宏会中断configure执行。

4.AC_DEFINE(variable, value, [description]):定义一个宏,如AC_DEFINE(IS_DEBUG, 1, []),则执行autoheader时将在头文件中生成#define IS_DEBUG 1

5.PHP_ADD_INCLUDE(path):添加编译时搜索include的文件的路径,即gcc -I参数。

6.PHP_CHECK_LIBRARY(library, function [, action-found [, action-not-found [, extra-libs]]]):检查依赖的库中是否存在需要的function。action-found参数是存在时执行的动作;action-not-found参数是不存在时执行的动作。如检查pthread库中是否存在pthread_create函数,如不存在则终止./configure执行:

  1. AC_CHECK_FUNC(function, [action-if-found], [action-if-not-found]):检查函数是否存在。

8.PHP_ADD_LIBRARY_WITH_PATH(LIBNAME, XXX_DIR/$PHP_LIBDIR, XXX_SHARED_LIBADD):添加链接库,即gcc -l(指定链接库)、gcc -L(添加链接库的搜索路径)参数。

9.PHP_NEW_EXTENSION(extname, sources [, shared [, sapi_class [, extra-cflags [, cxx [, zend_ext]]]]]):注册一个扩展。extname参数是扩展名;sources参数是空格隔开的扩展的全部.c源文件名;shared参数确定此扩展是动态库还是静态库。每个扩展的config.m4中都需要通过此宏完成扩展的编译设置。一个例子:

10.`PHP_INSTALL_HEADERS(path, [, file...]):该宏可将扩展的头文件安装到PHP的include目录下,当扩展提供了一些方法供其他扩展使用时,可使用该宏。

10.3 钩子函数

PHP扩展可定义钩子函数,当PHP执行到不同阶段时会回调扩展定义的钩子函数。

几个钩子函数执行的先后顺序:module startup->request startup->编译、执行(此阶段无钩子函数)->request shutdown->post deactivate->module shutdown。

10.3.1 模块初始化阶段

此阶段的钩子函数通过zend_module_entry->module_startup_func指定,通常此过程只会在SAPI启动后执行一次。此阶段可进行扩展提供的内部类和扩展提供的常量的注册;还可覆盖PHP编译、执行的两个函数指针zend_compile_file、zend_execute_ex,opcache的实现原理就是替换了zend_compile_file,使得PHP在编译时调用的是opcache定义的编译函数,该函数会对编译后的结果进行缓存。

扩展中可通过PHP_MINIT_FUNCTION()或ZEND_MINIT_FUNCTION()宏来定义此阶段的钩子函数,该宏为函数设置了统一参数,并把函数名加上了zm_startup_前缀:

定义完成后,可用PHP_MINIT()或ZEND_MINIT()宏获取函数名,然后将该函数指针赋值给zend_module_entry->module_startup_func即可:

10.3.2 请求初始化阶段

该阶段的钩子函数通过zend_module_entry->request_startup_func指定,此函数在编译、执行前回调,fmp模式下每个HTTP请求都是一个request,脚本执行前会先执行该钩子函数。此钩子函数可对每个请求进行处理,如过滤请求、获取请求IP所在城市、对请求数据解密等。此函数通过PHP_RINIT_FUNCTION()或ZEND_RINIT_FUNCTION()宏来定义:

获取该函数地址的宏为PHP_RINIT()或ZEND_RINIT():

10.3.3 请求结束阶段

此钩子函数通过zend_module_entry->request_shutdown_func指定,会在请求结束时被调用,该函数通过PHP_RSHUTDOWN_FUNCTION()或ZEND_RSHUTDOWN_FUNCTION()宏来定义:

该函数地址通过PHP_RSHUTDOWN()或ZEND_RSHUTDOWN()宏来获取:

10.3.4 post deactivate阶段

该钩子函数通过zend_module_entry->post_deactivate_func指定,该函数也是在请求结束后调用的,它比request_shutdown_func更晚执行(在调用request_shutdown_func后,PHP会输出缓冲区、释放请求中的全局变量等请求级资源,然后才会调用post_deactivate_func)。

此函数通过ZEND_MODULE_POST_ZEND_DEACTIVATE_D()宏来定义,通过ZEND_MODULE_POST_ZEND_DEACTICATE_N()宏来获取函数地址:

10.3.5 模块关闭阶段

此阶段的钩子函数通过zend_module_entry->module_shutdown_func指定,此阶段可做一些资源的清理。此钩子函数可通过PHP_MSHUTDOWN_FUNCTION()或ZEND_MSHUTDOWN_FUNCTION()宏来定义:

此钩子函数可通过PHP_MSHUTDOWN()或ZEND_MSHUTDOWN()宏来获取函数地址:

使用gdb调试时可使用上面介绍的钩子函数名的格式来设置断点。

如果扩展名为mytest,则扩展代码中可能会有:

10.4 全局资源

多线程环境下,直接使用全局变量实现数据共享会导致竞态条件。如果开发的扩展要支持多线程,则扩展中需要通过TSRM来注册和使用全局资源。

扩展中通常定义一个保存全局资源的结构体,然后将这个结构体像内核中的EG、CG那样注册到TSRM。这个结构体可通过ZEND_BEGIN_MODULE_GLOBALS(extension_name)、ZEND_END_MODULE_GLOBALS(extension_name)宏来定义,这两个宏必须成对出现,中间定义扩展中用到的全局变量的结构体类型即可,例子如下:

之后创建一个此结构体的全局变量,如果未开启线程安全(ZTS),直接创建普通的全局变量即可;如果开启了线程安全,需要向TSRM注册,得到一个唯一资源id。该创建结构体全局变量的过程也由专门的宏来完成:

以mytest扩展为例:

最后,定义一个像EG、CG的宏用于访问扩展的全局资源结构体,这一步通过ZEND_MODULE_GLOBALS_ACCESSOR(module_name, v)宏来完成:

该宏展开后:

之后就可在扩展中通过MYTEST_G(opene_cache)、MYTEST_G(class_table)读写结构体成员了。通常会把这个全局资源结构体的定义、访问结构体的宏的定义都放在头文件中,然后把全局变量的声明放到源文件中:

一个扩展中可定义多个全局变量结构,只要结构名不同即可。

10.5 ini配置

php将在以下位置查找php.ini文件:当前工作目录、环境变量PHPRC指定的目录、编译时指定的路径,命令行模式下,php.ini的查找路径可用-c参数指定。

php.ini文件的语法为配置标识符 = 值;空白字符被忽略,用分号开始的行也被忽略;[xxx]行被忽略;配置标识符大小写敏感;值可以是数字、字符串、PHP常量、位运算表达式。

扩展中如想获取配置项,需要为配置项设置解析规则,配置时需要将各项规则定义在PHP_INI_BEGIN()、PHP_INI_END()宏之间:

可见这两个宏实际生成了一个数组来保存各解析规则。

解析规则通过STD_PHP_INI_ENTRY()宏来定义:

参数含义:

1.default_value:默认值,不管转化后是什么类型,这里必须是字符串。

2.modifiable:可修改等级。ZEND_INI_USER为可在PHP脚本中修改;ZEND_INI_SYSTEM为可在php.ini、httpd.conf中修改;ZEND_INI_PERDIR为可以在php.ini、.htaccess、httpd.conf中修改;ZEND_INI_ALL为三种都可以。

3.on_modify:解析函数,当name配置了,将调用该函数解析。PHP提供了几个常用的解析函数:OnUpdateBool、OnUpdateLong、OnUpdateGEZero、OnUpdateReal、OnUpdateString、OnUpdateStringUnempty。

4.property_name:将值解析到struct_type参数指定的结构中的成员变量。

5.struct_type:解析到的结构类型。

6.struct_ptr:解析到的结构的地址。

即该宏的作用是将name项的值解析到(struct_type *)struct_ptr->property_name。该宏会为每一条解析规则生成一个zend_ini_entry_def结构,来保存其参数提供的信息:

还有另一个STD_PHP_INI_BOOLEAN()宏,它类似STD_PHP_INI_ENTRY()宏,但STD_PHP_INI_BOOLEAN()宏可将配置添加到phpinfo()的输出中。

比如我们想要将php.ini中的mytest.opene_cache配置项的值解析到上例MYTEST_G()结构中的open_cache成员,该成员类型为zend_long,默认值为109,则可这么定义:

展开后:

XtOffsetOf()宏在Linux环境下展开就是offsetof(),用来获取一个结构体成员的地址offset。比如offset = (void *)XtOffsetOf(zend_mytest_globals, open_cache),mytest_globals地址为ptr,则mytest_globals.open_cache可通过(char *)ptr + offset来访问,等价于ptr->open_cache

定义了映射规则后,接下来需要注册映射规则来使映射规则生效,可通过REGISTER_INI_ENTRIES()宏来注册,该宏展开后为zend_register_ini_entries(ini_entries, module_number),通常会将注册操作放到模块初始化阶段,此阶段php.ini已完成解析,所有配置项保存在configuration_hash哈希表中,zend_register_ini_entries()会查找哈希表,如果有名为name的配置项,则调用on_modify函数进行赋值。

如on_modify函数为OnUpdateLong(),则会调用zend_atol将字符串转换为long,然后赋值给指定结构体的指定成员,获取该成员地址时,使用的就是XtOffsetOf()宏。

如果PHP提供的几个on_modify函数不能满足需求,可通过ZEND_INI_MH(div_on_modify)宏来自定义on_modify函数:

例如,想要将php.ini中的配置项mytest.class插入MYTEST_G(class_table)哈希表,则可自定义一个ZEND_INI_MH(OnUpdateAddArray)函数: