源码阅读:PHP TSRM 线程安全管理器

@李彪  July 8, 2018

TSRM 简介

在查看php源代码或开发php扩展的时候,会出现大量 TSRMLS_ 宏字样在函数参数的位置,这些宏就是Zend为线程安全机制所提供的(Zend Thread Safety,简称ZTS)用于保证线程的安全 , 是防止多线程环境下以模块的形式加载并执行PHP解释器,导致内部一些公共资源读取错误,而提供的一种解决方法。

什么时候需要用 TSRM

只要服务器是多线程环境并且PHP以模块的形式提供,那么就需要TSRM启用,例如apache下的 worker 模式(多进程多线程)环境,这种情况就必须要使用线程安全版本的PHP,也就是要启用TSRM , 在Linux下是编译PHP的时候指定是否开启TSRM、windows下是提供线程安全版本和非线程安全版本的PHP。

PHP 如何实现 TSRM

正常多线程环境下操作公共的资源都是加上互斥锁,而PHP没有选择加锁,因为加锁可能多少会有些性能损耗,PHP的解决方法是为每一个线程都copy一份当前PHP内核所有的公共资源过来,每个线程指向自己的公共资源区,互不影响,各操作各的公共资源。

公共资源是什么

就是各种各样的 struct 结构体 定义。

TSRM数据结构

tsrm_tls_entry结构体

tsrm_tls_entry 线程结构体、每个线程都有一份该结构体。

typedef struct _tsrm_tls_entry tsrm_tls_entry;
struct _tsrm_tls_entry {
    void **storage;
    int count;
    THREAD_T thread_id;
    tsrm_tls_entry *next;
}
static tsrm_tls_entry   **tsrm_tls_table = NULL //线程指针表头指针
static int  tsrm_tls_table_size;  //当前线程结构体数量

字段说明

  1. void **storage :资源指针、就是指向自己的公共资源内存区
  2. int count : 资源数、就是 PHP内核 + 扩展模块 共注册了多少公共资源
  3. THREAD_T thread_id : 线程id
  4. tsrm_tls_entry *next:指向下一个线程指针,因为当前每一个线程指针都存在一个线程指针表里(类似于hash表),这个next可以理解成是hash冲突链式解决法.

tsrm_resource_type结构体

tsrm_resource_type 公共资源类型结构体、注册了多少公共资源就有多少个该结构体

typedef struct {
    size_t size;
    ts_allocate_ctor ctor;
    ts_allocate_dtor dtor;
    int done; 
} tsrm_resource_type;

static tsrm_resource_type   *resource_types_table=NULL;  //公共资源类型表头指针
static int  resource_types_table_size; //当前公共资源类型数量

字段说明

  1. size_t size : 资源大小
  2. ts_allocate_ctor ctor: 构造函数指针、在给每一个线程创建该资源的时候会调用一下当前ctor指针
  3. ts_allocate_dtor dtor : 析构函数指针、释放该资源的时候会调用一下当前dtor指针
  4. int done : 资源是否已经销毁 0:正常 1:已销毁

全局资源id

typedef int ts_rsrc_id;
static ts_rsrc_id id_count;

什么是全局资源id

TSRM 在注册公共资源的时候,会给每一个资源都生成一个唯一id,以后获取该资源时需指定对应的资源id。

为什么需要全局资源id

因为我们每个线程都会把当前注册的所有公共资源全部copy一份过来,也就是一个malloc()一个大数组,这个资源id就是该数组的索引,也就是要想获取对应的资源,需指定对应资源的id。

通熟易懂的说:
因为TSRM就是让每一个线程都指向自己的这一堆公共资源(数组),而想在这这一堆公共资源找到你想要的资源就要通过对应的资源id才可以,如果不是这种线程安全版本的,那就不会把这些公共资源都聚合到一堆,直接通过对应的名字获取就好了。

大概执行流程

  1. 内核初始化时 初始化TSRM 、注册内核涉及到的公共资源、注册外部扩展涉及到的公共资源。
  2. 对应的线程调用PHP解释器函数入口位置,初始化当前线程的 公共资源数据。
  3. 需要那个公共资源就通过对应的资源id获取即可。

