不可预知的全局存取:
在创建扩展时,你不知道运行的环境是否需要线程。通常情况下,在头文件里会定义ZTS标志,当PHP在线程安全环境下运行时,或者由于SAPI的需要,或者由于设置了这个enable-maintainer-zts参数,就会通过类似这种指令#ifdef ZTS来定义。
正如你前面看见到,只有在线程安全池里分配资源是有意义的,并且PHP被编译运行在线程安全的环境下。这就是为什么在前面要包含检查ZTS,在非线程安全的环境下没有ZTS定义。
在本章的前面很多地方的PHP_MINIT_FUNCTION(myextension)方法中,#ifdef ZTS代码被用来有条件的选择正确的初始化代码。ZTS模式使用ts_allocate_id()方法给myextension_globals_id 变量付值,非ZTS模式下,直接调myextension_globals的初始化方法。这两个变量会在你的源文件里用宏声明:DECLARE_MODULE_GLOBALS (myextension); 这会自动检测是否是ZTS模式,声明正确的变量。
在谈到存取全局变量时,你可能采用自定义宏来处理,在12章里,你会学习怎样设计这个宏并扩充成正确的形势来处理是否是ZTS模式。
通常情况下,PHP的运行环境是关闭线程安全模式的,除非明确的被告知需要线程安全,或者通过可选的配置打开线程安全。
就算不需要,也会有有打开TSRM层,这样做的目的是确保代码在所有环境下都能正常运行。但这样会出现速度问题。
线程安全被打开时,一个tsrm_ls的指针会批向很多内部方法。这个指针用来区分数据属于哪个线程。你也许会重新回顾用SAMPLE_G()宏的方式。没有这个指针,执行的方法就不知道去符号表哪里去查找和写入变量值。也无法知道哪个方法正在被执行,也无法跟踪内部寄存器。这个指针保证线程正确处理页面请求。
该指针参数是可选的,在一组定义中。当ZTS无效时,这些定义没有作用。ZTS打开时,定义如下:
#define TSRMLS_D void ***tsrm_ls
#define TSRMLS_DC , void ***tsrm_ls
#define TSRMLS_C tsrm_ls
#define TSRMLS_CC , tsrm_ls
在非ZTS的环境中,看下面的代码第一行,有两个参数,一个int,一个 char。在ZTS环境中,有三个参数,一个int,一个char,一个void***。当你的程序调这个方法时候,需求传这个参数,但是只参在ZTS有效的环境下。第二行代码是一个实际的调用。
int php_myext_action(int action_id, char *message TSRMLS_DC);
php_myext_action(42, "The meaning of life" TSRMLS_CC);
通过在方法调用里包含一个特殊的变量,php_myext_action能够使用tsrm_ls的值和MYEXT_G()宏存取全局数据。在非ZTS环境里tsrm_ls是无效的,但这也没什么问题,因为MYEXT_G()宏以及其它的类似宏都不使用它。
现在想象一下,你正在使用一个新的扩展,并且在你的命令行或者apache1里它工作的不错。
static int php_myext_isset(char *varname, int varname_len)
{
zval **dummy;
if (zend_hash_find(EG(active_symbol_table),
varname, varname_len + 1,
(void**)&dummy) == SUCCESS) {
/* Variable exists */
return 1;
} else {
/* Undefined variable */
return 0;
}
}你很满意,因为所有都看起来不错。于是你打包这个扩展,并把它给另外一个办公室用于产品环境。沮丧的事发生了,他们说这个扩展无法正常编译。这是因为他们用的是apache2.0线程模式,ZTS是打开的。当编译器遇到EG()宏的时候,试图在本地范围内寻找tsrm_ls变量,但找不到,因为你根本没有声明它,也没有给你的方法传这个变量。
修复当然很容易,只要把TSRMLS_DC 加到php_myext_isset()的声明里,然后把TSRMLS_CC加到调用的每一行。很不幸,产品组不是特别相信你扩展的质量,决定推迟两周。
这里就要提到enable-maintainer-zts ,当编译PHP时增加这一行到./configure配置文件里,就会自动包含ZTS,就算你在命令行根本用不着它。通过这些你能避免一些不必要的错误。
注意:
在PHP4里enable-maintainer-zts被叫作enable-experimental-zts。注意你的PHP版本。
偶尔,不能把tsrm_ls指针传给需要它的方法。基本上是因为你的扩展连接了一个库,它使用了回调方法,并且不为返回一个虚拟指针提供空间。看下面的代码:
void php_myext_event_callback(int eventtype, char *message)
{
zval *event;
/* $event = array('event'=>$eventtype,
'message'=>$message) */
MAKE_STD_ZVAL(event);
array_init(event);
add_assoc_long(event, "type", eventtype);
add_assoc_string(event, "message", message, 1);
/* $eventlog[] = $event; */
add_next_index_zval(EXT_G(eventlog), event);
}
PHP_FUNCTION(myext_startloop)
{
/* The eventlib_loopme() function,
* exported by an external library,
* waits for an event to happen,
* then dispatches it to the
* callback handler specified.
*/
eventlib_loopme(php_myext_event_callback);
}
虽然不是所有代码都有意义,你可以看到回调方法使用了EXT_G()宏,该宏在线程环境下需要tsrm_ls指针。换一个方法也没用,因为这个扩展库不知道PHP的线程模式。那么在这种情况下tsrm_ls怎么使用呢。
解决方式是用一种叫做TSRMLS_FETCH()的Zend宏,把它放在代码的前面时,它会先基于当前线程环境检查并声明一个tsrm_ls指针的复本。
到处使用宏的确很吸引人,不用麻烦去传递tsrm_ls指针,但要注意,TSRMLS_FETCH()调用耗费一定时间。在单线程里不明显,但随着你的线程数增加,调用TSRMLS_FETCH()的实例数也会增加,你的扩展会出现瓶颈,所以要小心节约使用。
注意:
确保兼容C++编译器,也确保把TSRMLS_FETCH()和其它的声明放在所有代码段的前面。
概述:
在本章里接触到一些在后续章节要深入学习的概念。也有了一定关于扩展运行的基础,不只是表面的东西。但在介绍Zend引擎和TSRM层以后,你会发现在你的应用中扩展PHP的优势。