拓冰建站拓冰建站
首页 / 资讯中心 / 正文

线程局部存储:TLS(Thread Local Storage)

当多个线程需要访问同一个名称的全局或静态变量但每个线程必须维护自己独立的变量副本且互不干扰时使用 TLSThread Local Storage线程局部存储。如图每个线程在 TLS 中维护独立的数据副本。从 C11 标准开始C 语言正式引入了_Thread_local关键字在threads.h中提供了宏thread_local。在此之前传统 GCC/Clang 使用__thread扩展MSVC 使用__declspec(thread)。TLS应用实例使用 TLS 的典型场景保存线程独立的错误码/状态标准 C 库的errno就是典型的 TLS 实现。多线程下如果errno是全局变量线程 A 的失败会覆盖线程 B 的状态通过 TLS 则互不影响。避免加锁Lock-free State例如多线程日志系统中保存线程私有的缓冲区Buffer或 Trace ID。如果用全局 Buffer 必须加互斥锁Mutex用 TLS 则完全免去锁开销。重构非线程安全的传统 API例如标准库中的strtok()依赖内部静态指针记录上一次解析位置因此不是线程安全的使用 TLS 存放该状态如strtok_r的思路即可将其改造为线程安全。代码如下#include iostream #include thread #include chrono #include string // 使用 thread_local 声明线程局部变量 thread_local int tls_last_error 0; thread_local std::string tls_log_buffer; void set_thread_error(int code, const std::string msg) { tls_last_error code; tls_log_buffer [Err std::to_string(code) ]: msg; } void worker_a() { set_thread_error(404, Resource not found on Thread A); std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作 std::cout Thread A - tls_log_buffer std::endl; } void worker_b() { set_thread_error(500, Internal error on Thread B); std::cout Thread B - tls_log_buffer std::endl; } int main() { std::thread t1(worker_a); std::thread t2(worker_b); t1.join(); t2.join(); return 0; }执行结果Thread B - [Err 500]: Internal error on Thread B Thread A - [Err 404]: Resource not found on Thread AGCC 的 4 种 TLS 模型4 种 TLS 模型Global Dynamic、Local Dynamic、Initial Exec、Local Exec时核心关注点是线程基址偏移量的获取方式是运行时动态计算还是编译/链接期直接确定以及它们对应的汇编指令复杂度与重定位类型。各模型在 x86_64 体系下的核心差异如下TLS 模型GCC 编译选项单变量属性写法核心机制汇编开销指令数 / 寄存器支持 dlopen适用场景Global Dynamic (GD)-ftls-modelglobal-dynamic__attribute__((tls_model(global-dynamic)))每次访问通过 GOT 表项传参调用__tls_get_addr()最高跨函数调用传参并保存寄存器是任意 .so 动态库‑fPIC共享库默认模型GCCLocal Dynamic (LD)-ftls-modellocal-dynamic__attribute__((tls_model(local-dynamic)))同模块仅一次__tls_get_addr()获取模块 TLS 基址后续变量用偏移中等同模块多 TLS 变量共享一次函数调用是同一.so内部大量 TLS 变量不跨 DSO 访问Initial Exec (IE)-ftls-modelinitial-exec__attribute__((tls_model(initial-exec)))跳过函数调用GOT 读取静态 TLS 偏移低2 次内存寻址 加法否占用 static‑TLS 槽dlopen 容易爆cannot allocate memory in static TLS block程序启动阶段加载的库无‑fPIC 主程序默认GCCLocal Exec (LE)-ftls-modellocal-exec__attribute__((tls_model(local-exec)))链接器硬编码相对于 FS/GS (TP) 的固定偏移最低单条内存访问指令否主程序内部 TLS 变量不能用于 so解释下一些关键参数。GOTGOT 表项Global Offset Table全局偏移表是ELF PIC/PIE 位置无关代码的核心数据结构存放在数据段是一张地址数组每一个元素就是一个GOT 表项GOT entry64 位下每项 8 字节存真实运行时地址。GOT配合PLT (过程链接表)是 ELF动态链接的核心机制只出现在动态可执行程序 /.so共享库静态编译、裸机 ELF 没有 GOT。背景共享库.so需要位置无关代码 PICPosition‑Independent Code。共享库每次加载到内存的虚拟地址是不固定的不能把硬编码地址写死在指令里。TLS 场景里GOT 分为两部分普通 GOT.got TLS 专用 GOT.got.tls也叫 TLS GOT。.got存全局变量的地址.got.plt存外部函数的地址和 PLT 配套使用TLS 模型 GD/LD/IE 全部依赖 TLS‑GOT 表项LE 完全不用 GOT。dlopendlopen()是 Linux glibc 提供的动态库运行时加载 API属于 POSIX 动态链接接口。 头文件dlfcn.h链接时必须加-ldl。void *dlopen(const char *filename, int flag);作用程序已经跑起来之后运行时才去加载一个.soDSO 动态共享对象不是程序启动时就加载。两种加载模式对比启动时加载静态链接依赖编译链接时写上-lxxx程序一启动ld.so 就把libxxx.so加载进进程地址空间。 这种库可以使用 IE / LEstatic‑TLS。dlopen 晚加载运行时加载程序已经执行很久了代码里主动调用dlopen(libxxx.so, RTLD_NOW)运行时才把 so 加载进来。 dlopen 加载的 DSO不能用 IE / LE TLS 模型只能用 GD / LD。原因IE/LE 依赖进程启动时预先分配好的 static‑TLS 内存池dlopen 晚加载无法扩容 static‑TLS直接报错cannot allocate memory in static TLS block。最小示例#include dlfcn.h #include stdio.h int main(){ // 运行时才加载 libtest.so void *handle dlopen(./libtest.so, RTLD_NOW); if(!handle){ printf(%s\n, dlerror()); return -1; } // 获取函数指针调用 void (*func)(void) dlsym(handle, hello); func(); dlclose(handle); return 0; }编译gcc main.c -o app -ldlGCC 默认规则重点GCC编译共享库带-fPIC默认 global‑dynamic编译可执行程序不带-fPIC/-fPIE默认 initial‑exec编译器会做自动升级优化即使你手动指定模型如果符号可见性、编译选项不满足gcc 会自动切换到更保守模型。例如给 so 强制local‑exec会被 gcc 直接降级为 global‑dynamic模块外部可见的 TLS 符号local‑dynamic会回退到 global‑dynamic。global‑dynamic(GD) vs local‑dynamic(LD)共同点都属于动态 TLS 模型支持dlopen晚加载都调用__tls_get_addr都需要两组 TLS‑GOT 重定位R_X86_64_DTPMOD64(模块 ID)、R_X86_64_DTPOFF64(模块内偏移)只能用于 PIC 共享库。核心差异__tls_get_addr调用次数、GOT 表是否共享、作用范围每一个.so就是一个独立 DSO每个 DSO 加载时动态链接器给它分配一个唯一的dso_id模块 ID。__tls_get_addr(dso_id, offset)的第一个参数就是这个 DSO 编号。Global‑DynamicGDgcc -ftls-modelglobal‑dynamic共享库默认每一个__thread变量独占一套 GOT1/GOT2DTPMODDSO id/DTPOFFDynamicTlsOFFset每一次访问该变量都完整调用一次__tls_get_addr。C 代码示例__thread int a; __thread int b; int f() { return a b; }x86‑64 汇编示意GDf: # 取a完整调用一次 __tls_get_addr mov rdi, QWORD [ripGOT1_a] # DTPMOD(a) mov rsi, QWORD [ripGOT2_a] # DTPOFF(a) call __tls_get_addr mov edx, [rax] # 取b又完整调用一次 __tls_get_addr两套独立GOT表项 mov rdi, QWORD [ripGOT1_b] # DTPMOD(b) mov rsi, QWORD [ripGOT2_b] # DTPOFF(b) call __tls_get_addr mov eax, [rax] add eax, edx retGOT变量 a、变量 b各占 2 个 TLS‑GOT 项开销访问 a 调一次访问 b 又调一次多次访问同一个变量也会重复调用。优点可以跨 DSO 访问别的 so 的 TLS 变量。缺点大量 TLS 变量时代码膨胀、函数调用开销高。Local‑DynamicLD‑ftls-modellocal‑dynamic约束只允许访问本 DSO 内部的 TLS 变量。 整个.so所有 TLS 变量只共享一套 GOT1/GOT2。__tls_get_addr(dso_id, 0)只调用 1 次拿到本 DSO 的 TLS 块起始基址后续所有 TLS 变量直接加静态偏移不再调用函数。同样 C 代码__thread int a; __thread int b; int f() { return a b; }x86‑64 汇编示意LD:f: # 整个DSO只调用一次 __tls_get_addr获取本模块TLS基址存入rbx mov rdi, QWORD [ripGOT_MOD] # 本so的 dso_id全局唯一一套 xor rsi, rsi # offset0 call __tls_get_addr mov rbx, rax # rbx 本DSO TLS块基址 TP dso_tls_offset # 访问 a直接基址 编译期已知静态偏移无函数调用 mov edx, [rbx OFFSET_A] # 访问 b同样基址 静态偏移无函数调用 mov eax, [rbx OFFSET_B] add eax, edx retGOT整个 so 全部 TLS 变量共用 2 个 TLS‑GOT 项不是每个变量一套。开销函数内最多一次__tls_get_addr调用后续变量只是简单加法 内存访问。硬限制不能访问其他 DSO 的 TLS 变量。一旦你代码里访问别的 so 的__thread变量链接器 / GCC 会自动回退降级为 GD 模式。Local Exec (LE) vs Local Exec (LE)IE、LE 统称为静态 TLS 模型完全不使用 DTPMOD / DTPOFF不调用__tls_get_addr()使用TPOFFR_X86_64_TPOFF64偏移是相对于 TP (% fs) 寄存器不是相对于 DSO TLS 块起点。 核心限制只能用于程序启动阶段就加载的模块dlopen()晚加载会报错cannot allocate memory in static TLS block因为要占用进程全局预分配的 static‑TLS 槽位槽数量有限。Initial‑ExecIE‑ftls-modelinitial‑exec适用启动时加载的 DSO.so不能 dlopen 晚加载。每个 TLS 变量分配1 项 TLS‑GOT重定位类型R_X86_64_GOTTPOFF。动态链接器 ld.so 在程序启动时把该变量相对于 TP (% fs) 的最终运行时偏移回填到 GOT 表项。指令PC 相对取 GOT 里的运行时偏移再fs:[reg]访问内存。x86‑64 汇编示意# 变量aIE模型 mov rdx, QWORD [rip TLS_GOT_a] # 从GOT读出ld.so回填好的TP偏移 mov eax, fs:[rdx] # TP rdx直接读取变量无函数调用关键点GOT 表项存运行时 TP 偏移编译期不知道这个值不需要__tls_get_addr允许访问本 DSO 以及其他启动加载 DSO 的 TLS 变量只要这个库用 IE就占用 static‑TLS 槽dlopen 加载该库直接失败。Local‑ExecLE‑ftls-modellocal‑exec适用只能主程序可执行文件内部 TLS 变量绝对不能用于.so。完全不用 GOT没有任何 GOT 表项。链接阶段链接器已经算出该 TLS 变量相对于 TP (% fs) 的最终固定偏移直接硬编码进指令立即数。x86‑64 汇编示意# 变量aLE模型offset是链接时算死的常数 mov eax, fs:[offset]关键点零 GOT、无重定位开销性能最高只能访问主程序内部的 TLS 变量不能访问任何 DSO 的 TLS如果你强行给‑shared 库传‑ftls-modellocal‑execGCC/ld 会直接降级回 GD主程序默认模型就是 LE。end
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门