msgbartop
PHP源码分析,Zend引擎分析,Web相关技术研究,Web技术分享–左手代码 右手诗
msgbarbottom

03 Aug 08 揭秘TSRM(Introspecting TSRM)

如果你曾经做过PHP的扩展,或者研究过PHP的源码,你就会看到这个东西到处都在。但是关于这个东西是什么,却鲜有资料叙及。

对于这个东西是什么,最常见的回答就是“你不用关心这个是什么,你只要在‘这里’‘那里’用上就是了,如果编译器告诉你缺少tsrm_ls,加上就好了 ”。这个答案虽然是一种很敷衍的回答,但其实也是有一定道理的,因为Zend Engine把这个宏搞的太复杂,并且对于一个初学PHP扩展的开发者来说,了解它是什么也没有太大的益处。
而我是一个喜欢追根究底的人。所以,如果你现在刚好比较闲,并有耐性了解这个东西是什么,那么就请继续读下去。

名词解释:
TSRM
线程安全资源管理器(Thread Safe Resource Manager),这是个尝尝被忽视,并很少被人说起的“层”(layer), 她在PHP源码的/TSRM目录下。一般的情况下,这个层只会在被指明需要的时候才会被启用(比如,Apache2+worker MPM,一个基于线程的MPM),对于Win32下的Apache来说,是基于多线程的,所以这个层在Win32下总是被启用的。
ZTS
Zend线程安全(Zend Thread Safety),当TSRM被启用的时候,就会定义这个名为ZTS的宏。
tsrm_ls
TSRM存储器(TSRM Local Storage),这个是在扩展和Zend中真正被实际使用的指代TSRM存储的变量名。
TSRMLS_??
这是一族(4个)宏,用来根据ZTS宏被定义与否来实现TSRM。4个宏如下:

#define TSRMLS_C   tsrm_ls
#define TSRMLS_D   void  *** tsrm_ls
#define TSRMLS_CC  ,tsrm_ls
#define TSRMLS_DS  ,void  ***tsrm_ls   //注意有个逗号
 

我们都知道,在C或者PHP编程中,要在多个函数中访问同一个变量有俩种方式,一种是通过参数传递,比如下面的代码:

    #include <stdio.h>

    void output_func(char *message)
    {
        printf("%s\n", message);
    }

    int main(int argc, char *argv[])
    {
        output_func(argv[0]);

        return 0;
    }
 

另外一种方式是,通过在函数的高一级作用域中存储这个变量(当然,对于PHP,要显示的指明Global变量(这个原因和PHP的作用域的实现-活动表有关系,本处不涉及,我会在将来的某篇文章中介绍她),如:

    #include <stdio.h>

    char *message;

    void output_func(void)
    {
        printf("%s\n", message);
    }

    int main(int argv, char *argv[])
    {
        message = argv[0];
        output_func();

        return 0;
}

对于在PHP使用第二种方式来说,一般的单线 程模型比如PHP CLI方式,Apache1,或者Apache2+prefork MPM(也是一种多进程模型),可以放心的被使用,也不会出错。全局变量在MINIT/RINIT的时候被创建,然后在整个进程运行时/请求处理期都能被 访问到,然后在MSHUTDOW/RSHUTDOWN的时候被释放。
但是在多线程的模型下,这种方式就不在安全了,比如Apache2+worker MPM和IIS。在这种情况下,所有的线程共享同一个进程的地址空间,也就说,多个线程共用一个全局变量,这个时候就会产生竞争。用C程序员的方式来说: 这个时候的全局变量是非线程安全的。
为了解决这个问题,并和单线程模式兼容,Zend使用了称作“Non_global Globals”的机制。这个机制的主要思想就是,对于多线程模型来说,每当一个新的线程被创建,就单独的分配一块内存,这块内存存储着一个全局变量的副 本。而这块内存会被一个Vector串起来,由Zend统一管理。为了说明这个方式,咱们看看如下的例子:

    typedef struct _zend_myextension_globals {
        int foo;
        char *bar;
    } zend_myextension_globals;

    #ifdef ZTS  //如果TSRM被启用
    int myextension_globals_id;
    #else
    zend_myextension_globals myextension_globals;
    #endif

    /* 当线程被创建的时候调用 */
    static void php_myextension_globals_ctor(zend_myextension_globals *myext_globals TSRMLS_DC)
    {
        myext_globals->foo = 0;
        myext_globals->bar = NULL;
    }

    /* 线程结束的时候被调用 */
    static void php_myextension_globals_dtor(zend_myextension_globals *myext_globals TSRMLS_DC)
    {
        if (myext_globals->bar) {
            efree(myext_globals->bar);
        }
    }

    PHP_MINIT_FUNCTION(myextension)
    {
    #ifdef ZTS
        ts_allocate_id(&myextension_globals_id, sizeof(zend_myextension_globals),
                       php_myextension_globals_ctor, php_myextension_globals_dtor);
    #else
        php_myextension_globals_ctor(&myextension_globals TSRMLS_CC);
    #endif

        return SUCCESS;
    }

    PHP_MSHUTDOWN_FUNCTION(myextension)
    {
    #ifndef ZTS
        php_myextension_globals_dtor(&myextension_globals TSRMLS_CC);
    #endif

        return SUCCESS;
    }

这个例子开始的时候向TSRM层申明了一个全局变量” zend_myextension_globals”,

        ts_allocate_id(&myextension_globals_id, sizeof(zend_myextension_globals),
                       php_myextension_globals_ctor, php_myextension_globals_dtor);

他指明了要申请的全局变量的大小,创建器和析构器。并讲这个生成的全局变量在Vector中的偏移量(Index)保存在了myextension_globals_id中。而对于没有启用TSRM的情况,这个全局变量只是简单的被创建。
如果你问我“为什么在没有启用TSRM的情况下还会有TSRMLS_CC?”,那说明你现在还没有被我弄糊涂;),恩,在ZTS没有被设置的情况下(没有启用TSRM),TSRMLS_CC会被编译器替换为空,因为:

     #ifdef ZTS
              #define TRSMLS_CC  ,tsrm_ls
  #else
      #define TSRMLS_CC
  #endif

在没有启用TSRM的情况下还指明TSRMLS_CC的原因仅仅是为了保持代码的一致性。

恩,现在已经设置了全局变量,那么接下来的问题就是,我们如果去访问它呢?看看如下的代码:

    #ifdef ZTS
    # define   MYEXTENSION_G(v)     \
                 (((zend_myextension_globals*)(*((void ***)tsrm_ls))[(myextension_globals_id)-1])->v)
    #else
    # define   MYEXTENSION_G(v)     (myextension_globals.v)
    #endif
 

呵呵,明白了吧? 在ZTS没有被设置的情况下,宏MYEXTENSION_G(V)简单的被等价于全局变量myextension_globals.v,而对于启用了TSRM的情况,MYEXTENSION_G(V)会被转化成在Vector中根据my_extension_globals_id来查找到要访问的全局变量。
现在,只要你在你的代码中,使用MYEXTENSION_G来访问你的全局变量,并在要使用这个全局变量的函数参数列表中添加上TSRMLS_CC,那么就能保证在单线程和多线程模型下的线程安全,和代码一致性。:)

Related Posts:

Tags: ,

Reader's Comments

  1. |

    哦,原来这样,前两天正一直在查ZTS这个东东呢,结果一直查不到。呵呵~

Leave a Comment

*
To prove you're a person (not a spam script), type the security word shown in the picture. Click on the picture to hear an audio file of the word.
Click to hear an audio file of the anti-spam word