用于线程局部储存数据结构的版本
I
(见图
1
),作为
IA-64 ABI
的一部分发展而来。作为崭新的定义,兼容不是问题。用于线程
t
的线程寄存器,由
tpt
表示。它指向一个线程控制块(
TCB
),在它偏移为
0
的位置,包含了一个指向该线程的动态线程向量(
dynamic thread
vector
)
dtvt
的指针。
动态线程向量在其第一个域包含了一个世代号(
generation
number
)
gent
,它用于
dtvt
的延迟调整(
deferred resizing
)及下面描述的
TLS
块的分配。而其它域包含了,指向不同的载入模块的,
TLS
块的指针。在启动时刻载入模块的
TLS
块直接跟在
TCB
后,因而具有一个,因架构而异,从线程指针地址开始的,固定偏移。对于所有一开始就存在的模块,在程序启动后,任意
TLS
块到
TCB
的偏移(因而线程局部变量)必须是固定的。
版本
II
具有相似的结构。唯一的区别在于,线程指针指向一个未指定大小及内容的线程控制块。
TCB
的某处包含了一个指向动态线程向量的指针,但未指出某处是何处。这由运行时环境操控,并且该指针不能被假定为可直接访问;编译器不允许产生直接访问
dtvt
的代码。
用于执行映像本身,及在启动时加载的所有模块的
TLS
块,都在线程指针所指向地址之下。这允许编译器产生直接访问这块内存的代码。通过动态线程向量访问
TLS
块也是可能的,它具有与版本
I
相同的结构,但它亦相对于线程指针,有在程序启动后即固定的偏移。在链接时刻,执行映像本身的
TLS
数据的偏移是已知的。
在程序启动时刻,为主线程构建了
TCB
连同动态线程向量。每个模块的
TLS
块的位置,通过使用架构特定的公式,根据各自
TLS
块的尺寸及对齐要求(
tlssizex
及
alignx
)来计算。在架构特定的段中,该公式将使用一个函数“
round
”,它返回第一个参数取整到其第二个参数整数倍的值:
round (x, y) = y *
┌
x/y
┐
TLS
块的内存不需要马上就分配。这依赖于模块编译使用的是静态或动态模式,而不管这是否有必要。如果使用的是静态模式,在程序启动时刻,由动态链接器根据重定位来计算地址(更准确些,到线程指针
tpt
的偏移),并且编译器产生,直接使用这些偏移来查找变量地址的,代码。在这个情形下,内存必须被马上分配。在动态模式中,查找变量地址被推迟到一个由运行时环境提供的名为
__tls_get_addr
的函数中。这个函数也可以分配及初始化必要的内存。
3.1.
启动及之后
对于使用线程局部储存的程序,启动代码必须在转交控制权之前,为初始线程设置内存。在静态链接的应用中,对线程局部储存的支持是有限的。某些平台(像
IA-64
)没有在
ABI
中定义静态链接(如果支持也不是标准的),其他平台像
Sun
,不鼓励使用静态链接,因为只有有限的功能可用。在任何情况下,在静态链接的代码中,动态加载模块受到很大的限制,甚至是不可能的。因此,处理线程局部储存要简单得多,因为只存在一个模块——执行映像本身。
在动态链接的代码中,处理线程局部储存则要有趣得多。在这个情形下,动态链接器必须包括对这种数据段处理的支持。动态加载使用线程局部储存的代码所提出的要求,在下一节中描述。
为了给线程局部储存设立内存,动态链接器从
PT_TLS
程序头项(参见表
2
)获取关于每个模块的线程局部储存的信息。收集所有模块的信息,可以通过一个包含如下内容的记录的链表来处理:
一个指向
TLS
初始化映像的指针,
TLS
初始化映像的大小,
模块的
tlsoffsetm
显示模块是否使用静态
TLS
模式的标识(仅当架构支持静态
TLS
模式)。
当动态加载另外的模块时,这个链表可以被延长(参见下一节),并且它将被线程库用来为新创建的线程设置
TLS
块。还有可能合并初始模块集中的两个或更多的初始化记录,以缩短这个链表。
如果所有的
TLS
内存要在启动时刻分配,其总尺寸将是
tlssizes
= tlsoffsetM
+
tlssizeM
,其中
M
是启动时刻的模块数目。不需要马上分配所有的内存,除非有一个模块是以静态模式编译的。如果所有的模块都使用动态模式,就可能推迟分配。一个优化的实现将不会盲目地追随,显示静态模式使用情况的标志。如果所要求的内存不大,就不值得推迟分配,这样甚至可能节省时间及资源。
正如在本节开头解释的那样,一个在线程局部储存中的变量,由一个模块的引用及
TLS
块中的偏移所指定。给定动态线程向量数据结构,我们可以把模块引用定义作一个以
1
开始的整数,它可以被用作
dtvt
数组的索引。每个模块接收到的数字由运行时环境决定。只是执行映像本身必须收到一个固定的数,
1
,并且其他加载的模块接收到的数不相同。
因此计算一个
TLS
变量的线程特定地址,是一个简单的操作,它可以由编译器使用版本
I
产生的代码来执行。但是遵循版本
II
架构的编译器不能这样做,不这样做也有一个很好的理由:延迟分配(参见下面)。
作为替代,定义了名为
__tls_get_addr
的函数,理论上它被像这样实现(这是这个函数在
IA-64
上的形式;其它架构可能使用不同的接口):
void *
__tls_get_addr (size_t m, size_t offset)
{
char *tls_block = dtv
[thread_id][m];
return tls_block + offset;
}
如何放置向量
dtv[thread_id]
是特定于架构的。描述
ABI
架构相关部分的章节将给出一些例子。应该把表达式
dtv[thread_id]
视为该进程的一个符号化的表示。
m
是模块
ID
,在该模块(应用本身或一个
DSO
)加载的时候,由动态链接器分配。
使用
__tls_get_addr
函数,还对实现动态模式带来额外的好处,这个模式把
TLS
块的分配推迟到第一次使用。对此,我们只要使用一个特殊的值填写
dtv[thread_id]
向量,这个值能与其它普通的值区分,并且它很可能表示一个空的项。改变
__tls_get_addr
的实现来完成这个额外的工作很简单:
void
*
__tls_get_addr (size_t m, size_t offset)
{
char *tls_block = dtv[thread_id][m];
if (tls_block ==
UNALLOCATED_TLS_BLOCK)
tls_block =
dtv[thread_id][m] = allocate_tls (m);
return tls_block + offset;
}
函数
allocate_tls
需要确定模块
m
的
TLS
所要求的内存,并恰当地初始化它。正如第二节所描述的,有两种数据:已初始化及未初始化。当模块
m
被加载时,已初始化的数据必须从重定位后的初始化映像中拷贝。未初始化的数据必须被置为
0
。一个实现可能看起来像这样:
void
*
allocate_tls (size_t m)
{
void *mem = malloc
(tlssize[m]);
memset (mempcpy (mem,
tlsinit_image[m], tlsinit_size[m]),
‘/0’, tlssize[m] –
tlsinit_size[m]);
return mem;
}
tlssize[m]
,
tlsinit_size[m]
及
tlsinit_image[m]
必须以一个依赖于实现的方式来确定。在模块
m
被加载后,它们都是已知的。注意到同样的映像
tlsinit_image[m]
被用于所有的线程,在它们创建的时候。一个线程不从其父亲处继承这个数据。
存储数据结构的这两个版本都允许使用静态模式。以这个方式编译的模块可以由动态段(
dynamic section
)的
DT_FLAGS
项的
DF_STATIC_TLS
标志来识别。如果这样的一个模块是初始模块集的一部分(记住,这样的模块不能被动态加载),用于
TLS
块的内存必须马上为启动时刻的初始线程,及为以后每个新创建的线程分配。否则,分配可被推迟,并且把
dtvt
的元素设置为一个由实现定义的值(上面的例子中是
UNALLOCATED_TLS_BLOCK
)。
3.2.
动态加载
模块的动态加载增加了更多的复杂性。首先,不应该限制,在某一时刻能被加载的,使用线程局部储存的模块数目,这意味着在需要时,
dtvt
数组可以被延长。其次,要绝对地避免内存泄露。当优化实现的速度时,必须要牢牢记住这一点。当释放一个被卸载模块的
TLS
块的内存时,浮现了速度问题。动态线程向量中的槽,迟早会被重用的。不这样做意味着,当加载新的模块时,总是延长这个向量。
因为释放及重新分配内存代价高昂,尤其是必须为每个线程都这样做,通过循环使用内存希望避免这个代价。但是如果同一个模块多次加载、卸载,必须不会导致内存泄露。
现在实现的限制已经明确了,必须描述需要执行的工作。动态加载包含线程局部储存的模块要求,为应用使用的,使用了这个内存的,当前及将来运行线程,进行准备。注意到加载本身不使用线程局部储存的模块,不管程序余下的部分是否使用线程局部储存,不要求特别的关注。新
TLS
块的信息必须被加入初始化记录链表中,并且增加已加载模块的计数
M
。除了今后被创建的线程,已经在运行的线程也要做准备。
加载一个新模块可以导致,为给定线程分配的动态线程向量可能太小,这样的结果。这就是每个
dtvt
中的世代计数
gent
所要检测的。如果访问这个向量,首先做的第一件事是确定世代数目是最新的,如果不是,分配一个更大的向量。尽管理论上,这可以由创建新线程的线程(或新线程本身)来完成,但这将导致同步问题,并且如果线程不使用任何线程局部储存,会带来不必要的工作。因为动态加载的模块不能使用静态模式,不需要马上就在
dtvt
中分配新元素。总是可以把分配推迟到第一次的使用,在那里会调用
__tls_get_addr
。
3.3.
静态链接的应用
在静态链接的应用中处理
TLS
,要远比在动态链接的代码中简单。最甚,如果确定静态链接的应用不能动态加载模块。即便在某些环境下允许动态加载的系统中,动态加载可能被局限在加载非常基本的模块,而不允许加载使用或定义了线程局部储存的模块。
因此静态链接的代码总是只有一个
TLS
块。而且因为仅有一个模块在使用,变量的偏移不是问题。因为所有的线程局部变量都要包含在这个唯一的
TLS
块里,偏移在链接时刻就是已知的。
链接器总是可以填入模块
ID
、偏移量,并执行代码放宽(
code
relaxation
)。启动代码除了为初始线程设立
TLS
块外,没有其它任务。线程库也为新创建的线程做同样的事情。这是一个简单的任务,因为只有一个初始化映像。
从这一节的讨论中,我们已经看到访问
TLS
块非常简单,因为
tlsoffset1
的值在链接时刻就知道了,把线程指针,
tlsoffset1
的值,及变量偏移相加,就得到变量的地址。对于某些架构,链接器可以通过改写编译器产生的代码,自动地帮助代码改进。在讨论线程局部储存访问模式时,我们将看到代码会得到何等简化。而当讨论链接器放宽(
linker relaxation
)时,我们将看到链接器如何执行所有需要的优化。
3.4.
架构特定的定义
不是所有的架构都使用同一个版本的线程局部储存数据结构,并且某些要求也不同。线程指针的处理是如此的“低级”(
low-level
),它本质上是特定于架构的。本节描述这些细节来填补到目前为止讨论的空缺,并且为描述启动代码的工作做准备。
3.4.1.
IA-64
细节
IA-64 ABI
指定使用上面版本
I
的线程局部储存数据结构。
TCB
的大小是
16
字节,其中前
8
个字节包含了指向动态线程向量的指针。余下
8
个字节保留为实现使用。
dtvt
的地址可以通过载入由线程寄存器,
tp
(
GR 13
)所指向的字
tpt
来确定。
dtvt
每个元素的大小是
8
字节,可以容纳一个指针。
在启动时刻出现的所有模块(即,那些不能被卸载的模块)的
TLS
块跟在
TCB
后创建。其
tlsoffsetx
的值计算如下:
tlsoffset1
= round (16, align1
)
tlsoffsetm+1
= round (tlsoffsetm
+ tlssizem
, alignm+1
)
对于所有
m
,有
1<= m <= M
,其中
M
是模块的总数。
函数
__tls_get_addr
在
IA-64 ABI
中的定义如上所描述:
extern
void *__tls_get_addr (size_t m, size_t offset);
它把模块
ID
及偏移用作参数,这要求重定位改变调用代码以提供需要的信息。
3.4.2.
IA-32
细节
IA-32 ABI
指定使用版本
II
的线程局部储存数据结构。注意:
IA-32 ABI
有两个版本(
version
)。在这两个模式间数据结构的布局没有不同。对于这两个
ABI
来说,
TCB
的大小无关紧要。编译器产生的代码不能直接访问动态线程向量。
dtvt
的每个元素是
4
字节大小,用作一个指针足够了,用于世代计数也足够。
因为
IA-32
架构的寄存器不多,线程寄存器通过段寄(
segment
)存器
%gs
间接编码得到。对线程寄存器的唯一要求是:实际的线程指针
tpt
可以通过
%gs
寄存器从绝对
0
地址载入。下面的代码将把线程指针载入
%eax
寄存器:
movl
%gs: 0, %eax
为了访问使用静态模式模块的
TLS
块,必须知道偏移
tlsoffsetm
。必须从线程寄存器值中减去
这些值。这不同于
IA-64
,在那里偏移是被加上。这些偏移的计算如下:
tlsoffset1
= round (tlssize1
, align1
)
tlsoffsetm+1
= round (tlsoffsetm
+ tlssizem+1
, alignm+1
)
对于所有
m
,有
1<= m <= M
,其中
M
是模块的总数。这些公式与
IA-64
的稍有不同,因为这些值是要被减去的。
函数
__tls_get_addr
同样与
IA-64
的稍有不同。其原型是:
extern
void *__tls_get_addr (tls_index *ti);
其中类型
tls_index
被定义如下:
typedef struct
{
unsigned long int ti_module;
unsigned long int ti_offset;
} tls_index;
成员名出于解释(
presentation
)的目的给出。它们在运行时环境外不可用。传递给函数的信息,与这个函数的
IA-64
版本相同,但只需要产生传递一个参数的代码,并且这些值不需要由调用代码从
GOT
载入。相反,这都集中在
__tls_get_addr
中。注意到这个结构体的成员的大小与
GOT
单个项的大小相同。因此这样的一个结构体可以定义在
GOT
上,占据
2
个
GOT
项。
这个函数的定义是区分
2
个
IA-32 ABI
的特征之一。由
Sun Microsystems
定义的
ABI
对这个函数使用传统的
IA-32
调用规范,通过栈传递参数。
GNU
版本的
ABI
则定义通过
%eax
寄存器传递参数。为了避免与
Sun
接口的冲突,这个函数有一个另外的名字(注意前导的
3
个下划线):
extern void *___tls_get_addr (tls_index *ti)
__attribute__
((__regparm__ (1)));
这个声明使用了
GNU C
编译器的记法。函数本身的差别不是很大,但是链接器操作的复杂性及产生代码的大小则有大的差异,
GNU
版本要好些。
对于在
GNU
系统上的实现,我们可以增加一个要求。
%gs: 0
所代表的地址,实际上就是线程指针。即,
%gs: 0
所指向字的内容就是这个字的地址。(
The address %gs: 0 represents is actually the same as the thread
pointer. I.e., the content of the word addressed via %gs: 0 is the address of
the very same location
)这个潜在的好处是巨大的,因为我们可以通过
%gs
寄存器直接访问内存,而不需要首先载入线程指针。下面
x86
的初始及局部执行模式(
initial
and local exec model
)的章节显示了这一好处。
3.4.3.
SPARC
细节
SPARC ABI
与
IA-32 ABI
几乎完全相同。两者都是由
Sun
设计的。
32
位及
64
位
SPARC
实现的差别在于包含指针的变量的大小不同。
正如
IA-32
,
TCB
的结构体没有指定。
%g7
寄存器被用作包含
tpt
的线程寄存器。在线程寄存器的协助下访问动态线程向量的行为由实现定义。
dtvt
每个元素的大小,对于
32
位
SPARC
是
4
字节,对于
64
位
SPARC
是
8
字节。
在启动时刻出现的模块的
TLS
块,根据版本
II
的数据结构布局来分配,并且
32
位,
64
位代码都使用相同的公式计算偏移。
tlsoffset1
= round (tlssize1
, align1
)
tlsoffsetm+1
= round (tlsoffsetm
+ tlssizem+1
, alignm+1
)
对于所有
m
,有
1<= m <= M
,其中
M
是模块的总数。
函数
__tls_get_addr
具有与
IA-32
相同的接口。其原型是:
extern
void *__tls_get_addr (tls_index *ti);
其中类型
tls_index
被定义如下:
typedef struct
{
unsigned long int ti_module;
unsigned long int ti_offset;
} tls_index;
这里成员名同样仅出于解释(
presentation
)的目的给出。它们在运行时环境外不可用。
因为类型
unsigned long int
在
32
位
SPRAC
上是
4
字节,而在
64
位
SPARC
上是
8
字节,
tls_index
的成员,对于两者
CPU
,都与
GOT
项大小相同,因此同样也可以在
GOT
数据结构上定义这个类型的对象。
3.4.4.
SH
细节
SH ABI
由
Kaz Kojima
按照版本
I
来设计。当前还没有对
64
位
SH
架构的支持。函数
__tls_get_addr
具有与
SPARC
相同的接口:
extern
void *__tls_get_addr (tls_index *ti);
其中类型
tls_index
被定义如下:
typedef struct
{
unsigned long int ti_module;
unsigned long int ti_offset;
} tls_index;
这里成员名如常仅出于解释(
presentation
)的目的给出。它们在运行时环境外不可用。
当前所支持的
SH ABI
的细节,因为处理器架构的原因,不同于
SPARC
,
IA-32
及
IA-64
的代码。在
SH-5
之前的处理器仅提供非常受限的取址模式,它仅允许最多
12
位的偏移。因为编译器不能对函数的大小及布局做任何假定(因而符号的相对位置),对象及函数的地址通常不能在运行时计算(译:似乎应该是编译时刻)。相反,地址被保存在变量中,在加载时刻,由运行时链接器计算这些值。这只需要为数据对象定义重定位类型。因为仅需要少数新的重定位类型,这极大地简化了
TLS
的处理。
访问
TLS
的代码序列是固定的。指令调度不被允许。在今天的
SH
实现中,这已不再需要,因为它们不再突出(
feature
)复杂的乱序执行(
out-of-order execution
)。
3.4.5.
Alpha
细节
Alpha ABI
是
IA-64
及
SPARC
ABI
的混合体。其线程局部储存数据结构,遵循上面的版本
I
。
TCB
的大小是
16
字节,其中前
8
个字节包含了指向动态线程向量的指针。余下
8
个字节保留为实现使用。
在启动时刻出现的所有模块(即,那些不能卸载的)的
TLS
块
跟在
TCB
后连续构建。其
tlsoffsetx
的值计算如下:
tlsoffset1
= round (16, align1
)
tlsoffsetm+1
= round (tlsoffsetm
+ tlssizem
, alignm+1
)
对于所有
m
,有
1<= m <= M
,其中
M
是模块的总数。
函数
__tls_get_addr
如为
SPARC
定义的那样:
extern
void *__tls_get_addr (tls_index *ti);
线程指针被保存指针线程的进程控制块中。这个值通过
PALcode
的入口点
PAL_rduniq
来访问。
3.4.6.
x86-64
细节
x86-64 ABI
与
IA-32 ABI
几乎完全相同。差别主要在于,包含指针变量的不同的大小,并且只有一个更接近
IA-32 GNU
版本的版本。
它使用
%fs
段寄存器,而不是
%gs
段寄存器。在线程寄存器的协助下,访问动态线程向量的行为,由实现定义。
dtvt
每个元素的大小是
8
字节。
在启动时刻出现的所有模块的
TLS
块,根据版本
II
的数据结构布局来分配,并使用相同的公式计算偏移。
tlsoffset1
= round (tlssize1
, align1
)
tlsoffsetm+1
= round (tlsoffsetm
+ tlssizem+1
, alignm+1
)
对于所有
m
,有
1<= m <= M
,其中
M
是模块的总数。
函数
__tls_get_addr
具有与
IA-32
相同的接口。其原型是:
extern
void *__tls_get_addr (tls_index *ti);
其中类型
tls_index
被定义如下:
typedef struct
{
unsigned long int ti_module;
unsigned long int ti_offset;
} tls_index;
这里成员名同样仅出于解释(
presentation
)的目的给出。它们在运行时环境外不可用。
3.4.7.
s390
细节
s390 ABI
使用版本
II
的线程局部储存数据结构。对于这个
ABI
,
TCB
的大小无关重要。线程指针保存在访问寄存器
%a0
里,在能作为地址使用前,需要被提取入一个通用寄存器。从
%a0
获取线程指针到,比如,
%r1
的一个方法是使用
ear
指令:
ear
%r1, %a0
在启动时刻出现的所有模块的
TLS
块根据版本
II
的数据结构的布局来分配,并且使用相同的公式计算偏移。
tlsoffseti
的值必须从线程寄存器的值中减去。
tlsoffset1
= round (tlssize1
, align1
)
tlsoffsetm+1
= round (tlsoffsetm
+ tlssizem+1
, alignm+1
)
对于所有
m
,有
1<= m <= M
,其中
M
是模块的总数。
S390 ABI
定义使用函数
__tls_get_offset
,而不是其它
ABI
所使用的函数
__tls_get_addr
。其原型是:
unsigned
long int __tls_get_offset (unsigned long int offset);
这个函数具有隐藏的第二个参数。调用者需要设立
GOT
寄存器
%r12
,来包含调用者模块的全局偏移表(
global offset table
)的地址。参数
offset
,当加上
GOT
寄存器的值时,得到,位于调用者的全局偏移表中的,
tls_index
结构体的地址。类型
tls_index
被定义如下:
typedef struct
{
unsigned long int ti_module;
unsigned long int ti_offset;
} tls_index;
__tls_get_offset
的返回值是线程指针的一个偏移。为了得到所要求的变量的地址,线程指针需要加上返回值。
__tls_get_offset
的使用看起来似乎比标准的
__tls_get_addr
更复杂,但是对于
s390
,使用
__tls_get_offset
产生更好的代码序列。
3.4.8.
s390x
细节
s390x ABI
非常接近于
s390 ABI
。线程局部储存数据结构遵循版本
II
。对于这个
ABI
来说,
TCB
的大小无关紧要。线程指针被保存在访问寄存器对(
access register pair
)
%a0
及
%a1
中,其中线程指针的高
32
位在
%a0
里,低
32
位在
%a1
中。把线程指针获取入,比如寄存器,
%r1
的一个方法是使用下面的指令序列:
ear %1, %a0
sllg %r1, %r1, 32
ear %r1, %a1
在启动时刻出现的所有模块的
TLS
块,使用与
s390
相同的公式计算
tlsoffsetm
,并且
s390x ABI
使用与
s390
相同的
__tls_get_offset
接口。