首頁  >  文章  >  後端開發  >  PHP-TSRM線程安全管理器-源碼分析

PHP-TSRM線程安全管理器-源碼分析

*文
*文原創
2017-12-21 17:08:531555瀏覽

在查看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 執行緒結構體、每個執行緒都有一份該結構體

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;  //当前线程结构体数量

字段說明

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

字段說明

size_t size : 资源大小
ts_allocate_ctor ctor: 构造函数指针、在给每一个线程创建该资源的时候会调用一下当前ctor指针
ts_allocate_dtor dtor : 析构函数指针、释放该资源的时候会调用一下当前dtor指针
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才可以,如果不是這種線程安全版本的,那就不會把這些公共資源都聚合到一堆,直接透過對應的名字取得就好了。

大概執行流程

核心初始化時 初始化TSRM 、註冊核心涉及到的公共資源、註冊外部擴充涉及到的公共資源。

對應的執行緒呼叫PHP解釋器函數入口位置,初始化目前執行緒的 公共資源資料。

需要那個公共資源就透過對應的資源id取得即可。

TSRM初始化結構圖


PHP-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解釋器,當每個線程結束的時候並不會馬上把當前線程創建的資源數據銷毀掉(因為有可能該線程又會馬上被使用到,就不用再重新初始化該線程對應所有的公共資源數據了, 直接就可以使用),而是等進程要結束的時候,才會遍歷所有線程,釋放所有的線程以及對應的資源資料。

原始碼註解

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核心初始化的時候調用,為了節省內存,預設都會是一個線程數和一個資源類型數,之後如果不夠用會進行擴容

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 所以反复調用該函數也會有性能損耗.

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);
}

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 是否线程安全版本.

扩展里公共资源定义:

//定义公共资源数据,替换之后就是一个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;

扩展里的资源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)非线程安全版本:则自动声明当前结构体变量,每次通过变量名获取资源就好了,因为不存在其他线程争抢的情况

扩展里获取公共资源数据

#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数据,如果不是则直接通过资源变量名字获取即可

扩展里初始化公共资源

//一般初始化公共资源数据,都会在扩展的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_宏定义.


相关阅读:

PHP中的TSRM及其宏的使用(线程安全管理)

php cgi与fpm关系

PHP CGI FastCGI php-fpm 解惑

以上是PHP-TSRM線程安全管理器-源碼分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn