30 March 2014

学习自:第三节 PHP中的线程安全深入研究PHP及Zend Engine的线程安全模型究竟什么是TSRMLS_CC?

综合以上三篇文章,再看一下 PHP 源码才明白 TSRM 是怎么回事,这里做个学习记录。

线程安全


当PHP运行于多线程服务器时,处理请求的生命周期如下图:

在没有 TSRM 的时候,将会存在非线程安全问题,线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

在PHP的进程中,存在许多全局变量,比如许多符号表。

TSRM


PHP解决并发的思路非常简单,既然存在资源竞争,那么直接规避掉此问题, 将多个资源直接复制多份,多个线程竞争的全局变量在进程空间中各自都有一份,各做各的,完全隔离。线程通过 TSRM 对自己拥有的全局变量进行访问,如下图(这是一个较高级别的抽象,详细情况会在下文讨论):

与 TSRM 有关的重要的数据结构:

1
2
3
4
5
6
7
8
typedef struct _tsrm_tls_entry tsrm_tls_entry;

struct _tsrm_tls_entry {
    void **storage;//存放全局变量的数组
    int count;//全局变量数目
    THREAD_T thread_id;//线程id
    tsrm_tls_entry *next;//下一个线程
}

每一个tsrm_tls_entry对应一个线程。多个tsrm_tls_entry链接在一起,形成tsrm_tls_table,这是一个静态全局变量,定义如下:

1
static tsrm_tls_entry **tsrm_tls_table=NULL;//TSRM/TSRM.c 42

1
2
3
4
5
6
typedef struct {
    size_t size;//资源大小
    ts_allocate_ctor ctor;//资源构造函数
    ts_allocate_dtor dtor;//资源析构函数
    int done;//资源是否被释放了
} tsrm_resource_type;

tsrm_resource_type以资源(或者说全局变量)为单位,每次一个新的资源被分配时,就会创建一个tsrm_resource_type。所有tsrm_resource_type以数组(线性表)的方式组成tsrm_types_table,其下标就是这个资源的ID。该数据结构主要用于 TSRM 为每个线程创建/释放资源。

资源id的分配


当通过ts_allocate_id函数分配全局资源ID时,PHP内核会锁一下,确保生成的资源ID的唯一, 这里锁的作用是在时间维度将并发的内容变成串行,因为并发的根本问题就是时间的问题。

当加锁以后,id_count自增,生成一个资源ID,生成资源ID后,就会给当前资源ID分配存储的位置, 每一个资源都会存储在tsrm_types_table中,当一个新的资源被分配时,就会创建一个tsrm_resource_type。 每次所有tsrm_resource_type以数组的方式组成tsrm_types_table,其下标就是这个资源的ID。 其实我们可以将tsrm_types_table看做一个HASH表,key是资源ID,value是tsrm_resource_type结构。 只是,任何一个数组都可以看作一个HASH表,如果数组的key值有意义的话。id_count的定义如下(是一个全局静态变量):

1
static ts_rsrc_id id_count;

在分配了资源ID后,ts_allocate_id函数会接着遍历所有线程为每一个线程的tsrm_tls_entry分配这个资源需要的内存空间。

那么在何时调用ts_allocate_id呢?如果在 RINIT 阶段调用那么将会导致重复多次分配全局资源,且性能将下降许多,而且浪费大量内存。ts_allocate_id函数的代码实现就是为了在 MINIT 阶段来分配全局资源的。所以我们应该在 MINIT 阶段调用该函数。

了解资源分配过程的图示:

分配资源后,访问资源


在代码中,我们可以通过FG(user_stream_current_filename)访问当前线程拥有的全局资源的 value ,这是standard/file.c中的源码。FG 的定义为:

1
2
3
4
5
6
7
#ifedf ZTS
#define FG(v) TSRMG(file_globals_id, php_file_globals *, v)
extern PHPAPI int file_globals_id;
#else
#define FG(v) (file_globals.v)
extern PHPAPI php_file_globals file_globals;
#endif

如果启用了ZTS,那么FG(user_stream_current_filename)会被展开位:

TSRMG(file_globals_id, php_file_globals *, user_stream_current_filename)

接着被展开为:

(((php_file_globals *) (*((void ***) tsrm_ls))[TSRM_UNSHUFFLE_RSRC_ID(file_globals_id)])->user_stream_current_filename)

最后展开为:

(((php_file_globals *) (*((void ***) tsrm_ls))[((file_globals_id)-1)])->user_stream_current_filename)

从而获得线程相应的全局资源。

那么tsrm_ls从何而来,为什么在函数中可以使用它?先看下面宏定义(在TSRM/TSRM.h文件中):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifdef ZTS
...//省略部分代码
#define TSRMLS_D void ***tsrm_ls//在函数声明或定义的时候使用,
#define TSRMLS_DC , TSRMLS_D//同上,就是多了个逗号
#define TSRMLS_C tsrm_ls//在函数调用的时候使用
#define TSRMLS_CC , TSRMLS_C//同上,就是多了个逗号
...
#else
...
#define TSRMLS_D void
#define TSRMLS_DC
#define TSRMLS_C
#define TSRMLS_CC

#endif

所以tsrm_ls是作为参数传递到我们的函数中的。在非 ZTS 的情况下,TSRMLS_*的定义都为空。

最后个问题,tsrm_ls是从什么时候开始出现的,从哪里来?答案就在php_module_startup函数中,在PHP内核的模块初始化时, 如果是ZTS模式,则会定义一个局部变量tsrm_ls,这就是我们线程安全开始的地方。

main/main.c中:

1
2
3
4
5
6
7
8
9
10
...//省略部分代码
#ifdef ZTS
    zend_executor_globals *executor_globals;
    void ***tsrm_ls;
    php_core_globals *core_globals;
#endif
...
#ifdef ZTS
    tsrm_ls = ts_resource(0);
#endif

ts_resource又是什么?在TSRM/TSRM.h文件中:

1
#define ts_resource(id)                     ts_resource_ex(id, NULL)

ts_resource_ex函数在TSRM/TSRM.c中,它会返回当前线程相应的tsrm_tls_entry的成员属性storage的地址。

其他


从上面的分析可以看出,tsrm_ls一直都是作为函数参数传递的。

在C语言中,如果要在不同的函数传递相同的数据快,那么有两种方法:

  • 通过函数的参数传递;
  • 全局变量。

所以,当我们在编写PHP扩展时,为了能够兼容开起了ZTS的PHP,如果我们的函数中使用到了全局变量,那么就应该添加TSRMLS_*