TSRM初始化结构图

TSRM源码分析

TSRM源文件路径

/php-5.3.27/TSRM/TSRM.c
/php-5.3.27/TSRM/TSRM.h

TSRM涉及到主要的函数

初始化tsrm
tsrm_startup()

注册公共资源
ts_allocate_id()

获取、注册所有公共资源,不存在则初始化,返回 &storage 指针
#define TSRMLS_FETCH() void ***tsrm_ls = (void ***) ts_resource_ex(0, NULL)

通过指定资源id获取对应的资源
#define ts_resource(id) ts_resource_ex(id, NULL)

初始化当前线程,并copy已有的公共资源数据到storage指针
allocate_new_resource()

TSRM 一些常见的宏定义

#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则ZTS为真,那么这组TSRM宏就会被定义,常在扩展里面看到的函数参数列表的这些宏,就会被替换成void ***tsrm_ls 指针,实际上就是当前的线程调用该函数把该线程的公共资源区地址&storage**传递进去,以保证函数内部执行流程准确的获取对应线程的公共资源。

TSRM 大概的调用函数方式

调用
TSRMLS_FETCH() 替换 void ***tsrm_ls

执行
-> test(int a TSRMLS_CC) -> test_1(int b TSRMLS_CC)

替换
-> test(int a ,tsrm_ls) -> test_1(int b ,tsrm_ls)

TSRM 如何释放

上面说了apache的worker模式 多进程多线程,就是一个进程开多个线程调用PHP解释器,当每个线程结束的时候并不会马上把当前线程创建的资源数据销毁掉(因为有可能该线程又会马上被使用到,就不用再重新初始化该线程对应所有的公共资源数据了, 直接就可以使用),而是等进程要结束的时候,才会遍历所有线程,释放所有的线程以及对应的资源数据。

源代码注释

1. tsrm_startup 函数说明

TSRM_API int tsrm_startup(int expected_threads, int expected_resources, int debug_level, char *debug_filename)
{
    //省略...
    
    //默认线程数
    tsrm_tls_table_size = expected_threads;
    //创建tsrm_tls_entry指针数组
    tsrm_tls_table = (tsrm_tls_entry **) calloc(tsrm_tls_table_size, sizeof(tsrm_tls_entry *));
    //省略...
    
    //全局资源唯一ID初始化
    id_count=0;
    //默认资源类型数
    resource_types_table_size = expected_resources;
    //省略...
    
    //创建tsrm_resource_type结构体数组
    resource_types_table = (tsrm_resource_type *) calloc(resource_types_table_size, sizeof(tsrm_resource_type));
    //省略...
    
    return 1;
}

一般该函数在PHP内核初始化的时候调用,为了节省内存,默认都会是一个线程数和一个资源类型数,之后如果不够用会进行扩容。

2.ts_allocate_id 函数说明

TSRM_API ts_rsrc_id ts_allocate_id(ts_rsrc_id *rsrc_id, size_t size, ts_allocate_ctor ctor, ts_allocate_dtor dtor)
{
    int i;
    //省略...
    //生成当前资源的唯一id
    *rsrc_id = TSRM_SHUFFLE_RSRCidD(id_count++);
    TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Obtained resource id %d", *rsrc_id));
    
    //判断当前资源类型表是否小于当前资源数
    //如果小于则对资源类型表进行扩容
    if (resource_types_table_size < id_count) {
        resource_types_table = (tsrm_resource_type *) realloc(resource_types_table, sizeof(tsrm_resource_type)*id_count);
        //省略...
        resource_types_table_size = id_count;
    }
    //赋值公共资源的大小,构造函数和析构函数指针
    resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].size = size;
    resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].ctor = ctor;
    resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].dtor = dtor;
    resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].done = 0;
    
    //遍历说有的线程结构体,把当前创建的资源数据赋给storage指向的内存空间
    for (i=0; i<tsrm_tls_table_size; i++) {
        tsrm_tls_entry *p = tsrm_tls_table[i];
        
        //第一种情况
        //p有可能是null,因为还没有调用 TSRMLS_FETCH() 初始化线程结构体指针
        //所以 resource_types_table 就先暂时保存该资源的 size,之后等初始化
        //线程结构体指针的时候,会自动在创建该公共资源的内存空间,并赋值storage
        
        //第二种情况
        //已初始化对应的线程结构体指针,那么就直接根据当前新创建的资源id号对
        //p->storage进行扩容,因为资源id都是递增增加的,并根据当前资源的size
        //malloc创建具体的资源内存空间,创建完成之后回调一下ctor
        while (p) {
            if (p->count < id_count) {
                int j;

                p->storage = (void *) realloc(p->storage, sizeof(void *)*id_count);
                for (j=p->count; j<id_count; j++) {
                    p->storage[j] = (void *) malloc(resource_types_table[j].size);
                    if (resource_types_table[j].ctor) {
                        resource_types_table[j].ctor(p->storage[j], &p->storage);
                    }
                }
                //id_count每次+1 , 实际上就是我们公共资源的总数量
                p->count = id_count;
            }
            //指向下一个线程结构体指针
            p = p->next;
        }
    }
    //省略...
    //返回刚才id_count++
    return *rsrc_id;
}

