msgbartop
PHP语言, PHP扩展, Zend引擎相关的研究,技术,新闻分享 – 左手代码 右手诗
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: ,

19 Responses to “揭秘TSRM(Introspecting TSRM)”

  1. nabice |

    感觉第一段代码有个错误:

    这是一族(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吧。

  2. GenialX |

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

  3. pp |

    就是为key加了一个前缀?

  4. Wonder |

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

  5. pandora compatible sterling crownea |

    This site truly has all the information I wanted
    about this subject and didn’t know who to ask.

  6. bluex |

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

  7. cdy |

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

  8. littlewhite |

    你好,我对于模块和线程之间的关系还不是很清楚,我现在是这样理解的:

    每个线程会有一个tsrm_ls,一个线程可以有很多个模块,每个模块会在这个tsrm_ls里有个id表示其偏移量,在tsrm[id]中就是这个模块的全局变量

    不知道有没有理解错误

  9. zhongyi |

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

  10. sam |

    v587

  11. redsun |

    鸟哥,我编译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. 雪候鸟 |

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

  13. liuzhiqiang |

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

  14. 雪候鸟 |

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

  15. liuzhiqiang |

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

  16. 雪候鸟 |

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

  17. liuzhiqiang |

    看了上面的例子,有个问题还是没弄明白,按照这个(((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. Tweets that mention 揭秘TSRM(Introspecting TSRM) | 风雪之隅 -- Topsy.com |

    [...] This post was mentioned on Twitter by MC.Spring, sagasw. sagasw said: LuaPython都有类似的概念Env,不算稀奇 RT @mcspring http://bit.ly/9hCPj6 看了 @laruence 同学的这篇文章才明白PHP中的TSRM鱼TSRMLS_?簇名字的含义,缩写真的是太强大了,特别是在C语言中 [...]

  19. jackywdx |

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

Leave a Reply

*