Home > Article > Backend Development > [Translation] [php extension development and embedded] Chapter 12 - PHP startup process
Initiation, termination, and some of the points in between
In this book, you have learned more Use the MINIT function for the first time to perform initialization tasks when PHP loads your extended shared library. In Chapter 1 "The Life Cycle of PHP", you also learned three other startup/termination functions. The counterpart to MINIT is MSHUTDOWN, and there are also There is a pair of RINIT/RSHUTDOWN methods that are called when each page request starts and terminates.
Lifecycle
In addition to these four functions that are directly linked to the module structure, there are two functions that are used only in the thread environment and handle the starting and termination of each thread, as well as the storage space they use. Before starting, first set your Copy the PHP extension skeleton program to ext/sample4 in the PHP source tree. The code is as follows
##config.m4
PHP_ARG_ENABLE(sample4, [Whether to enable the "sample4" extension], [ enable-sample4 Enable "sample4" extension support]) if test $PHP_SAMPLE4 != "no"; then PHP_SUBST(SAMPLE4_SHARED_LIBADD) PHP_NEW_EXTENSION(sample4, sample4.c, $ext_shared) fi
php_sample4.h
#ifndef PHP_SAMPLE4_H /* Prevent double inclusion */ #define PHP_SAMPLE4_H /* Define Extension Properties */ #define PHP_SAMPLE4_EXTNAME "sample4" #define PHP_SAMPLE4_EXTVER "1.0" /* Import configure options when building outside of the PHP source tree */ #ifdef HAVE_CONFIG_H #include "config.h" #endif /* Include PHP Standard Header */ #include "php.h" /* Define the entry point symbol * Zend will use when loading this module */ extern zend_module_entry sample4_module_entry; #define phpext_sample4_ptr &sample4_module_entry #endif /* PHP_SAMPLE4_H */
sample4.c
#include "php_sample4.h" #include "ext/standard/info.h" static function_entry php_sample4_functions[] = { { NULL, NULL, NULL } }; PHP_MINIT_FUNCTION(sample4) { return SUCCESS; } PHP_MSHUTDOWN_FUNCTION(sample4) { return SUCCESS; } PHP_RINIT_FUNCTION(sample4) { return SUCCESS; } PHP_RSHUTDOWN_FUNCTION(sample4) { return SUCCESS; } PHP_MINFO_FUNCTION(sample4) { } zend_module_entry sample4_module_entry = { #if ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER, #endif PHP_SAMPLE4_EXTNAME, php_sample4_functions, PHP_MINIT(sample4), PHP_MSHUTDOWN(sample4), PHP_RINIT(sample4), PHP_RSHUTDOWN(sample4), PHP_MINFO(sample4), #if ZEND_MODULE_API_NO >= 20010901 PHP_SAMPLE4_EXTVER, #endif STANDARD_MODULE_PROPERTIES }; #ifdef COMPILE_DL_SAMPLE4 ZEND_GET_MODULE(sample4) #endif
Be careful, each Both the startup and termination functions return SUCCESS when exiting. If one of these functions returns FAILURE, the engine considers the process to have failed and interrupts the execution of php.
Module Life Cycle
has been used many times in the previous chapters, so MINIT should be familiar to you. It is triggered when the module is first loaded into the process space. For single Requesting sapi such as CLI and CGI, or multi-threaded sapi such as apache2-worker, it is only executed once, because it does not involve fork.
For multi-process sapi, such as apache1, apache2- prefork, for multiple webserver processes through their mod_php instances. Each mod_php instance must load its own extension module, so MINIT will be executed multiple times, but it will still only be executed once for each process.
When the module is unloaded, the MSHUTDOWN method is called. At this time, all resources of the module (such as persistent memory blocks) will be released and returned to the operating system.
Engine-side features, such as classes, resource IDs, stream wrappers and filters, user space global variables, instructions in php.ini, these public resources are allocated and released during the INIT and SHUTDOWN phases of the module.
Theoretically, you don’t need to do the resource release work in the MSHUTDOWN phase, leaving it to the OS to do the implicit memory and file release. But in apache 1.3 When using your extension, you will notice an interesting phenomenon, apache will load mod_php, run MINIT in the process, then immediately unload mod_php, trigger the MSHUTDOWN method, and then load it again. Without the correct MSHUTDOWN stage, during the MINIT stage The initially allocated resources will be leaked.
Thread life cycle
In multi-threaded sapi, Sometimes it is necessary to allocate each thread its own independent resources, or track its own single request counter. For these special cases, a set of per-thread hooks exist, allowing them to be executed when the thread starts and terminates. A typical case is When a sapi like apache2-worker is started, it will spawn a dozen or more threads to handle concurrent requests.
任何在多请求间共享, 在同一进程中不同线程有不能访问的资源, 都是在线程的构造器和析构器中分配和释放的. 比如这可能包括EG(persistent_list)HashTable中的持久化资源, 因为它们通常包括网络或文件资源, 需要考虑指令间它们的状态一致性.
请求生命周期
最后一个也是最短的生命周期是请求生命周期, 在这个周期内, 你的扩展可能会去初始化默认的用户空间变量, 或初始化内部状态跟踪信息. 因为这些方法在每个页面请求都被调用, 因此要尽可能的保证这些处理和内存分配可以执行的足够快.
通过MINFO对外暴露模块信息
除非你计划只有很少人使用你的扩展, 并且并没有计划修改API, 否则你就需要能够告诉用户空间一些关于扩展自身的信息. 比如, 是否所有的环境和版本特有特性都可用? 它编译的外部库的版本是什么? 是否有网站或邮件地址可以让你扩展的用户在需要时寻求帮助?
如果你曾经看过phpinfo()或php -i的输出, 你就会注意到, 所有这些信息都被组织到一种良好格式, 易于解析的输出中. 你的扩展可以很简单的在这些内容中增加自己的信息, 只需要在你的模块中增加一个MINFO()函数即可:
PHP_MINFO_FUNCTION(sample4) { php_info_print_table_start(); php_info_print_table_row(2, "Sample4 Module", "enabled"); php_info_print_table_row(2, "version", PHP_SAMPLE4_EXTVER); php_info_print_table_end(); }
通过使用这些包装函数, 你的模块信息将在从webserver sapi(比如cgi, iis, apache等)输出时自动的包装为HTML标签, 而在使用cli时输出为普通文本. 为了使得构建时你的扩展中可以使用这些函数原型, 你需要#include "ext/standard/info.h".
下面是这个头文件中可用的php_info_*()族函数.
char *php_info_html_esc(char *str TSRMLS_DC)
用户空间htmlentites()函数的底层实现php_escape_html_entities()的一个包装. 返回的字符串是用emalloc()分配的, 使用后必须显式的使用efree()释放.
void php_info_print_table_start(void)
void php_info_print_table_end(void)
输出html表格的开始/结束标签. html输出禁用时, 比如在cli中, 它将在start中输出换行符, end中不输出任何内容.
void php_info_print_table_header(int cols, ...)
void php_info_print_table_colspan_header(int cols, char *header)
输出一行表头. 第一个版本为每个可变参输出一个b4d429308760b6c2d20d6300079ed38e01c3ce868d2b3d9bce8da5c1b7e41e5b, 内容是cols后面的字符串参数. clospan版的则只输出一个b4d429308760b6c2d20d6300079ed38e01c3ce868d2b3d9bce8da5c1b7e41e5b, 并给它指定colspan属性.
void php_info_print_table_row(int cols, ...)
void php_info_print_table_row_ex(int cols, char *class, ...)
这两个版本都为每个可变参输出一个b6c5a531a458a2e790c1fd6421739d1cb90dd5946f0946207856a8a37f441edf. 两者的不同在于前者将为其设置class="v"属性, 而后者则允许调用者指定自己的类名用于自定义格式. 没有打开HTML格式输出时, 由于只是文本输出, 两者的差异就不复存在了.
void php_info_print_box_start(int flag)
void php_info_print_box_end()
这两个函数只是简单的输出一个表格(0a89a319bef8e3344dcfd73a23f3b7afb6c5a531a458a2e790c1fd6421739d1c, b90dd5946f0946207856a8a37f441edffd273fcf5bcad3dfdad3c41bd81ad3e5)的开始和结束. 如果给定的flag值非0, 则使用class="h", 否则使用class="v". 使用非html输出时, 标记为0将导致在star中输出一个换行符, 此时这两个函数不会在产生其他任何输出.
void php_info_print_hr(void)
这个函数在html启用时输出231a563c997aa9e3e0ae614bd16728b0标签, 或者, 当没有启用html输出时, 输出31个下划线, 并在前后各输出两个换行符.
在MINFO中通常可以使用PHPWRITE()和php_printf(), 但手动输出内容时应该注意它需要依赖于当前的SAPI期望输出文本还是html. 可以通过测试全局的sapi_module结构体的phpinfo_as_text属性来确认这一点:
PHP_MINFO_FUNCTION(sample4) { php_info_print_table_start(); php_info_print_table_row(2, "Sample4 Module", "enabled"); php_info_print_table_row(2, "version", PHP_SAMPLE4_EXTVER); if (sapi_module.phpinfo_as_text) { /* No HTML for you */ php_info_print_table_row(2, "By", "Example Technologies\nhttp://www.php.cn/"); } else { /* HTMLified version */ php_printf("<tr>" "<td class=\"v\">By</td>" "<td class=\"v\">" "<a href=\"http://www.example.com\"" " alt=\"Example Technologies\">" "<img src=\"http://www.example.com/logo.png\" />" "</a></td></tr>"); } php_info_print_table_end(); }
常量
向用户空间脚本暴露信息更好的方法是使用扩展定义脚本可以在运行时访问的常量, 并可以通过这些常量改变扩展的某些行为. 在用户空间中, 我们使用define()函数定义常量; 内部, 则是和它非常相似的REGISTER_*_CONSTANT()一族的宏.
多数常量是你想要它们在所有脚本中初始化为相同值的数据. 它们是在MINIT函数中定义的.
PHP_MINIT_FUNCTION(sample4) { REGISTER_STRING_CONSTANT("SAMPLE4_VERSION", PHP_SAMPLE4_EXTVER, CONST_CS | CONST_PERSISTENT); return SUCCESS; }
这个宏的第一个参数是要暴露给用户空间的常量名. 在这个例子中, 用户空间就可以执行echo SAMPLE4_VERSION; 得到输出1.0. 这里有一点要特别注意, REGISTER_*_CONSTANT()一族的宏使用了sizeof()调用去确定常量名的长度. 也就是说只能使用字面量值. 如果使用char *变量则会导致不正确的结果(sizeof(char *)在32位平台上通常是4, 而不是真正字符串的长度).
下一个参数是常量的值. 多数情况下, 它只需要一个参数, 不过, 对于STRINGL版本, 你还需要一个参数去指定长度. 在注册字符串常量时, 字符串的值并不会拷贝到常量中, 只是引用它. 也就是说需要在持久化内存中为其分配空间, 并在对应的SHUTDOWN阶段释放它们.
最后一个参数是一个有两个可选值的位域操作结果. CONST_CS标记说明该常量大小写敏感. 对于用户空间定义的常量以及几乎所有的内部常量来说, 这都是默认行为. 只有极少数的情况, 比如trUE, FALSE, NULL, 在注册时省略了这个标记用以说明它们是不区分大小写的.
注册常量时的第二个标记是持久化标记. 当在MINIT中定义常量时, 它们必须被构建为跨请求的持久化常量. 但是, 如果在请求中定义常量, 比如在RINIT中, 你可能就需要省略这个标记以允许引擎在请求结束时销毁该常量了.
下面是4个可用的常量注册宏的原型. 一定要记住, 名字参数必须是字符串字面量而不能是char *变量:
REGISTER_LONG_CONSTANT(char *name, long lval, int flags) REGISTER_DOUBLE_CONSTANT(char *name, double dval, int flags) REGISTER_STRING_CONSTANT(char *name, char *value, int flags) REGISTER_STRINGL_CONSTANT(char *name, char *value, int value_len, int flags)
如果字符串必须从变量名初始化, 比如在循环中, 你可以使用如下的函数调用(上面的宏就是映射到这些函数中的):
void zend_register_long_constant(char *name, uint name_len, long lval, int flags, int module_number TSRMLS_DC) void zend_register_double_constant(char *name, uint name_len, double dval, int flags, int module_number TSRMLS_DC) void zend_register_string_constant(char *name, uint name_len, char *strval, int flags, int module_number TSRMLS_DC) void zend_register_stringl_constant(char *name, uint name_len, char *strval, uint strlen, int flags, int module_number TSRMLS_DC)
此时, 名字参数的长度可以直接由调用作用域提供. 你应该注意到, 这次就必须显式的传递TSRMLS_CC参数了, 并且, 这里还引入了另外一个参数.
module_number是在你的扩展被加载或被卸载时传递给你的信息. 你不用关心它的值, 只需要传递它就可以了. 在MINIT和RINIT函数原型中都提供了它, 因此, 在你定义常量的时候, 它就是可用的. 下面是函数版的常量注册例子:
PHP_MINIT_FUNCTION(sample4) { register_string_constant("SAMPLE4_VERSION", sizeof("SAMPLE4_VERSION"), PHP_SAMPLE4_EXTVER, CONST_CS | CONST_PERSISTENT, module_number TSRMLS_CC); return SUCCESS; }
要注意当sizeof()用于确定SAMPLE4_VERSION的长度时, 这里并没有减1. 常量的名字是包含它的终止NULL的. 如果使用strlen()确定长度, 要记得给结果加1以使其包含终止的NULL.
除了数组和对象, 其他的类型都可以被注册, 但是因为在ZEND API中不存在这些类型的宏或函数, 你就需要手动的定义常量. 按照下面的范本, 仅需要在使用时修改去创建恰当类型的zval *即可:
void php_sample4_register_boolean_constant(char *name, uint len, zend_bool bval, int flags, int module_number TSRMLS_DC) { zend_constant c; ZVAL_BOOL(&c.value, bval); c.flags = CONST_CS | CONST_PERSISTENT; c.name = zend_strndup(name, len - 1); c.name_len = len; c.module_number = module_number; zend_register_constant(&c TSRMLS_CC); }
扩展的全局空间
如果可以保证任何时刻一个进程中只有一个php脚本在执行, 你的扩展就可以随意的定义全局变量并去访问它们, 因为已知在opcode执行过程中不会有其他脚本被执行. 对于非线程sapi, 这是可行的, 因为所有的进程空间中都只能同时执行一个代码路径.
然而在线程sapi中, 可能会有两个或更多的线程同时读或更糟糕的情况是同时写相同的值. 为了解决这个问题, 就引入了一个扩展的全局空间概念, 它为每个扩展的数据提供一个唯一的数据存储桶.
定义扩展的全局空间
要给你的扩展申请一块存储的桶, 首先就需要在php_sample4.h上的一个标准结构体中定义所有你的全局变量. 比如, 假设你的扩展要保存一个计数器, 保持对某个方法在请求内被调用次数的跟踪, 你就需要定义一个结构体包含一个unsigned long:
ZEND_BEGIN_MODULE_GLOBALS(sample4) unsigned long counter; ZEND_END_MODULE_GLOBALS(sample4)
ZEND_BEGIN_MODULE_GLOBALS和ZEND_END_MODULE_GLOBALS宏为扩展全局变量结构的定义提供了统一的框架. 如果你看过这个块的展开形式, 就可以很容易的理解它了:
typedef struct _zend_sample4_globals { unsigned long counter; } zend_sample4_globals;
你可以像在其他的C语言结构体中增加成员一样, 为它增加其他成员. 现在, 你有了存储桶的(数据结构)定义, 接下来要做的就是声明一个这个类型的变量, 你需要在扩展的sample4.c文件中, #include "php_sample4.h"语句下一行声明它:
ZEND_DECLARE_MODULE_GLOBALS(sample4);
它将根据是否启用了线程安全, 被解析为两种不同的格式. 对于非线程安全构建, 比如apache1, apache2-prefork, cgi, cli以等等, 它是直接在真正的全局作用域声明了一个zend_sample4_globals结构体的直接值:
zend_sample4_globals sample4_globals;
这和你在其他单线程应用中声明的全局变量没有什么差异. 计数器的值直接通过sample4_globals.counter访问. 而对于线程安全构建, 则是另外一种处理, 它只是声明了一个整型值, 以后它将扮演到真实数据的引用的角色:
int sample4_globals_id;
设置这个ID就代表声明你的扩展全局变量到引擎中. 通过提供的信息, 引擎将在每个新的线程产生时分配一块内存 专门用于线程服务请求时的似有存储空间. 在你的MINIT函数中增加下面的代码块:
#ifdef ZTS ts_allocate_id(&sample4_globals_id, sizeof(zend_sample4_globals), NULL, NULL); #endif
注意, 这个语句被包裹在一个ifdef中, 以放置在没有启用Zend线程安全(ZTS)时执行它. 这是因为sample4_globals_id只在线程环境下才会被声明, 非线程环境的构建则使用的是sample4_globals变量的直接值.
每个线程的初始化和终止
在非线程构建中, 你的zend_sample4_globals结构体在一个进程中只有一份拷贝. 你可以给它设置初始值或在MINIT或RINIT中为其分配资源, 进行初始化, 在MSHUTDOWN和RSHUTDOWN阶段如果需要, 则进行相应的释放.
然而, 对于线程构建, 每次一个新的线程产生时, 都会分配一个新的结构体. 实际上, 这在webserver启动时可能会发生很多次, 而在webserver进程的整个生命周期中, 这可能会发生成百上千次. 为了知道怎样初始化和终止你的扩展全局空间, 引擎需要执行一些回调函数. 这就是上面的例子中你传递给ts_allocate_id()的NULL参数; 在你的MINIT函数上面增加下面的两个函数:
static void php_sample4_globals_ctor( zend_sample4_globals *sample4_globals TSRMLS_DC) { /* 在线程产生时初始化一个新的zend_sample4_globals结构体 */ sample4_globals->counter = 0; } static void php_sample4_globals_dtor( zend_sample4_globals *sample4_globals TSRMLS_DC) { /* 在初始化阶段分配的各种资源, 都在这里释放 */ }
接着, 在启动和终止时使用这些函数:
PHP_MINIT_FUNCTION(sample4) { REGISTER_STRING_CONSTANT("SAMPLE4_VERSION", PHP_SAMPLE4_EXTVER, CONST_CS | CONST_PERSISTENT); #ifdef ZTS ts_allocate_id(&sample4_globals_id, sizeof(zend_sample4_globals), (ts_allocate_ctor)php_sample4_globals_ctor, (ts_allocate_dtor)php_sample4_globals_dtor); #else php_sample4_globals_ctor(&sample4_globals TSRMLS_CC); #endif return SUCCESS; } PHP_MSHUTDOWN_FUNCTION(sample4) { #ifndef ZTS php_sample4_globals_dtor(&sample4_globals TSRMLS_CC); #endif return SUCCESS; }
要注意, 在没有开启ZTS时, ctor和dtor函数是手动调用的. 不要忘记: 非线程环境也需要初始化和终止.
你可能奇怪为什么在php_sample4_globals_ctor()和php_sample4_globals_dtor()中直接使用了TSRMLS_CC. 如果你认为"这完全不需要, 它在ZTS禁用时解析出来是空的内容, 并且由#ifndef指令, 我们知道ZTS是被禁用的, 你的观点绝对正确. 声明中的相关的TSRMLS_DC指令仅用于保证代码的一致性. 从积极的一面考虑, 如果ZEND API修改这些值使得在非ZTS构建中也有有效内容时, 你的代码就不需要修改就做好了相应的调整.
访问扩展的全局空间
现在你的扩展有了一个全局变量集合, 你可以开始在你的代码中访问它们了. 在非ZTS模式中这很简单, 只需要访问进程全局作用域的sample4_globals变量的相关成员即可, 比如, 下面的用户空间函数增加了你前面定义的计数器并返回它的当前值:
PHP_FUNCTION(sample4_counter) { RETURN_LONG(++sample4_globals.counter); }
很简单很容易. 不幸的是, 这种方式在线程环境的PHP构建中不能工作. 这种情况下你就需要做更多的工作. 下面是使用ZTS语义的该函数返回语句:
RETURN_LONG(++TSRMG(sample4_globals_id, zend_sample4_globals*, counter));
TSRMG()宏需要你已经传递的TSRMLS_CC参数, 它会从当前线程池的资源结构中查找需要的数据. 这里, 它使用sample4_globals_id索引映射到内存池中你扩展的全局结构体的位置, 最终, 使用数据类型映射的元素名得到结构体中的偏移量. 因为你并不知道运行时你的扩展是否使用ZTS模式, 因此, 你需要让你的代码适应两种情况. 要做到这一点, 就需要按照下面方式重写该函数:
PHP_FUNCTION(sample4_counter) { #ifdef ZTS RETURN_LONG(++TSRMG(sample4_globals_id, \ zend_sample4_globals*, counter)); #else /* non-ZTS */ RETURN_LONG(++sample4_globals.counter); #endif }
看起来不舒服? 是的, 如果你所有的代码都基于这样的ifdef指令去处理线程安全的全局访问, 它看起来可能比Perl还糟糕! 这就是为什么在所有的PECL扩展中都使用了一个抽象的宏来封装全局访问的原因. 在你的php_sample4.h文件中进行如下定义:
#ifdef ZTS #include "TSRM.h" #define SAMPLE4_G(v) TSRMG(sample4_globals_id, zend_sample4_globals*, v) #else #define SAMPLE4_G(v) (sample4_globals.v) #endif
这样, 就可以让你访问扩展全局空间时变得简单易懂:
PHP_FUNCTION(sample4_counter) { RETURN_LONG(++SAMPLE4_G(counter)); }
这个宏给你一种似曾相识的感觉吗? 应该是这样的. 它和你已经使用过的EG(symbol_table)以及EG(active_symbol_table)是仙童的概念和实践. 在阅读php源码树中其他部分以及其他扩展时, 你会经常碰到这种宏. 下表列出了常用的全局访问宏:
访问宏 |
关联数据 |
EG() |
执行全局空间.这个结构体主要用于引擎内部对当前请求的状态跟踪.这个全局空间中可以找到符号表,函数表,类表,常量表,资源表等. |
CG() |
Core global space. is mainly used by the Zend engine in script compilation and the bottom layer of the kernel Use . during execution. It is rare to test these values directly in your extension. . |
##PG() |
##php Global space. Most"core"php.ini directives map to phpOne or more elements in the global variable structure.For example: PG(register_globals), PG(safe_mode) and PG(memory_limit) |
FG()
| ##File global space.Most filesI/O Or stream-related global variables are loaded into this structure and exposed through standard extensions . |