当需要注册创建一个公共资源数据的时候就要调用该函数,一般都是在多线程环境下才会调用,也可看出来,该函数会遍历所有的线程结构体指针,并不断的ralloc和malloc 所以反复调用该函数也会有性能损耗.

3.TSRMLS_FETCH() -> ts_resource_ex 函数说明

TSRM_API void *ts_resource_ex(ts_rsrc_id id, THREAD_T *th_id)
{
    THREAD_T thread_id;
    int hash_value;
    tsrm_tls_entry *thread_resources;
    //省略...
    
    if(tsrm_tls_table) {
        //获取当前线程ID
        if (!th_id) {
            //省略...
            thread_id = tsrm_thread_id();
        } else {
            thread_id = *th_id;
        }

    TSRM_ERROR((TSRM_ERROR_LEVEL_INFO, "Fetching resource id %d for thread %ld", id, (long) thread_id));
    tsrm_mutex_lock(tsmm_mutex);
    
    #define THREAD_HASH_OF(thr,ts)  (unsigned long)thr%(unsigned long)ts
    //通过线程id和当前初始化线程数大小进行取模运算,算出当前线程指针位置因为
    //当前线程指针都存在tsrm_tls_table表里,如果当前位置已经存在一个线程指针
    //则 tsrm_tls_table->next 实际上就是一个hash冲突链式解决方法.
    hash_value = THREAD_HASH_OF(thread_id, tsrm_tls_table_size);
    thread_resources = tsrm_tls_table[hash_value];
    //如果不存在去创建当前线程,并将之前调用ts_allocate_id注册创建的那些公共资源
    //全部copy过来.
    if (!thread_resources) {
        allocate_new_resource(&tsrm_tls_table[hash_value], thread_id);
        return ts_resource_ex(id, &thread_id);
    } else {
         do {
            //判断线程id是否相等
            if (thread_resources->thread_id == thread_id) {
                break;
            }
            //如果不等于则next
            if (thread_resources->next) {
                thread_resources = thread_resources->next;
            } else {
               //如果不存在则还是去初始化创建当前线程
                allocate_new_resource(&thread_resources->next, thread_id);
                return ts_resource_ex(id, &thread_id);
            }
         } while (thread_resources);
    }
    //找到或创建完当前线程之后,返回当前线程公共资源区&storage指针 
    //如果指定资源id的话则返回 storage[id] 指针
    TSRM_SAFE_RETURN_RSRC(thread_resources->storage, id, thread_resources->count);
}

4.allocate_new_resource 函数说明

static void allocate_new_resource(tsrm_tls_entry **thread_resources_ptr, THREAD_T thread_id)
{
    int i;
    //thread_resources_ptr 
    //有可能是&tsrm_tls_table[hash_value]指针
    //有可能是&tsrm_tls_table[hash_value]->next指针,这种情况就是hash冲突了
    (*thread_resources_ptr) = (tsrm_tls_entry *) malloc(sizeof(tsrm_tls_entry));
    (*thread_resources_ptr)->storage = (void **) malloc(sizeof(void *)*id_count);
    (*thread_resources_ptr)->count = id_count;
    (*thread_resources_ptr)->thread_id = thread_id;
    (*thread_resources_ptr)->next = NULL;
    
    /* Set thread local storage to this new thread resources structure */
    tsrm_tls_set(*thread_resources_ptr);
    if (tsrm_new_thread_begin_handler) {
        tsrm_new_thread_begin_handler(thread_id, &((*thread_resources_ptr)->storage));
    }

    //这个循环就是把resource_types_table表里面的全部资源类型数据取出来
    //根据size大小创建具体的内存空间,并赋值给当前线程的storage
    //因为刚才调用ts_allocate_id这个函数,可能存在线程指针没有初始化的情况
    //所以只创建全局资源类型数据了,并没有创建具体的资源数据.
    for (i=0; i<id_count; i++) {
        if (resource_types_table[i].done) {
            (*thread_resources_ptr)->storage[i] = NULL;
        } else
        {
            (*thread_resources_ptr)->storage[i] = (void *) malloc(resource_types_table[i].size);
            if (resource_types_table[i].ctor) {
                resource_types_table[i].ctor((*thread_resources_ptr)->storage[i], &(*thread_resources_ptr)->storage);
            }
        }
    }  
    //调用该函数指针,复制配置信息并回调有配置callback函数的配置项来
    //填充当前线程对应的storage全局区
    if (tsrm_new_thread_end_handler) {
        tsrm_new_thread_end_handler(thread_id, &((*thread_resources_ptr)->storage));
    }
}

扩展TSRM使用

我们在开发扩展的时候也要按照线程安全版本去开发,通过 ZTS 宏判断当前 PHP 是否线程安全版本。

1.扩展里公共资源定义:

//定义公共资源数据,替换之后就是一个zend_模块名字的结构体
ZEND_BEGIN_MODULE_GLOBALS(module_name)
int id;
char name;
ZEND_END_MODULE_GLOBALS(module_name)
//对应的宏定义
#define ZEND_BEGIN_MODULE_GLOBALS(module_name)
    typedef struct _zend_##module_name##_globals {
#define ZEND_END_MODULE_GLOBALS(module_name)
} zend_##module_name##_globals;
//替换后
typedef struct _zend_module_name_globals {
   int id;
   char name;
} zend_module_name_globals;

