시작과 종료, 그리고 그 사이의 몇 가지 지점
이 책에서 당신은 PHP가 확장 공유 라이브러리를 로드할 때 처음으로 MINIT 함수를 사용하여 초기화 작업을 수행했습니다. 1장 "PHP의 라이프 사이클"에서는 MINIT에 대응하는 세 가지 다른 시작/종료 함수도 배웠습니다. , 각 페이지 요청이 시작되고 종료될 때 호출되는 RINIT/RSHUTDOWN 메서드 쌍도 있습니다.
Lifecycle
모듈 구조에 직접 연결되는 이 4가지 함수 외에 스레드 환경에서만 사용되며 각 스레드의 시작과 종료, 저장 공간을 처리하는 함수 2개가 있습니다. 시작하기 전에 먼저 PHP 소스 트리에서 php 확장 스켈레톤 프로그램 복사를 ext/sample4로 설정하세요. 코드는 다음과 같습니다.
config.m4
아아앙php_sample4.h
아아앙sample4.c
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
각 시작 및 종료 함수는 종료할 때마다 SUCCESS를 반환합니다. 이러한 함수 중 하나가 FAILURE를 반환하면 엔진은 프로세스가 실패한 것으로 간주하고 PHP 실행을 중단합니다.
모듈 수명 기간
은 이전 장에서 여러 번 사용되었으므로 MINIT는 이미 익숙할 때 실행됩니다. 모듈은 먼저 프로세스 공간에 로드되며, CLI 및 CGI와 같은 단일 요청 sapi 또는 apache2-worker와 같은 다중 스레드 sapi의 경우 포크가 관련되지 않으므로 한 번만 실행됩니다.
apache1, apache2 -prefork와 같은 다중 프로세스 sapi의 경우 mod_php 인스턴스를 통한 여러 웹 서버 프로세스의 경우 각 mod_php 인스턴스는 자체 확장 모듈을 로드해야 하므로 MINIT는 여러 번 실행되지만 여전히 실행됩니다.
모듈이 언로드되면 MSHUTDOWN 메서드가 호출됩니다. 이때 모듈의 모든 리소스(예: 영구 메모리 블록)가 호출됩니다.
클래스, 리소스 ID, 스트림 래퍼 및 필터, 사용자 공간 전역 변수, php.ini의 지침과 같은 엔진 측 기능이 공개되었습니다. 리소스는 모듈의 INIT 및 SHUTDOWN 단계에서 할당 및 해제됩니다.
이론적으로는 MSHUTDOWN 단계에서 리소스 해제 작업을 수행할 필요가 없습니다. 암시적 메모리 및 파일 릴리스를 수행하기 위해 OS에 적용합니다. 그러나 Apache에서는 1.3에서 확장 기능을 사용할 때 흥미로운 현상을 발견할 수 있습니다. Apache는 mod_php를 로드하고 프로세스에서 MINIT를 실행한 다음 즉시 mod_php를 언로드하고 MSHUTDOWN 메서드를 트리거합니다. , 그런 다음 올바른 MSHUTDOWN 단계가 없으면 MINIT 단계에서 처음에 할당된 리소스가 누출됩니다.
스레드 수명 주기
멀티 스레드 sapi에서는 때로는 각 스레드에 자체 독립적인 리소스를 할당하거나 자체 단일 요청 카운터를 추적해야 하는 경우가 있습니다. 후크가 존재하므로 스레드가 시작되고 종료될 때 실행될 수 있습니다. 일반적인 경우 apache2-worker와 같은 sapi가 시작되면 동시 요청을 처리하기 위해 12개 이상의 스레드가 생성됩니다. 任何在多请求间共享, 在同一进程中不同线程有不能访问的资源, 都是在线程的构造器和析构器中分配和释放的. 比如这可能包括EG(persistent_list)HashTable中的持久化资源, 因为它们通常包括网络或文件资源, 需要考虑指令间它们的状态一致性. 请求生命周期 最后一个也是最短的生命周期是请求生命周期, 在这个周期内, 你的扩展可能会去初始化默认的用户空间变量, 或初始化内部状态跟踪信息. 因为这些方法在每个页面请求都被调用, 因此要尽可能的保证这些处理和内存分配可以执行的足够快. 通过MINFO对外暴露模块信息 除非你计划只有很少人使用你的扩展, 并且并没有计划修改API, 否则你就需要能够告诉用户空间一些关于扩展自身的信息. 比如, 是否所有的环境和版本特有特性都可用? 它编译的外部库的版本是什么? 是否有网站或邮件地址可以让你扩展的用户在需要时寻求帮助? 如果你曾经看过phpinfo()或php -i的输出, 你就会注意到, 所有这些信息都被组织到一种良好格式, 易于解析的输出中. 你的扩展可以很简单的在这些内容中增加自己的信息, 只需要在你的模块中增加一个MINFO()函数即可: 通过使用这些包装函数, 你的模块信息将在从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) 输出一行表头. 第一个版本为每个可变参输出一个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();
}
void php_info_print_table_row(int cols, ...)
void php_info_print_table_row_ex(int cols, char *class, ...)
这两个版本都为每个可变参输出一个
void php_info_print_box_start(int flag)
void php_info_print_box_end()
这两个函数只是简单的输出一个表格( )的开始和结束. 如果给定的flag值非0, 则使用class="h", 否则使用class="v". 使用非html输出时, 标记为0将导致在star中输出一个换行符, 此时这两个函数不会在产生其他任何输出.,
void php_info_print_hr(void)
这个函数在html启用时输出
标签, 或者, 当没有启用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() | 핵심 전역 공간.은 주로 스크립트 컴파일의 Zend 엔진에서 사용됩니다. 및 커널 하단 레이어 실행 중에 .을 사용합니다. 확장에서 . |
PG() | php글로벌 공간. 대부분의 "core의 " php.ini 지시문은 <🎜에 매핑됩니다. >php전역 변수 구조의 하나 이상의 요소 . 예를 들어, : PG(register_globals), PG(안전 모드) 및 PG(memory_limit) |
FG() | 파일 전역 공간.대부분의 파일I/O 또는 스트림 관련 전역 변수가 이 구조에 로드되고 표준 확장 . |