源码学习之线程安全,源码分析

从成效域上的话,C语言可以定义4种不一样的变量:全局变量,静态全局变量,局地变量,静态局地变量。

从功能域上的话,C语言可以定义4种不相同的变量:全局变量,静态全局变量,局地变量,静态局地变量。

在阅读PHP源码和读书PHP扩充开发的进度中,我接触到大方分包“TSRM”字眼的宏。通过查阅资料,知道这么些宏与Zend的线程安全部制有关,而大多数材料中都提出按照既定规则使用这一个宏就可以,而没有申明那个宏的实际效果。不通晓怎么回事总是令人不好受的,因而我透过阅读源码和查看有限的资料简要领悟一下相关机制,本文是本身对商讨内容的下结论。
本文首先解释了线程安全的定义及PHP中线程安全的背景,然后详细探讨了PHP的线程安全体制ZTS(Zend
Thread
Safety)及实际的落成TSRM,切磋内容囊括有关数据结构、完结细节及运行机制,最终研商了Zend对于单线程和三八线程环境的选择性编译难题。

TSRM 介绍

在查看php源代码或支付php扩大的时候,会油不过生大量 TSRMLS_
宏字样在函数参数的职位,那么些宏就是Zend为线程安全部制所提供的(Zend
Thread `Safety,简称ZTS)用于有限支撑线程的安全 ,
是幸免八线程环境下以模块的方式加载并履行PHP解释器,导致其中一些公家资源读取错误,而提供的一种缓解方法。

上边仅从函数作用域的角度分析一下区其余变量,假若所有变量声明不重名。

  • 全局变量,在函数外申明,例如,int gVar;。全局变量,所有函数共享,在任何地方出现那些变量名都是指那些变量

  • 静态全局变量(static sgVar),其实也是富有函数共享,可是这几个会有编译器的限制,算是编译器提供的一种效应

  • 有的变量(函数/块内的int var;),不共享,函数的反复实施中涉及的那一个变量都是互为独立的,他们只是重名的不等变量而已

  • 局地静态变量(函数中的static int sVar;),本函数间共享,函数的每一回实践中涉及的这几个变量都是这几个同一个变量

地点三种成效域都是从函数的角度来定义成效域的,可以满足所有大家对单线程编程中变量的共享意况。
现在大家来分析一下十二线程的情景。在三十二线程中,多少个线程共享除函数调用栈之外的别样资源。
因而上边二种成效域从概念来看就改成了。

  • 全局变量,所有函数共享,由此有着的线程共享,分化线程中冒出的两样变量都是那同一个变量

  • 静态全局变量,所有函数共享,也是持有线程共享

  • 有些变量,此函数的各次执行中涉嫌的那么些变量没有调换,由此,也是逐一线程间也是不共享的

  • 静态局地变量,本函数间共享,函数的每一次执行涉及的那个变量都是同一个变量,由此,各类线程是共享的

上面仅从函数功效域的角度分析一下见仁见智的变量,假使所有变量表明不重名。

  • 全局变量,在函数外表明,例如,int gVar;。全局变量,所有函数共享,在任哪里方出现那么些变量名都是指那个变量

  • 静态全局变量(static sgVar),其实也是拥有函数共享,不过那些会有编译器的限制,算是编译器提供的一种效应

  • 一对变量(函数/块内的int var;),不共享,函数的屡屡执行中提到的那几个变量都是互相独立的,他们只是重名的不等变量而已

  • 一些静态变量(函数中的static int sVar;),本函数间共享,函数的每四次实施中提到的那些变量都是那么些同一个变量

地点二种成效域都是从函数的角度来定义作用域的,可以餍足所有我们对单线程编程中变量的共享景况。
现在我们来分析一下三多线程的图景。在三十六线程中,八个线程共享除函数调用栈之外的其它资源。
因而上边两种作用域从概念来看就改为了。

  • 全局变量,所有函数共享,因而所有的线程共享,差距线程中出现的不比变量都是这同一个变量

  • 静态全局变量,所有函数共享,也是富有线程共享

  • 一些变量,此函数的各次执行中涉及的这些变量没有联系,因此,也是各类线程间也是不共享的

  • 静态局地变量,本函数间共享,函数的历次执行涉及的这么些变量都是同一个变量,因而,种种线程是共享的

线程安全

线程安全难题,简单来说就是二十四线程环境下何以安全存取公共资源。大家掌握,每个线程只具备一个私有栈,共享所属进程的堆。在C中,当一个变量被声称在其余函数之外时,就改成一个全局变量,那时那个变量会被分配到进程的共享存储空间,分歧线程都引用同一个地点空间,由此一个线程如果改动了这几个变量,就会影响到方方面面线程。这好像为线程共享数据提供了有益,可是PHP往往是各样线程处理一个呼吁,由此期望每个线程拥有一个全局变量的副本,而不愿意请求间互动烦扰。
早期的PHP往往用来单线程环境,每个进程只启动一个线程,由此不存在线程安全题材。后来面世了多线程环境下利用PHP的场地,因而Zend引入了Zend线程安全机制(Zend
Thread Safety,简称ZTS)用于有限支撑线程的安全。

怎么着时候需求用 TSRM

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

一、缘起TSRM

在四线程系统中,进度保留着资源所有权的属性,而七个冒出执行流是执行在进度中运行的线程。
如 Apache2 中的
worker,主控制进度生成多个子进度,每个子进程中隐含固定的线程数,各种线程独立地拍卖请求。
同样,为了不在请求到来时再生成线程,MinSpareThreads 和 马克斯SpareThreads
设置了至少和最多的空闲线程数; 而 马克斯Clients
设置了所有子进度中的线程总数。若是现有子进程中的线程总数不可以满意负荷,控制进程将派生新的子进度。

当 PHP 运行在如上好像的十六线程服务器时,此时的 PHP
处在多线程的生命周期中。
在一定的日子内,一个进度空间中会存在多少个线程,同一进度中的三个线程公用模块伊始化后的全局变量,
如果和 PHP 在 CLI
情势下同样运行脚本,则多个线程会估算读写一些储存在进度内存空间的公家资源(如在几个线程公用的模块初步化后的函数外会存在较多的全局变量),

那会儿这一个线程访问的内存地址空间相同,当一个线程修改时,会潜移默化其余线程,那种共享会升高部分操作的进程,
可是多少个线程间就发出了较大的耦合,并且当多个线程并发时,就会生出广泛的数额一致性难点或资源竞争等并发常见难题,
比如多次周转结果和单线程运行的结果不一样。倘若每个线程中对全局变量、静态变量唯有读操作,而无写操作,则这一个个全局变量就是线程安全的,只是那种状态不太现实。

为缓解线程的面世难题,PHP 引入了 TSRM: 线程安全资源管理器(Thread Safe
Resource Manager)。 TRSM 的已毕代码在 PHP 源码的 /TSRM
目录下,调用四处可知,日常,我们称为 TSRM 层。 一般的话,TSRM
层只会在被指明须要的时候才会在编译时启用(比如,Apache2+worker
MPM,一个依据线程的MPM), 因为 Win32 下的 Apache
来说,是根据二十四线程的,所以这些层在 Win32 下再三再四被开启的。

一、缘起TSRM

在八线程系统中,进度保留着资源所有权的属性,而八个冒出执行流是执行在经过中运作的线程。
如 Apache2 中的
worker,主控制进程生成三个子进度,每个子进度中涵盖固定的线程数,各个线程独立地拍卖请求。
同样,为了不在请求到来时再生成线程,MinSpareThreads 和 马克斯SpareThreads
设置了最少和最多的空闲线程数; 而 马克斯Clients
设置了所有子进度中的线程总数。若是现有子过程中的线程总数不可能满足负荷,控制进程将派生新的子进度。

当 PHP 运行在如上接近的多线程服务器时,此时的 PHP
处在三十二线程的生命周期中。
在一定的时日内,一个经过空间中会存在三个线程,同一进度中的多个线程公用模块开头化后的全局变量,
若是和 PHP 在 CLI
方式下同样运行脚本,则多少个线程会预计读写一些仓储在进程内存空间的公家资源(如在七个线程公用的模块起始化后的函数外会存在较多的全局变量),

那时那一个线程访问的内存地址空间相同,当一个线程修改时,会潜移默化其余线程,这种共享会提升部分操作的进程,
不过多少个线程间就生出了较大的耦合,并且当多个线程并发时,就会时有发生广泛的数量一致性难点或资源竞争等并发常见难点,
比如多次运作结果和单线程运行的结果分歧。假设每个线程中对全局变量、静态变量唯有读操作,而无写操作,则那一个个全局变量就是线程安全的,只是那种气象不太现实。

为化解线程的出现难题,PHP 引入了 TSRM: 线程安全资源管理器(Thread Safe
Resource Manager)。 TRSM 的落到实处代码在 PHP 源码的 /TSRM
目录下,调用四处可知,常常,大家誉为 TSRM 层。 一般的话,TSRM
层只会在被指明须要的时候才会在编译时启用(比如,Apache2+worker
MPM,一个按照线程的MPM), 因为 Win32 下的 Apache
来说,是基于二十四线程的,所以这些层在 Win32 下接二连三被启封的。

ZTS的基本原理及落实

PHP 怎么样促成 TSRM

健康十六线程环境下操作公共的资源都是加上互斥锁,而PHP没有选用加锁,因为加锁可能有些会微微品质损耗,PHP的化解方法是为每一个线程都copy一份当前PHP内核所有的共用资源苏醒,每个线程指向自己的公共资源区,互不影响,各操作各的集体资源。

二、TSRM的实现

进度保留着资源所有权的特性,线程做并发访问,PHP 中引入的 TSRM
层关怀的是对共享资源的走访,
那里的共享资源是线程之间共享的留存于经过的内存空间的全局变量。 当 PHP
在单进度形式下时,一个变量被声称在其他函数之外时,就改为一个全局变量。

先是定义了之类多少个越发紧要的全局变量(那里的全局变量是八线程共享的)。

/* The memory manager table */
static tsrm_tls_entry   **tsrm_tls_table=NULL;
static int              tsrm_tls_table_size;
static ts_rsrc_id       id_count;

/* The resource sizes table */
static tsrm_resource_type   *resource_types_table=NULL;
static int                  resource_types_table_size;

**tsrm_tls_table 的全拼 thread safe resource manager thread local
storage table,用来存放在各类线程的 tsrm_tls_entry 链表。

tsrm_tls_table_size 用来代表 **tsrm_tls_table 的大小。

id_count 作为全局变量资源的 id 生成器,是大局唯一且递增的。

*resource_types_table 用来存放在全局变量对应的资源。

resource_types_table_size 表示 *resource_types_table 的大小。

中间涉及到四个重点的数据结构 tsrm_tls_entry 和 tsrm_resource_type

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;// 下一个节点的指针
};

typedef struct {
    size_t size;// 被定义的全局变量结构体的大小
    ts_allocate_ctor ctor;// 被定义的全局变量的构造方法指针
    ts_allocate_dtor dtor;// 被定义的全局变量的析构方法指针
    int done;
} tsrm_resource_type;

当新增一个全局变量时,id_count 会自增1(加上线程互斥锁)。然后按照全局变量需求的内存、构造函数、析构函数生成对应的资源tsrm_resource_type,存入 *resource_types_table,再根据该资源,为各样线程的有着tsrm_tls_entry节点添加其对应的全局变量。

有了这一个大体的刺探,上边通过缜密分析 TSRM 环境的初阶化和资源 ID
的分配来掌握这一完整的进度。

二、TSRM的实现

进程保留着资源所有权的习性,线程做并发访问,PHP 中引入的 TSRM
层关心的是对共享资源的拜会,
这里的共享资源是线程之间共享的存在于经过的内存空间的全局变量。 当 PHP
在单进度格局下时,一个变量被声称在其余函数之外时,就改为一个全局变量。

第一定义了如下多少个极度关键的全局变量(那里的全局变量是三八线程共享的)。

/* The memory manager table */
static tsrm_tls_entry   **tsrm_tls_table=NULL;
static int              tsrm_tls_table_size;
static ts_rsrc_id       id_count;

/* The resource sizes table */
static tsrm_resource_type   *resource_types_table=NULL;
static int                  resource_types_table_size;

**tsrm_tls_table 的全拼 thread safe resource manager thread local
storage table,用来存放各种线程的 tsrm_tls_entry 链表。

tsrm_tls_table_size 用来表示 **tsrm_tls_table 的大小。

id_count 作为全局变量资源的 id 生成器,是全局唯一且递增的。

*resource_types_table 用来存放全局变量对应的资源。

resource_types_table_size 表示 *resource_types_table 的大小。

其间涉及到四个举足轻重的数据结构 tsrm_tls_entry 和 tsrm_resource_type

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;// 下一个节点的指针
};

typedef struct {
    size_t size;// 被定义的全局变量结构体的大小
    ts_allocate_ctor ctor;// 被定义的全局变量的构造方法指针
    ts_allocate_dtor dtor;// 被定义的全局变量的析构方法指针
    int done;
} tsrm_resource_type;

当新增一个全局变量时,id_count 会自增1(加上线程互斥锁)。然后根据全局变量须求的内存、构造函数、析构函数生成对应的资源tsrm_resource_type,存入 *resource_types_table,再根据该资源,为每个线程的具有tsrm_tls_entry节点添加其对应的全局变量。

有了这些大致的刺探,上边通过缜密分析 TSRM 环境的伊始化和资源 ID
的分配来掌握这一完好的经过。

着力思维

说起来ZTS的大旨理维是很直观的,不是就是内需各样全局变量在每个线程都具备一个副本吗?那自己就提供那样的体制:
在三三十二线程环境下,申请全局变量不再是简约声贝拉米(Bellamy)个变量,而是所有进度在堆上分配一块内存空间作为“线程全局变量池”,在进度启动时开首化这些内存池,每当有线程需求报名全局变量时,通过相应措施调用TSRM(Thread
Safe Resource
Manager,ZTS的实际完毕)并传递要求的参数(如变量大小等等),TSRM负责在内存池中分红相应内存区块并将那块内存的引用标识重临,那样下次那一个线程需求读写此变量时,就可以透过将唯一的引用标识传递给TSRM,TSRM将担当真正的读写操作。那样就完成了线程安全的全局变量。下图给出了ZTS原理的示意图:
澳门葡京备用网址 1Thread1和Thread2同属一个进度,其中分别须求一个大局变量Global
Var,TSRM为两者在线程全局内存池中(藏蓝色部分)各自分配了一个区域,并且经过唯一的ID举行标识,这样三个线程就可以透过TSRM存取自己的变量而互不苦恼。
上边通过实际的代码片段看一下Zend具体是怎么兑现这一个机制的。那里自己用的是PHP5.3.8的源码。
TSRM的完毕代码在PHP源码的“TSRM”目录下。

公家资源是什么样

即使充裕多彩的 struct 结构体 定义

TSRM 环境的初叶化

模块初叶化阶段,在相继 SAPI main 函数中通过调用 tsrm_startup 来初叶化
TSRM
环境。tsrm_startup 函数会传出五个更加重大的参数,一个是 expected_threads,表示预期的线程数,
一个是 expected_resources,表示预期的资源数。不一致的 SAPI
有两样的起始化值,比如mod_php5,cgi 那些都是一个线程一个资源。

TSRM_API int tsrm_startup(int expected_threads, int expected_resources, int debug_level, char *debug_filename)
{
    /* code... */

    tsrm_tls_table_size = expected_threads; // SAPI 初始化时预计分配的线程数,一般都为1

    tsrm_tls_table = (tsrm_tls_entry **) calloc(tsrm_tls_table_size, sizeof(tsrm_tls_entry *));

    /* code... */

    id_count=0;

    resource_types_table_size = expected_resources; // SAPI 初始化时预先分配的资源表大小,一般也为1

    resource_types_table = (tsrm_resource_type *) calloc(resource_types_table_size, sizeof(tsrm_resource_type));

    /* code... */

    return 1;
}

简短出里面已毕的多个举足轻重的做事,初阶化了 tsrm_tls_table
链表、resource_types_table 数组,以及
id_count。而那五个全局变量是富有线程共享的,完结了线程间的内存管理的一致性。

TSRM 环境的开头化

模块初始化阶段,在一一 SAPI main 函数中通过调用 tsrm_startup 来开始化
TSRM
环境。tsrm_startup 函数会流传五个可怜重大的参数,一个是 expected_threads,表示预期的线程数,
一个是 expected_resources,表示预期的资源数。分化的 SAPI
有两样的初步化值,比如mod_php5,cgi 这个都是一个线程一个资源。

TSRM_API int tsrm_startup(int expected_threads, int expected_resources, int debug_level, char *debug_filename)
{
    /* code... */

    tsrm_tls_table_size = expected_threads; // SAPI 初始化时预计分配的线程数,一般都为1

    tsrm_tls_table = (tsrm_tls_entry **) calloc(tsrm_tls_table_size, sizeof(tsrm_tls_entry *));

    /* code... */

    id_count=0;

    resource_types_table_size = expected_resources; // SAPI 初始化时预先分配的资源表大小,一般也为1

    resource_types_table = (tsrm_resource_type *) calloc(resource_types_table_size, sizeof(tsrm_resource_type));

    /* code... */

    return 1;
}

从简出其中已毕的多个主要的做事,伊始化了 tsrm_tls_table
链表、resource_types_table 数组,以及
id_count。而那三个全局变量是有着线程共享的,完成了线程间的内存管理的一致性。

数据结构

TSRM中比较根本的数据结构有三个:tsrm_tls_entry和tsrm_resource_type。上边先看tsrm_tls_entry。
tsrm_tls_entry定义在TSRM/TSRM.c中:

typedef struct _tsrm_tls_entry tsrm_tls_entry;    struct _tsrm_tls_entry {   void **storage;   int count;   THREAD_T thread_id;   tsrm_tls_entry *next;  }

每个tsrm_tls_entry结构负责表示一个线程的保有全局变量资源,其中thread_id存储线程ID,count记录全局变量数,next指向下一个节点。storage可以看成指针数组,其中每个元素是一个针对性本节点代表线程的一个全局变量。最后各样线程的tsrm_tls_entry被重组一个链表结构,并将链表头指针赋值给一个大局静态变量tsrm_tls_table。注意,因为tsrm_tls_table是一个原汁原味的全局变量,所以所有线程会共享那几个变量,那就兑现了线程间的内存管理一致性。tsrm_tls_entry和tsrm_tls_table结构的示意图如下:
澳门葡京备用网址 2tsrm_resource_type的内部结构相对简便易行一些:

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

上文说过tsrm_tls_entry是以线程为单位的(每个线程一个节点),而tsrm_resource_type以资源(或者说全局变量)为单位,每一趟一个新的资源被分配时,就会创立一个tsrm_resource_type。所有tsrm_resource_type以数组(线性表)的章程组成tsrm_resource_table,其下标就是以此资源的ID。每个tsrm_resource_type存储了此资源的深浅和布局、析构方法指针。某种程度上,tsrm_resource_table可以看做是一个哈希表,key是资源ID,value是tsrm_resource_type结构。

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

资源 ID 的分配

咱俩驾驭初叶化一个全局变量时索要使用 ZEND_INIT_MODULE_GLOBALS
宏(上面的数组增加的例证中会有认证),而其实际则是调用的
ts_allocate_id 函数在二十四线程环境下申请一个全局变量,然后回来分配的资源
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;

    TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Obtaining a new resource id, %d bytes", size));

    // 加上多线程互斥锁
    tsrm_mutex_lock(tsmm_mutex);

    /* obtain a resource id */
    *rsrc_id = TSRM_SHUFFLE_RSRC_ID(id_count++); // 全局静态变量 id_count 加 1
    TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Obtained resource id %d", *rsrc_id));

    /* store the new resource type in the resource sizes table */
    // 因为 resource_types_table_size 是有初始值的(expected_resources),所以不一定每次都要扩充内存
    if (resource_types_table_size < id_count) {
        resource_types_table = (tsrm_resource_type *) realloc(resource_types_table, sizeof(tsrm_resource_type)*id_count);
        if (!resource_types_table) {
            tsrm_mutex_unlock(tsmm_mutex);
            TSRM_ERROR((TSRM_ERROR_LEVEL_ERROR, "Unable to allocate storage for resource"));
            *rsrc_id = 0;
            return 0;
        }
        resource_types_table_size = id_count;
    }

    // 将全局变量结构体的大小、构造函数和析构函数都存入 tsrm_resource_type 的数组 resource_types_table 中
    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;

    /* enlarge the arrays for the already active threads */
    // PHP内核会接着遍历所有线程为每一个线程的 tsrm_tls_entry
    for (i=0; i<tsrm_tls_table_size; i++) {
        tsrm_tls_entry *p = tsrm_tls_table[i];

        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) {
                        // 最后对 p->storage[j] 地址存放的全局变量进行初始化,
                        // 这里 ts_allocate_ctor 函数的第二个参数不知道为什么预留,整个项目中实际都未用到过,对比PHP7发现第二个参数也的确已经移除了
                        resource_types_table[j].ctor(p->storage[j], &p->storage);
                    }
                }
                p->count = id_count;
            }
            p = p->next;
        }
    }

    // 取消线程互斥锁
    tsrm_mutex_unlock(tsmm_mutex);

    TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Successfully allocated new resource id %d", *rsrc_id));
    return *rsrc_id;
}

当通过 ts_allocate_id 函数分配全局资源 ID 时,PHP
内核会先加上互斥锁,确保生成的资源 ID
的绝无仅有,那里锁的职能是在时间维度将出现的始末变成串行,因为并发的根本难题就是光阴的题材。当加锁将来,id_count
自增,生成一个资源 ID,生成资源 ID 后,就会给当下资源 ID
分配存储的职位, 每一个资源都会储存在 resource_types_table
中,当一个新的资源被分配时,就会创设一个 tsrm_resource_type。 所有
tsrm_resource_type 以数组的艺术组成
tsrm_resource_table,其下标就是这么些资源的 ID。 其实大家可以将
tsrm_源码学习之线程安全,源码分析。resource_table 看做一个 HASH 表,key 是资源 ID,value 是
tsrm_resource_type 结构(任何一个数组都可以看成一个 HASH
表,假设数组的key 值有意义的话)。

在分配了资源 ID 后,PHP 内核会接着遍历所有线程为每一个线程的
tsrm_tls_entry 分配那么些线程全局变量须要的内存空间。
那里每个线程全局变量的大小在分级的调用处指定(也就是全局变量结构体的轻重缓急)。最后对地点存放的全局变量举行开头化。为此我画了一张图予以注脚

澳门葡京备用网址 3

上图中还有一个怀疑的地点,tsrm_tls_table 的因素是哪些添加的,链表是哪些落到实处的。大家把这么些难题先留着,前边会商量。

每三回的 ts_allocate_id 调用,PHP
内核都会遍历所有线程并为每一个线程分配相应资源,
借使那么些操作是在PHP生命周期的伏乞处理阶段举办,岂不是会再度调用?

PHP 考虑了那种意况,ts_allocate_id 的调用在模块初始化时就调用了。

TSRM 启动后,在模块发轫化进程中会遍历每个增添的模块开端化方法,
扩充的全局变量在扩大的兑现代码早先讲明,在 MINIT 方法中伊始化。
其在早先化时会知会 TSRM
申请的全局变量以及大小,那里所谓的打招呼操作实际就是眼前所说的
ts_allocate_id 函数。 TSRM
在内存池中分红并登记,然后将资源ID再次来到给伸张。

资源 ID 的分配

咱俩驾驭伊始化一个全局变量时索要运用 ZEND_INIT_MODULE_GLOBALS
宏(下边的数组扩张的事例中会有证实),而其实际则是调用的
ts_allocate_id 函数在多线程环境下申请一个全局变量,然后回来分配的资源
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;

    TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Obtaining a new resource id, %d bytes", size));

    // 加上多线程互斥锁
    tsrm_mutex_lock(tsmm_mutex);

    /* obtain a resource id */
    *rsrc_id = TSRM_SHUFFLE_RSRC_ID(id_count++); // 全局静态变量 id_count 加 1
    TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Obtained resource id %d", *rsrc_id));

    /* store the new resource type in the resource sizes table */
    // 因为 resource_types_table_size 是有初始值的(expected_resources),所以不一定每次都要扩充内存
    if (resource_types_table_size < id_count) {
        resource_types_table = (tsrm_resource_type *) realloc(resource_types_table, sizeof(tsrm_resource_type)*id_count);
        if (!resource_types_table) {
            tsrm_mutex_unlock(tsmm_mutex);
            TSRM_ERROR((TSRM_ERROR_LEVEL_ERROR, "Unable to allocate storage for resource"));
            *rsrc_id = 0;
            return 0;
        }
        resource_types_table_size = id_count;
    }

    // 将全局变量结构体的大小、构造函数和析构函数都存入 tsrm_resource_type 的数组 resource_types_table 中
    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;

    /* enlarge the arrays for the already active threads */
    // PHP内核会接着遍历所有线程为每一个线程的 tsrm_tls_entry
    for (i=0; i<tsrm_tls_table_size; i++) {
        tsrm_tls_entry *p = tsrm_tls_table[i];

        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) {
                        // 最后对 p->storage[j] 地址存放的全局变量进行初始化,
                        // 这里 ts_allocate_ctor 函数的第二个参数不知道为什么预留,整个项目中实际都未用到过,对比PHP7发现第二个参数也的确已经移除了
                        resource_types_table[j].ctor(p->storage[j], &p->storage);
                    }
                }
                p->count = id_count;
            }
            p = p->next;
        }
    }

    // 取消线程互斥锁
    tsrm_mutex_unlock(tsmm_mutex);

    TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Successfully allocated new resource id %d", *rsrc_id));
    return *rsrc_id;
}

当通过 ts_allocate_id 函数分配全局资源 ID 时,PHP
内核会先加上互斥锁,确保生成的资源 ID
的唯一,那里锁的效应是在岁月维度将应运而生的情节变成串行,因为并发的向来难题就是时间的题材。当加锁将来,id_count
自增,生成一个资源 ID,生成资源 ID 后,就会给当下资源 ID
分配存储的义务, 每一个资源都会储存在 resource_types_table
中,当一个新的资源被分配时,就会创制一个 tsrm_resource_type。 所有
tsrm_resource_type 以数组的艺术结合
tsrm_resource_table,其下标就是这几个资源的 ID。 其实大家得以将
tsrm_resource_table 看做一个 HASH 表,key 是资源 ID,value 是
tsrm_resource_type 结构(任何一个数组都得以看成一个 HASH
表,如若数组的key 值有意义的话)。

在分配了资源 ID 后,PHP 内核会接着遍历所有线程为每一个线程的
tsrm_tls_entry 分配那个线程全局变量须要的内存空间。
那里每个线程全局变量的高低在各自的调用处指定(也就是全局变量结构体的大小)。最终对地点存放的全局变量举行早先化。为此我画了一张图予以证实

澳门葡京备用网址 4

上图中还有一个狐疑的地方,tsrm_tls_table 的要素是何许添加的,链表是什么兑现的。我们把那几个难点先留着,前边会探讨。

每五遍的 ts_allocate_id 调用,PHP
内核都会遍历所有线程并为每一个线程分配相应资源,
假诺那一个操作是在PHP生命周期的呼吁处理阶段进行,岂不是会另行调用?

PHP 考虑了那种情况,ts_allocate_id 的调用在模块开端化时就调用了。

TSRM 启动后,在模块开头化进度中会遍历每个扩大的模块开首化方法,
扩充的全局变量在增添的兑现代码伊始注脚,在 MINIT 方法中初始化。
其在初步化时会知会 TSRM
申请的全局变量以及大小,那里所谓的通报操作实际就是前边所说的
ts_allocate_id 函数。 TSRM
在内存池中分红并登记,然后将资源ID重回给扩张。

已毕细节

这一小节分析TSRM一些算法的贯彻细节。因为整个TSRM涉及代码比较多,那里拣其中装有代表性的多个函数分析。
第三个值得注意的是tsrm_startup函数,那些函数在经过开端阶段被sapi调用,用于起先化TSRM的环境。由于tsrm_startup略长,那里摘录出自我觉着应当小心的地点:

/* Startup TSRM (call once for the entire process) */  TSRM_API int tsrm_startup(int expected_threads, int expected_resources, int debug_level, char *debug_filename)  {   /* code... */     tsrm_tls_table_size = expected_threads;     tsrm_tls_table = (tsrm_tls_entry **) calloc(tsrm_tls_table_size, sizeof(tsrm_tls_entry *));   if (!tsrm_tls_table) {    TSRM_ERROR((TSRM_ERROR_LEVEL_ERROR, "Unable to allocate TLS table"));    return 0;   }   id_count=0;     resource_types_table_size = expected_resources;   resource_types_table = (tsrm_resource_type *) calloc(resource_types_table_size, sizeof(tsrm_resource_type));   if (!resource_types_table) {    TSRM_ERROR((TSRM_ERROR_LEVEL_ERROR, "Unable to allocate resource types table"));    free(tsrm_tls_table);    tsrm_tls_table = NULL;    return 0;   }     /* code... */     return 1;  }

其实tsrm_startup的机要义务就是初叶化上文提到的三个数据结构。第三个相比较好玩的是它的前三个参数:expected_threads和expected_resources。那五个参数由sapi传入,表示推断的线程数和资源数,可以见到tsrm_startup会根据那七个参数预先分配空间(通过calloc)。由此TSRM会首先分配可容纳expected_threads个线程和expected_resources个资源的。要看各类sapi默许会传入什么,可以看种种sapi的源码(在sapi目录下),我大约看了眨眼间间:
澳门葡京备用网址 5可以见见相比较常用的sapi如mod_php5、php-fpm和cgi都是预分配一个线程和一个资源,那样是因为不愿浪费内存空间,而且多数意况下PHP如故运行于单线程环境。
那里还足以见见一个id_count变量,那些变量是一个大局静态变量,其效能就是通过自增爆发资源ID,这一个变量在那边被开端化为0。所以TSRM暴发资源ID的办法相当简单:就是一个整形变量的自增。
第一个需要仔细分析的就是ts_allocate_id,编写过PHP伸张的心上人对那么些函数肯定不陌生,这一个函数…

个人博客最新地址为www.codinglabs.org,阅读全文请点击

/* allocates a new thread-safe-resource 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;     TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Obtaining a new resource id, %d bytes", size));     tsrm_mutex_lock(tsmm_mutex);     /* obtain a resource id */   *rsrc_id = TSRM_SHUFFLE_RSRC_ID(id_count++);   TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Obtained resource id %d", *rsrc_id));     /* store the new resource type in the resource sizes table */   if (resource_types_table_size < id_count) {    resource_types_table = (tsrm_resource_type *) realloc(resource_types_table, sizeof(tsrm_resource_type)*id_count);    if (!resource_types_table) {     tsrm_mutex_unlock(tsmm_mutex);     TSRM_ERROR((TSRM_ERROR_LEVEL_ERROR, "Unable to allocate storage for resource"));     *rsrc_id = 0;     return 0;    }    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;     /* enlarge the arrays for the already active threads */   for (i=0; i<tsrm_tls_table_size; i++) {    tsrm_tls_entry *p = tsrm_tls_table[i];      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);       }      }      p->count = id_count;     }     p = p->next;    }   }   tsrm_mutex_unlock(tsmm_mutex);     TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Successfully allocated new resource id %d", *rsrc_id));   return *rsrc_id;  }

rsrc_id最后存放的就是新资源的ID。其实那几个函数的一对落实格局让我比较费解,首先是重回ID的法门。因为rsrc_id是按引入传入的,所以最终也就应该包含资源ID,那么最后浑然不用在return
*rsrc_id,可以重返一个预购整数表示成功或破产(例如1打响,0败北),那里有点费一遍事的情致,而且多了一遍寻址。其余“*rsrc_id
= TSRM_SHUFFLE_RSRC_ID(id_count++);
”让自家倍感很奇怪,因为TSRM_SHUFFLE_RSRC_ID被定义为“((rsrc_id)+1)”,那么那里展开就是:

*rsrc_id = ((id_count++)+1)

怎么不写成那样吗:

*rsrc_id = ++id_count

真是怪哉。 好的,且不论已毕是不是站得住,大家先延续研讨这么些函数吧。
首先要将id_count自增,生成一个新的资源ID,然后为那一个新资源创设一个tsrm_resource_type并放入resource_type_table,接着遍历所有线程(注意是拥有)为每一个线程的tsrm_tls_entry分配这么些线程全局变量须求的内存空间(p->storage[j]
= (void *) malloc(resource_types_table[j].size); )。
那里要求留意,对于每四回ts_allocate_id调用,Zend会遍历所有线程并为每一个线程分配相应资源,因为ts_allocate_id实际是在MINIT阶段被调用,而不是在哀求处理阶段被调用的。换言之,TSRM会在经过建立时统一分配好线程全局资源,关于那一个下文种专门讲述。
抽象来看,可以将全方位线程全局资源池看做一个矩阵,一个维度为线程,一个维度为id_count,由此任意时刻有所线程全局变量的多少为“线程数*id_count”。tsrm_tls_entry和tsrm_resource_type可以当做那几个矩阵在三个维度上的目录。
通过分析可以见到,每一趟调用ts_allocate_id的代价是很大的,由于ts_allocate_id并不曾优先分配算法,每回在id_count维度申请一个新的变量,就关乎三回realloc和N次malloc(N为线程数),申请M个全局变量的代价为:
2 * M * t(realloc) + N * M * t(malloc)
由此要尽量减弱ts_allocate_id的调用次数。正因这一个缘故,在PHP扩大开发中倡导将一个模块所需的全局变量注明为一个社团体然后五遍性申请,而并非分开报名。

字段表达

  1. void **storage :资源指针、就是指向自己的公共资源内存区
  2. int count : 资源数、就是 PHP内核 + 扩展模块 共注册了多少公共资源
  3. THREAD_T thread_id : 线程id
  4. 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; //当前公共资源类型数量

全局变量的应用

以正规化的数组扩张为例,首先会申明当前伸张的全局变量。

ZEND_DECLARE_MODULE_GLOBALS(array)

接下来在模块起头化时会调用全局变量早先化宏开首化
array,比如分配内存空间操作。

static void php_array_init_globals(zend_array_globals *array_globals)
{
    memset(array_globals, 0, sizeof(zend_array_globals));
}

/* code... */

PHP_MINIT_FUNCTION(array) /* {{{ */
{
    ZEND_INIT_MODULE_GLOBALS(array, php_array_init_globals, NULL);
    /* code... */
}

那里的扬言和开端化操作都是分别ZTS和非ZTS。

#ifdef ZTS

#define ZEND_DECLARE_MODULE_GLOBALS(module_name)                            \
    ts_rsrc_id module_name##_globals_id;

#define ZEND_INIT_MODULE_GLOBALS(module_name, globals_ctor, globals_dtor)   \
    ts_allocate_id(&module_name##_globals_id, sizeof(zend_##module_name##_globals), (ts_allocate_ctor) globals_ctor, (ts_allocate_dtor) globals_dtor);

#else

#define ZEND_DECLARE_MODULE_GLOBALS(module_name)                            \
    zend_##module_name##_globals module_name##_globals;

#define ZEND_INIT_MODULE_GLOBALS(module_name, globals_ctor, globals_dtor)   \
    globals_ctor(&module_name##_globals);

#endif

对于非ZTS的景况,直接申明变量,初叶化变量;对于ZTS情状,PHP内核会添加TSRM,不再是声称全局变量,而是用ts_rsrc_id代替,先河化时也不再是早先化变量,而是调用ts_allocate_id函数在多线程环境中给当下这几个模块申请一个全局变量并赶回资源ID。其中,资源ID变量名由模块名加global_id组成。

若是要调用当前扩张的全局变量,则使用:ARRAYG(v),这几个宏的概念:

#ifdef ZTS
#define ARRAYG(v) TSRMG(array_globals_id, zend_array_globals *, v)
#else
#define ARRAYG(v) (array_globals.v)
#endif

即使是非ZTS则平昔调用全局变量的特性字段,倘诺是ZTS,则须要通过TSRMG获取变量。

TSRMG的定义:

#define TSRMG(id, type, element) (((type) (*((void ***) tsrm_ls))[TSRM_UNSHUFFLE_RSRC_ID(id)])->element)

去掉这一堆括号,TSRMG宏的意趣就是从tsrm_ls中按资源ID获取全局变量,并再次来到对应变量的性质字段。

那么现在的题材是这几个 tsrm_ls 从哪儿来的?

全局变量的使用

以专业的数组伸张为例,首先会注明当前扩展的全局变量。

ZEND_DECLARE_MODULE_GLOBALS(array)

然后在模块开首化时会调用全局变量初阶化宏发轫化
array,比如分配内存空间操作。

static void php_array_init_globals(zend_array_globals *array_globals)
{
    memset(array_globals, 0, sizeof(zend_array_globals));
}

/* code... */

PHP_MINIT_FUNCTION(array) /* {{{ */
{
    ZEND_INIT_MODULE_GLOBALS(array, php_array_init_globals, NULL);
    /* code... */
}

此处的宣示和伊始化操作都是分别ZTS和非ZTS。

#ifdef ZTS

#define ZEND_DECLARE_MODULE_GLOBALS(module_name)                            \
    ts_rsrc_id module_name##_globals_id;

#define ZEND_INIT_MODULE_GLOBALS(module_name, globals_ctor, globals_dtor)   \
    ts_allocate_id(&module_name##_globals_id, sizeof(zend_##module_name##_globals), (ts_allocate_ctor) globals_ctor, (ts_allocate_dtor) globals_dtor);

#else

#define ZEND_DECLARE_MODULE_GLOBALS(module_name)                            \
    zend_##module_name##_globals module_name##_globals;

#define ZEND_INIT_MODULE_GLOBALS(module_name, globals_ctor, globals_dtor)   \
    globals_ctor(&module_name##_globals);

#endif

对于非ZTS的意况,直接表明变量,开首化变量;对于ZTS景况,PHP内核会添加TSRM,不再是声称全局变量,而是用ts_rsrc_id代替,起先化时也不再是起头化变量,而是调用ts_allocate_id函数在八线程环境中给当下以此模块申请一个全局变量并重回资源ID。其中,资源ID变量名由模块名加global_id组成。

假设要调用当前伸张的全局变量,则利用:ARRAYG(v),这么些宏的概念:

#ifdef ZTS
#define ARRAYG(v) TSRMG(array_globals_id, zend_array_globals *, v)
#else
#define ARRAYG(v) (array_globals.v)
#endif

若果是非ZTS则一向调用全局变量的习性字段,若是是ZTS,则需求通过TSRMG获取变量。

TSRMG的定义:

#define TSRMG(id, type, element) (((type) (*((void ***) tsrm_ls))[TSRM_UNSHUFFLE_RSRC_ID(id)])->element)

去掉这一堆括号,TSRMG宏的情致就是从tsrm_ls中按资源ID获取全局变量,并重回对应变量的性质字段。

那么现在的题材是那一个 tsrm_ls 从哪儿来的?

ZTS与生命周期

那边须求简单提一下PHP的生命周期。
PHP的切实可行生命周期方式取决于sapi的兑现,但貌似都会有MINIT、RINIT、SCRIPT、RSHUTDOWN和MSHUTDOWN八个典型阶段,差距的只是各样阶段的施行次数差距。例如在CLI或CGI形式下,那四个等级依次执行两次,而在Apache或法斯特CGI形式下屡次一个MINIT和MSHUTDOWN中间对应多少个RINIT、SCRIPT、RSHUTDOWN。关于PHP生命周期的话题我回头写文单独啄磨,这里只是简单说一下。
MINIT和MSHUTDOWN是PHP
Module的初步化和清理阶段,往往在经过伊始后和甘休前进行,在那三个级次依次模块的MINIT和MSHUTDOWN方法会被调用。而RINIT、SCRIPT、RSHUTDOWN是每两次呼吁都会触发的一个小周期。在多线程格局中,PHP的生命周期如下:
澳门葡京备用网址 6在这种形式下,进度启动后仅执行四回MINIT。之所以要强调那一点,是因为TSRM的全局变量资源分配就是在MINIT阶段落成的,后续阶段只收获而不会再请求新的全局变量,那就简单明白为何在ts_allocate_id中每次id_count加一内需遍历所有线程为种种线程分配相同的资源。到此地,终于得以看清TSRM分配线程全局变量的全貌:
进度启动后,在MINIT阶段启动TSRM(通过sapi调用tsrm_startup),然后在遍历模块时调用每一个模块的MINIT方法,模块在MINIT中报告TSRM要提请多少全局变量及大小(通过ts_allocate_id),TSRM在内存池中分红并搞好登记办事(tsrm_tls_table和resource_types_table),然后将凭证(资源ID)重回给模块,告诉模块然后拿着那个证据来取你的全局变量。

字段表明

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

tsrm_ls 的起头化

tsrm_ls 通过 ts_resource(0) 初叶化。展开实际最终调用的是 ts_resource_ex(0,NULL) 。下面将 ts_resource_ex 一些宏展开,线程以 pthread 为例。

#define THREAD_HASH_OF(thr,ts)  (unsigned long)thr%(unsigned long)ts

static MUTEX_T tsmm_mutex;

void *ts_resource_ex(ts_rsrc_id id, THREAD_T *th_id)
{
    THREAD_T thread_id;
    int hash_value;
    tsrm_tls_entry *thread_resources;

    // tsrm_tls_table 在 tsrm_startup 已初始化完毕
    if(tsrm_tls_table) {
        // 初始化时 th_id = NULL;
        if (!th_id) {

            //第一次为空 还未执行过 pthread_setspecific 所以 thread_resources 指针为空
            thread_resources = pthread_getspecific(tls_key);

            if(thread_resources){
                TSRM_SAFE_RETURN_RSRC(thread_resources->storage, id, thread_resources->count);
            }

            thread_id = pthread_self();
        } else {
            thread_id = *th_id;
        }
    }
    // 上锁
    pthread_mutex_lock(tsmm_mutex);

    // 直接取余,将其值作为数组下标,将不同的线程散列分布在 tsrm_tls_table 中
    hash_value = THREAD_HASH_OF(thread_id, tsrm_tls_table_size);
    // 在 SAPI 调用 tsrm_startup 之后,tsrm_tls_table_size = expected_threads
    thread_resources = tsrm_tls_table[hash_value];

    if (!thread_resources) {
        // 如果还没,则新分配。
        allocate_new_resource(&tsrm_tls_table[hash_value], thread_id);
        // 分配完毕之后再执行到下面的 else 区间
        return ts_resource_ex(id, &thread_id);
    } else {
         do {
            // 沿着链表逐个匹配
            if (thread_resources->thread_id == thread_id) {
                break;
            }
            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);
    }

    TSRM_SAFE_RETURN_RSRC(thread_resources->storage, id, thread_resources->count);

    // 解锁
    pthread_mutex_unlock(tsmm_mutex);

}

而 allocate_new_resource 则是为新的线程在相应的链表中分配内存,并且将有着的全局变量都投入到其 storage 指针数组中。

static void allocate_new_resource(tsrm_tls_entry **thread_resources_ptr, THREAD_T thread_id)
{
    int i;

    (*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;

    // 设置线程本地存储变量。在这里设置之后,再到 ts_resource_ex 里取
    pthread_setspecific(*thread_resources_ptr);

    if (tsrm_new_thread_begin_handler) {
        tsrm_new_thread_begin_handler(thread_id, &((*thread_resources_ptr)->storage));
    }

    for (i=0; i<id_count; i++) {
        if (resource_types_table[i].done) {
            (*thread_resources_ptr)->storage[i] = NULL;
        } else {
            // 为新增的 tsrm_tls_entry 节点添加 resource_types_table 的资源
            (*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);
            }
        }
    }

    if (tsrm_new_thread_end_handler) {
        tsrm_new_thread_end_handler(thread_id, &((*thread_resources_ptr)->storage));
    }

    pthread_mutex_unlock(tsmm_mutex);
}

上边有一个知识点,Thread Local Storage
,现在有一全局变量 tls_key,所有线程都足以运用它,改变它的值。
表面上看起来那是一个全局变量,所有线程都得以动用它,而它的值在每一个线程中又是单身存储的。那就是线程本地存储的意义。
那么怎么着促成线程本地存储吗?

急需联合 tsrm_startupts_resource_exallocate_new_resource 函数并配以注释一起举例表明:

// 以 pthread 为例
// 1. 首先定义了 tls_key 全局变量
static pthread_key_t tls_key;

// 2. 然后在 tsrm_startup 调用 pthread_key_create() 来创建该变量
pthread_key_create( &tls_key, 0 ); 

// 3. 在 allocate_new_resource 中通过 tsrm_tls_set 将 *thread_resources_ptr 指针变量存入了全局变量 tls_key 中
tsrm_tls_set(*thread_resources_ptr);// 展开之后为 pthread_setspecific(*thread_resources_ptr);

// 4. 在 ts_resource_ex 中通过 tsrm_tls_get() 获取在该线程中设置的 *thread_resources_ptr 
//    多线程并发操作时,相互不会影响。
thread_resources = tsrm_tls_get();

在精晓了 tsrm_tls_table 数组和其中链表的创导之后,再看 ts_resource_ex 函数中调用的那几个再次回到宏

#define TSRM_SAFE_RETURN_RSRC(array, offset, range)     \
    if (offset==0) {                                    \
        return &array;                                  \
    } else {                                            \
        return array[TSRM_UNSHUFFLE_RSRC_ID(offset)];   \
    }

就是按照传入 tsrm_tls_entry 和 storage 的数组下标 offset ,然后回来该全局变量在该线程的 storage数组中的地址。到此地就知道了在三十二线程中拿走全局变量宏 TSRMG 宏定义了。

实则那在大家写增加的时候会平常利用:

#define TSRMLS_D void ***tsrm_ls   /* 不带逗号,一般是唯一参数的时候,定义时用 */
#define TSRMLS_DC , TSRMLS_D       /* 也是定义时用,不过参数前面有其他参数,所以需要个逗号 */
#define TSRMLS_C tsrm_ls
#define TSRMLS_CC , TSRMLS_C

NOTICE 写扩大的时候可能过多同班都分不清楚到底用哪一个,通过宏展开我们能够见见,他们各自是带逗号和不带逗号,以及表达及调用,那么匈牙利(Hungary)语中“D”就是意味着:Define,而
前面的”C”是 Comma,逗号,前面的”C”就是Call。

如上为ZTS形式下的定义,非ZTS方式下其定义全部为空。

tsrm_ls 的初阶化

tsrm_ls 通过 ts_resource(0) 开头化。展开实际最终调用的是 ts_resource_ex(0,NULL) 。下面将 ts_resource_ex 一些宏展开,线程以 pthread 为例。

#define THREAD_HASH_OF(thr,ts)  (unsigned long)thr%(unsigned long)ts

static MUTEX_T tsmm_mutex;

void *ts_resource_ex(ts_rsrc_id id, THREAD_T *th_id)
{
    THREAD_T thread_id;
    int hash_value;
    tsrm_tls_entry *thread_resources;

    // tsrm_tls_table 在 tsrm_startup 已初始化完毕
    if(tsrm_tls_table) {
        // 初始化时 th_id = NULL;
        if (!th_id) {

            //第一次为空 还未执行过 pthread_setspecific 所以 thread_resources 指针为空
            thread_resources = pthread_getspecific(tls_key);

            if(thread_resources){
                TSRM_SAFE_RETURN_RSRC(thread_resources->storage, id, thread_resources->count);
            }

            thread_id = pthread_self();
        } else {
            thread_id = *th_id;
        }
    }
    // 上锁
    pthread_mutex_lock(tsmm_mutex);

    // 直接取余,将其值作为数组下标,将不同的线程散列分布在 tsrm_tls_table 中
    hash_value = THREAD_HASH_OF(thread_id, tsrm_tls_table_size);
    // 在 SAPI 调用 tsrm_startup 之后,tsrm_tls_table_size = expected_threads
    thread_resources = tsrm_tls_table[hash_value];

    if (!thread_resources) {
        // 如果还没,则新分配。
        allocate_new_resource(&tsrm_tls_table[hash_value], thread_id);
        // 分配完毕之后再执行到下面的 else 区间
        return ts_resource_ex(id, &thread_id);
    } else {
         do {
            // 沿着链表逐个匹配
            if (thread_resources->thread_id == thread_id) {
                break;
            }
            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);
    }

    TSRM_SAFE_RETURN_RSRC(thread_resources->storage, id, thread_resources->count);

    // 解锁
    pthread_mutex_unlock(tsmm_mutex);

}

而 allocate_new_resource 则是为新的线程在相应的链表中分配内存,并且将有所的全局变量都投入到其 storage 指针数组中。

static void allocate_new_resource(tsrm_tls_entry **thread_resources_ptr, THREAD_T thread_id)
{
    int i;

    (*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;

    // 设置线程本地存储变量。在这里设置之后,再到 ts_resource_ex 里取
    pthread_setspecific(*thread_resources_ptr);

    if (tsrm_new_thread_begin_handler) {
        tsrm_new_thread_begin_handler(thread_id, &((*thread_resources_ptr)->storage));
    }

    for (i=0; i<id_count; i++) {
        if (resource_types_table[i].done) {
            (*thread_resources_ptr)->storage[i] = NULL;
        } else {
            // 为新增的 tsrm_tls_entry 节点添加 resource_types_table 的资源
            (*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);
            }
        }
    }

    if (tsrm_new_thread_end_handler) {
        tsrm_new_thread_end_handler(thread_id, &((*thread_resources_ptr)->storage));
    }

    pthread_mutex_unlock(tsmm_mutex);
}

地点有一个知识点,Thread Local Storage
,现在有一全局变量 tls_key,所无线程都可以行使它,改变它的值。
表面上看起来那是一个全局变量,所有线程都能够选择它,而它的值在每一个线程中又是独自存储的。那就是线程本地存储的含义。
那么如何促成线程本地存储吗?

急需联合 tsrm_startupts_resource_exallocate_new_resource 函数并配以注释一起举例表明:

// 以 pthread 为例
// 1. 首先定义了 tls_key 全局变量
static pthread_key_t tls_key;

// 2. 然后在 tsrm_startup 调用 pthread_key_create() 来创建该变量
pthread_key_create( &tls_key, 0 ); 

// 3. 在 allocate_new_resource 中通过 tsrm_tls_set 将 *thread_resources_ptr 指针变量存入了全局变量 tls_key 中
tsrm_tls_set(*thread_resources_ptr);// 展开之后为 pthread_setspecific(*thread_resources_ptr);

// 4. 在 ts_resource_ex 中通过 tsrm_tls_get() 获取在该线程中设置的 *thread_resources_ptr 
//    多线程并发操作时,相互不会影响。
thread_resources = tsrm_tls_get();

在精通了 tsrm_tls_table 数组和内部链表的创建之后,再看 ts_resource_ex 函数中调用的那一个再次回到宏

#define TSRM_SAFE_RETURN_RSRC(array, offset, range)     \
    if (offset==0) {                                    \
        return &array;                                  \
    } else {                                            \
        return array[TSRM_UNSHUFFLE_RSRC_ID(offset)];   \
    }

就是按照传入 tsrm_tls_entry 和 storage 的数组下标 offset ,然后回来该全局变量在该线程的 storage数组中的地址。到那边就清楚了在三十二线程中赢得全局变量宏 TSRMG 宏定义了。

实则那在我们写增加的时候会平日利用:

#define TSRMLS_D void ***tsrm_ls   /* 不带逗号,一般是唯一参数的时候,定义时用 */
#define TSRMLS_DC , TSRMLS_D       /* 也是定义时用,不过参数前面有其他参数,所以需要个逗号 */
#define TSRMLS_C tsrm_ls
#define TSRMLS_CC , TSRMLS_C

NOTICE 写增加的时候恐怕过多同室都分不清楚到底用哪一个,通过宏展开大家得以观察,他们各自是带逗号和不带逗号,以及表明及调用,那么西班牙王国(The Kingdom of Spain)语中“D”就是代表:Define,而
前面的”C”是 Comma,逗号,前面的”C”就是Call。

上述为ZTS格局下的概念,非ZTS情势下其定义全体为空。

ZTS在单线程和四线程环境的选拔性编译

上文说过,很多气象下PHP照旧被用于单线程环境,那时倘若仍旧依据上述行为,显著过于折腾。因为在单线程环境下不存在线程安全题材,全局变量只要简单注明使用就好,没需要搞那么一大堆动作。PHP的设计者考虑到的那或多或少,允许在编译时指定是不是打开多线程支持,唯有当在configure是指定–enable-maintainer-zts选项或启用四线程sapi时,PHP才会编译线程安全的代码。具体来说,当启用线程安全编译时,一个叫ZTS的常量被定义,PHP代码在各类与线程安全相关的地点通过#ifdef检查是或不是编译线程安全代码。
在商量相关细节前自己先说有些和好的意见,对于ZTS多线程和单线程环境拔取性编译设计上,我个人认为是丰硕失利的。因为美丽的规划应该隔离变化,换言之ZTS有任务将选用性编译相关的东西隔离起来,而不让其污染到模块的编辑,这一个机制对模块开发相应是晶莹的。不过ZTS的设计者就如生怕大家不领悟有其一东西,让其完全污染了整套PHP,模块开发者不得不面对一堆奇奇怪怪的TSRM宏,着实令人十分忧伤。所以上边我就带着悲痛的心怀切磋一下那块内容。
为了看看模块是什么样兑现选拔性编译代码的,大家建立一个空的PHP扩张模块。到PHP源码的ext目录下执行如下命令:

./ext_skel --extname=zts_research

ext_skel是一个脚手架程序,用于创设PHP扩张模块。此时相会到ext目录下多了个zts_research目录。ext_skel为怎么生成了一个模块的主义,并顺便了累累提醒性注释。在这些目录下找到php_zts_research.h并打开,相比有意思的是一念之差一段代码:

/*       Declare any global variables you may need between the BEGIN      and END macros here:         ZEND_BEGIN_MODULE_GLOBALS(zts_research)      long  global_value;      char *global_string;  ZEND_END_MODULE_GLOBALS(zts_research)  */

很强烈那里提示了定义全局变量的艺术:用ZEND_BEGIN_MODULE_GLOBALS和ZEND_END_MODULE_GLOBALS七个宏包住有所全局变量。下边看一下那四个宏,那八个宏定义在Zend/zend_API.h文件里:

#define ZEND_BEGIN_MODULE_GLOBALS(module_name)  \   typedef struct _zend_##module_name##_globals {  #define ZEND_END_MODULE_GLOBALS(module_name)  \   } zend_##module_name##_globals;

原先这四个宏只是将一个模块的兼具全局变量封装为一个结构体定义,名称为zend_module_name_globals。关于为啥要封装成结构体,上文有关系。
php_zts_research.h此外比较有趣的一处就是:

#ifdef ZTS  #define ZTS_RESEARCH_G(v) TSRMG(zts_research_globals_id, zend_zts_research_globals *, v)  #else  #define ZTS_RESEARCH_G(v) (zts_research_globals.v)  #endif

zts_research_globals是zts_research模块全局变量结构的变量名称,类型为zend_module_name_globals,在哪定义的稍后会商讨。那里ZTS_RESEARCH_G就是以此模块获取全局变量的宏,如若ZTS没有概念(非线程安全时),就直接从这一个协会中得到相应字段,假如线程安全开启时,则利用TSRMG那一个宏。

#define TSRMG(id, type, element) (((type) (*((void ***) tsrm_ls))[TSRM_UNSHUFFLE_RSRC_ID(id)])->element)

那些宏就不具体细究了,因为其实太难懂了,基本思想就是使用上文提到的TSRM机制从线程全局变量池中得到相应的数码,其中tsrm_ls可以视作是线程全局变量池的指针,获取变量的凭证就是资源ID。
看到此间恐怕还有点晕,例如zts_research_globals这么些变量哪来的?zts_research_globals_id又是哪来的?为了弄清那个题材,必要打开ext/zts_research/zts_research.c这些文件,其中有这般的代码:

/* If you declare any globals in php_zts_research.h uncomment this:  ZEND_DECLARE_MODULE_GLOBALS(zts_research)  */

提醒很精晓,假若在php_zts_research.h中定义了别的全局变量则将那段代码的申明消除,看来这一个ZEND_DECLARE_MODULE_GLOBALS宏就是生死攸关了。然后在Zend/zend_API中有那般的代码:

#ifdef ZTS    #define ZEND_DECLARE_MODULE_GLOBALS(module_name)       \   ts_rsrc_id module_name##_globals_id;    /* code... */    #else    #define ZEND_DECLARE_MODULE_GLOBALS(module_name)       \   zend_##module_name##_globals module_name##_globals;    /* code... */    #endif

当线程安全开启时,那里其实定义了一个整形的资源ID(ts_rsrc_id
被typedef定义为int),而当线程安全不开启时,则直接定义一个结构体。在这么些模块中分别对应zts_research_globals_id和zts_research_globals。
到那里思路基本理顺了:倘若ZTS没有被启用,则直接声惠氏个全局变量结构体,并直接通过存取其字段完成全局变量存取;借使ZTS开启,则定义一个整形变量作为资源ID,然后经过ts_allocate_id函数向TSRM申请一块内存放置结构体(须求程序员手工在MINIT函数中落成,脚手架生成的次第中从不),并因而TSRM存取数据。
最终一个疑难:tsrm_ls在哪儿?固然通过上述方法从TSRM中取数据,那么自然要通晓线程全局变量内存池的指针tsrm_ls。那就是本身说过的最污染PHP的地点,假使您读书过PHP源码或编辑过模块,对以下四个宏肯定眼熟:TSRMLS_D,TSRMLS_DC,TSRMLS_DTSRMLS_C,TSRMLS_CC。实际在PHP内部每趟定义方法或调用方法时都要在参数列表末了加上中间的一个宏,其实就是为着将tsrm_ls传给函数以便存取全局变量,那八个宏的概念如下:

#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

澳门葡京备用网址 ,在未曾打开ZTS时,三个宏被定义为空,但此时在概念PHP方法或调用方法时照旧将宏加在参数列表后,这是为了维持代码的一致性,当然,因为在非ZTS环境下根本不会用到tsrm_ls,所以并未任何难题。

大局资源id

typedef int ts_rsrc_id;
static ts_rsrc_id id_count;

参考资料

  • 究竟怎么着是TSRMLS_CC?-
    54chen
  • 深切钻研PHP及Zend
    Engine的线程安全模型

 

本文来源:

参考资料

  • 究竟怎么是TSRMLS_CC?-
    54chen
  • 浓厚钻研PHP及Zend
    Engine的线程安全模型

 

本文来源:

总结

正文商量了PHP和Zend的线程安全模型,应该说自家个人觉得Zend内核中ZTS的落实巧妙但不够优雅,但近来在支付PHP模块时总免不了常与之对立。那块内容相对偏门,大概从未资料对ZTS和TSRM进行详尽解释,不过透彻通晓ZTS机制对于在PHP模块开发中正确合理利用全局变量是很要紧的。希望本文对读者有所协理。


怎么是大局资源id

TSRM
在登记公共资源的时候,会给每一个资源都生成一个唯一id,未来获得该资源时需点名相应的资源id。

怎么要求全局资源id

因为大家每个线程都会把最近注册的装有公共资源总体copy一份过来,也就是一个malloc()一个天机组,那几个资源id就是该数组的目录,也就是要想得到相应的资源,需点名对应资源的id。

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

大概执行流程

  1. 根本开首化时 起头化TSRM
    、注册内核涉及到的公物资源、注册外部增添涉及到的国有资源。
  2. 相应的线程调用PHP解释器函数进口地点,开首化当前线程的 公共资源数量。
  3. 亟待万分公共资源就通过相应的资源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_宏定义.

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*
*
Website