2.扩展里的资源id定义

#ifdef ZTS
  #define ZEND_DECLARE_MODULE_GLOBALS(module_name)              
          ts_rsrc_id module_name##_globals_id;
#else
#define ZEND_DECLARE_MODULE_GLOBALS(module_name)                               
          zend_##module_name##_globals module_name##_globals;
#endif

(1) 线程安全版本:则自动声明全局资源唯一id,因为每个线程都会通过当前的id去storage指向内存区获取资源数据
(2)非线程安全版本:则自动声明当前结构体变量,每次通过变量名获取资源就好了,因为不存在其他线程争抢的情况

3.扩展里获取公共资源数据

#ifdef ZTS
    #define MODULE_G(v) TSRMG(xx_globals_id, zend_xx_globals *, v)
#else
    #define MODULE_G(v) (xx_globals.v)
#endif

如上每次获取资源全部通过自己定义的MODULE_G()宏获取,如果是线程安全则通过对应的TSRM管理器获取当前线程指定的资源id数据,如果不是则直接通过资源变量名字获取即可。

4.扩展里初始化公共资源

//一般初始化公共资源数据,都会在扩展的MINIT函数执行
//如果是ZTS则ts_allocate_id调用之.
PHP_MINIT_FUNCTION(myextension){
    #ifdef ZTS
       ts_allocate_id(&xx_globals_id,sizeof(zend_module_name_globals),ctor,dtor)
    #endif
}

结束语

上面介绍的就是PHP-TSRM线程安全管理器的实现,了解TSRM之后,无论是看内核源码还是开发PHP扩展都有很大的好处,因为内核和扩展里面充斥着大量的TSRM_宏定义。


评论已关闭