TSRM 线程安全资源管理器
学习自:第三节 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_*
。