链接程序和库指南

线程局部存储的运行时分配

在程序的生命周期中,将会在三个时间创建 TLS。

运行时线程局部数据存储的布局如图 8–1 中所示。

图 8–1 线程局部存储的运行时存储布局

运行时线程局部存储的布局

程序启动

在程序启动时,运行时系统将为主线程创建 TLS。

首先,运行时链接程序以逻辑方式将所有已装入动态库(包括动态可执行文件)的 TLS 模板合并成单个静态模板。在合并的模板中为每个动态库的 TLS 模板指定一个偏移 (tlsoffsetm),如下所示。

tlssizem+1alignm+1 分别是动态库 m 的分配模板的大小和对齐方式。其中,1 <= m <= MM 是已装入动态库的总数。round(offset, align) 函数返回向上舍入为最接近 align 倍数的偏移。TLS 模板正好放置在线程指针 tpt 的前面。根据从 tpt 中减去的数字访问 TLS 数据。

接下来,运行时链接程序将计算启动时为 TLS 分配的总大小 tlssizeS。此大小等于 tlsoffsetM

然后,运行时链接程序构造初始化记录的链接表。此列表中的每条记录都描述一个已装入动态库的 TLS 初始化映像。每条记录都包含以下字段。

线程库使用此信息为初始线程分配存储空间。将初始化此存储空间,并为初始线程创建动态 TLS 向量。

创建线程

对于初始线程和所创建的每个新线程,线程库将为每个已装入动态库分配一个新的 TLS 块。各个块可以单独进行分配,也可以作为一个连续块进行分配。

每个线程 t 都有一个关联的线程指针 tpt,该指针指向线程控制块 TCB。线程指针 tp 始终包含当前正在运行的线程的 tpt 值。

然后,线程库为当前线程 t 创建一个指针向量 dtvt。每个向量的第一个元素都包含一个生成编号 gent,该生成编号用于确定需要扩展向量的时间。 请参见延迟分配线程局部存储块

向量 dtvt,m 中剩余的每个元素都是一个指针(指向为属于动态库 m 的 TLS 保留的块)。

对于启动后动态装入的目标文件,线程库延迟分配 TLS 块。将在第一次引用已装入目标文件中的 TLS 变量时进行分配。必须使用动态 TLS 模型引用在启动后动态装入的目标文件中定义的 TLS。对于延迟分配的块,将指针 dtvt,m 设置为实现定义的特殊值。


注 –

运行时链接程序可以将所有启动目标文件的 TLS 模板进行分组,以便在向量 dtv t,1 中共享单个元素。这种分组不会影响前面介绍的偏移计算,也不会影响初始化记录列表的创建。但是,对于以下各节,总目标文件数 M 的值从值 1 开始。


然后,线程库将初始化映像复制到新存储块中的相应位置。

启动后动态装入

可以在进程启动后装入包含 TLS 的共享库。因此,运行时链接程序必须扩展初始化记录列表,以包含新目标文件的初始化模板。为新目标文件指定索引 m = M + 1,并以 1 为增量累加计数器 M。但是,直到实际引用块时才会分配新的 TLS 块。

卸载包含 TLS 的库时,将释放该库使用的 TLS 块。

延迟分配线程局部存储块

在动态 TLS 模型中,当线程 t 需要访问目标文件 m 的 TLS 块时,代码将更新 dtvt 并执行 TLS 块的初始分配。线程库将提供以下接口以便动态分配 TLS。

typedef struct {

    unsigned long ti_moduleid;

    unsigned long ti_tlsoffset;

} TLS_index;



extern void * __tls_get_addr(TLS_index * ti);     (SPARC and x64)

extern void * ___tls_get_addr(TLS_index * ti);    (32–bit x86)

注 –

此函数的 SPARC 和 64 位 x86 定义具有相同的函数签名。但是,32 位 x86 版本不使用缺省调用约定来传递栈中的参数。相反,32 位 x86 版本通过更有效的 %eax 寄存器来传递其参数。为了表示将使用此替代调用方法,32 位 x86 函数的名称中有三个前导下划线。


这两个版本的 tls_get_addr() 都检查每线程生成计数器 gent,以便确定是否需要更新向量。如果向量 dtvt 已过时,则例程将更新此向量,可能会重新分配此向量以便为更多项留出空间。然后,例程将检查是否已分配与 dtvt,m 对应的 TLS 块。如果尚未分配此向量,则例程将使用运行时链接程序提供的初始化记录列表中的信息来分配并初始化块。指针 dtv t,m 被设置为指向已分配的块。例程返回一个指向块中的给定偏移的指针。