Press "Enter" to skip to content

揭秘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,那么就能保证在单线程和多线程模型下的线程安全,和代码一致性。:)

22 Comments

  1. jacob.ho
    jacob.ho July 27, 2020

    请问鸟哥,这样给每个线程都复制一份。如果一个线程修改这些公共的变量,zend是如何保持同步这些修改使其保持一致性?

  2. nabice
    nabice December 8, 2016

    感觉第一段代码有个错误:
    这是一族(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 //注意有个逗号
    最后一个应该是TSRMLS_DC吧。

  3. GenialX
    GenialX November 20, 2016

    非常感谢关于TSRM的分享,最近用php-cpp编译扩展出现了相关问题,‘tsrm_ls’ was not declared in this scope。如果有网友遇到类似问题,欢迎联系我讨论~

  4. pp
    pp September 25, 2015

    就是为key加了一个前缀?

  5. Wonder
    Wonder May 16, 2015

    感觉和TLS(线程本地存储)有点像

  6. bluex
    bluex April 3, 2014

    好文章,谢谢。学习了。。。

  7. cdy
    cdy March 23, 2013

    静态变量是如何处理的?如何线程同步?

  8. littlewhite
    littlewhite March 18, 2013

    你好,我对于模块和线程之间的关系还不是很清楚,我现在是这样理解的:
    每个线程会有一个tsrm_ls,一个线程可以有很多个模块,每个模块会在这个tsrm_ls里有个id表示其偏移量,在tsrm[id]中就是这个模块的全局变量
    不知道有没有理解错误

  9. zhongyi
    zhongyi September 13, 2012

    鸟哥,请教一个问题,在我的环境下,按照上面的代码创建了扩展,运行的时候,或报segmentation fault,我的环境是TS的。后来在stackoverflow(http://stackoverflow.com/questions/10200193/php-module-crashes-on-ts-allocate-dtor),找到了一个别人类似的问题,按照他的做法,增加在MSHUTDOWN里增加了ts_free_id,运行没有问题了,不过不太清楚是为什么会这样呢,如有时间恳请赐教,万分感谢。

  10. sam
    sam June 5, 2012

    v587

  11. redsun
    redsun February 14, 2012

    鸟哥,我编译levelDB的PHP扩展的时候遇到
    /server/php-leveldb/leveldb.cpp: In function ‘bool _create_object(zval**, zend_class_entry*)’:
    /server/php-leveldb/leveldb.cpp:372: 错误:‘tsrm_ls’在此作用域中尚未声明
    的错误, 我加了
    #include “TSRM/TSRM.h” 这个头文件,再次make的时候为什么还是报相同的错误

  12. 雪候鸟
    雪候鸟 June 6, 2011

    @liuzhiqiang 恩, 另外, 本文是为了解释TSRM所以设计了一些场景, 而一般的, 如果是模块的globals, 是不需要显示自己调用ts_allocate_id的, PHP的模块加载会做这一步工作

  13. liuzhiqiang
    liuzhiqiang June 6, 2011

    OK,这样就和代码中的逻辑对上了,多谢多谢啊。

  14. 雪候鸟
    雪候鸟 June 6, 2011

    @liuzhiqiang 当前线程的全局变量, 模块就是扩展, 每一个扩展都可以有自己的全局变量(不是语言意义的全局变量,而是module_globals)

  15. liuzhiqiang
    liuzhiqiang June 6, 2011

    tsrm_ls中放的是当前线程还是所有线程的全局变量?这里说的模块和线程之间是什么关系?

  16. 雪候鸟
    雪候鸟 June 6, 2011

    @liuzhiqiang 大体上是这样, 根据thread得到一个thread_id, 根据这个thread_id, 在一个全局的内存空间后给这个thread分配所有申明的全局变量大小总和的空间, 也就是tsrm_ls, 而在每一个模块初始化的时候, 都会分配一个模块id, 也就是对应在tsrm_ls中的偏移.

  17. liuzhiqiang
    liuzhiqiang June 5, 2011

    看了上面的例子,有个问题还是没弄明白,按照这个(((zend_myextension_globals*)(*((void ***)tsrm_ls))[(myextension_globals_id)-1])->v)的意思,myextension_globals是放在tsrm_ls里面的,可是这个是如何放进去的呢?
    php_myextension_globals_ctor这个函数只是在参数列表里面包含了void ***tsrm_ls,函数体只是初始化变量,并未讲变量村到tsrm_ls中啊。
    貌似可能是ts_allocate_id这个函数在完成这个动作,但是怎么实现的呢?
    如果真是这样的话,php_myextension_globals_ctor这个函数是不是就不许要第二个参数了? 即void ***tsrm_ls??
    不知道我理解的对不对,请指教啊,十分感谢!!

  18. jackywdx
    jackywdx October 26, 2008

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

Comments are closed.