news 2026/8/28 5:44:07

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
线程局部存储:TLS(Thread Local Storage)

多个线程需要访问同一个名称的全局或静态变量,但每个线程必须维护自己独立的变量副本且互不干扰时,使用 TLS(Thread 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 A

GCC 的 4 种 TLS 模型

4 种 TLS 模型(Global Dynamic、Local Dynamic、Initial Exec、Local Exec)时,核心关注点是:线程基址偏移量的获取方式(是运行时动态计算,还是编译/链接期直接确定)以及它们对应的汇编指令复杂度与重定位类型

各模型在 x86_64 体系下的核心差异如下:

TLS 模型GCC 编译选项单变量属性写法核心机制汇编开销(指令数 / 寄存器)支持 dlopen适用场景
Global Dynamic (GD)-ftls-model=global-dynamic__attribute__((tls_model("global-dynamic")))每次访问通过 GOT 表项传参调用__tls_get_addr()最高(跨函数调用,传参并保存寄存器)任意 .so 动态库;‑fPIC共享库默认模型GCC
Local Dynamic (LD)-ftls-model=local-dynamic__attribute__((tls_model("local-dynamic")))同模块仅一次__tls_get_addr()获取模块 TLS 基址,后续变量用偏移中等(同模块多 TLS 变量共享一次函数调用)同一.so内部大量 TLS 变量,不跨 DSO 访问
Initial Exec (IE)-ftls-model=initial-exec__attribute__((tls_model("initial-exec")))跳过函数调用,GOT 读取静态 TLS 偏移低(2 次内存寻址 + 加法)否(占用 static‑TLS 槽,dlopen 容易爆cannot allocate memory in static TLS block程序启动阶段加载的库;无‑fPIC 主程序默认GCC
Local Exec (LE)-ftls-model=local-exec__attribute__((tls_model("local-exec")))链接器硬编码相对于 FS/GS (TP) 的固定偏移最低,单条内存访问指令主程序内部 TLS 变量,不能用于 so

解释下一些关键参数。

GOT

GOT 表项(Global Offset Table,全局偏移表)是ELF PIC/PIE 位置无关代码的核心数据结构,存放在数据段,是一张地址数组,每一个元素就是一个GOT 表项(GOT entry),64 位下每项 8 字节,存真实运行时地址。

GOT配合PLT (过程链接表),是 ELF动态链接的核心机制,只出现在动态可执行程序 /.so共享库;静态编译、裸机 ELF 没有 GOT

背景:共享库.so需要位置无关代码 PIC(Position‑Independent Code)

共享库每次加载到内存的虚拟地址是不固定的,不能把硬编码地址写死在指令里。

TLS 场景里,GOT 分为两部分:普通 GOT(`.got`) + TLS 专用 GOT(`.got.tls`,也叫 TLS GOT)。

  • .got:存全局变量的地址
  • .got.plt:存外部函数的地址,和 PLT 配套使用

TLS 模型 GD/LD/IE 全部依赖 TLS‑GOT 表项;LE 完全不用 GOT。

dlopen

dlopen()是 Linux glibc 提供的动态库运行时加载 API,属于 POSIX 动态链接接口。 头文件:<dlfcn.h>,链接时必须加-ldl

void *dlopen(const char *filename, int flag);

作用:程序已经跑起来之后,运行时才去加载一个.so(DSO 动态共享对象),不是程序启动时就加载。

两种加载模式对比

  1. 启动时加载(静态链接依赖)编译链接时写上-lxxx,程序一启动,ld.so 就把libxxx.so加载进进程地址空间。 👉 这种库可以使用 IE / LE(static‑TLS)。
  2. 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 -ldl

GCC 默认规则(重点)GCC

  1. 编译共享库带-fPIC默认 = global‑dynamic
  2. 编译可执行程序,不带-fPIC/-fPIE默认 = initial‑exec
  3. 编译器会做自动升级优化:即使你手动指定模型,如果符号可见性、编译选项不满足,gcc 会自动切换到更保守模型。例如:
    1. 给 so 强制local‑exec会被 gcc 直接降级为 global‑dynamic;
    2. 模块外部可见的 TLS 符号,local‑dynamic会回退到 global‑dynamic。

global‑dynamic(GD) vs local‑dynamic(LD)

共同点:

  1. 都属于动态 TLS 模型,支持dlopen晚加载;
  2. 都调用__tls_get_addr
  3. 都需要两组 TLS‑GOT 重定位:R_X86_64_DTPMOD64(模块 ID)、R_X86_64_DTPOFF64(模块内偏移);
  4. 只能用于 PIC 共享库。

核心差异:__tls_get_addr调用次数、GOT 表是否共享、作用范围

每一个.so就是一个独立 DSO;每个 DSO 加载时,动态链接器给它分配一个唯一的dso_id(模块 ID)

__tls_get_addr(dso_id, offset)的第一个参数就是这个 DSO 编号。

Global‑Dynamic(GD,gcc -ftls-model=global‑dynamic,共享库默认)

每一个__thread变量,独占一套 GOT1/GOT2(DTPMOD(DSO id)/DTPOFF(DynamicTlsOFFset))每一次访问该变量,都完整调用一次__tls_get_addr

C 代码示例:

__thread int a; __thread int b; int f() { return a + b; }

x86‑64 汇编示意(GD):

f: # 取a:完整调用一次 __tls_get_addr mov rdi, QWORD [rip+GOT1_a] # DTPMOD(a) mov rsi, QWORD [rip+GOT2_a] # DTPOFF(a) call __tls_get_addr mov edx, [rax] # 取b:又完整调用一次 __tls_get_addr!!两套独立GOT表项 mov rdi, QWORD [rip+GOT1_b] # DTPMOD(b) mov rsi, QWORD [rip+GOT2_b] # DTPOFF(b) call __tls_get_addr mov eax, [rax] add eax, edx ret
  • GOT:变量 a、变量 b各占 2 个 TLS‑GOT 项
  • 开销:访问 a 调一次,访问 b 又调一次;多次访问同一个变量也会重复调用。
  • 优点:可以跨 DSO 访问别的 so 的 TLS 变量
  • 缺点:大量 TLS 变量时代码膨胀、函数调用开销高。

Local‑Dynamic(LD,‑ftls-model=local‑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 [rip+GOT_MOD] # 本so的 dso_id(全局唯一一套) xor rsi, rsi # offset=0 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 ret
  • GOT:整个 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(),使用TPOFF(R_X86_64_TPOFF64,偏移是相对于 TP (% fs) 寄存器,不是相对于 DSO TLS 块起点。 核心限制:只能用于程序启动阶段就加载的模块dlopen()晚加载会报错cannot allocate memory in static TLS block,因为要占用进程全局预分配的 static‑TLS 槽位,槽数量有限。

Initial‑Exec(IE,‑ftls-model=initial‑exec

适用:启动时加载的 DSO(.so),不能 dlopen 晚加载。

  • 每个 TLS 变量分配1 项 TLS‑GOT,重定位类型:R_X86_64_GOTTPOFF
  • 动态链接器 ld.so 在程序启动时,把该变量相对于 TP (% fs) 的最终运行时偏移回填到 GOT 表项
  • 指令:PC 相对取 GOT 里的运行时偏移,再fs:[reg]访问内存。

x86‑64 汇编示意:

# 变量a,IE模型 mov rdx, QWORD [rip + TLS_GOT_a] # 从GOT读出ld.so回填好的TP偏移 mov eax, fs:[rdx] # TP + rdx,直接读取变量,无函数调用

关键点:

  1. GOT 表项存运行时 TP 偏移,编译期不知道这个值;
  2. 不需要__tls_get_addr
  3. 允许访问本 DSO 以及其他启动加载 DSO 的 TLS 变量
  4. 只要这个库用 IE,就占用 static‑TLS 槽;dlopen 加载该库直接失败。

Local‑Exec(LE,‑ftls-model=local‑exec

适用:只能主程序(可执行文件)内部 TLS 变量,绝对不能用于.so

  • 完全不用 GOT,没有任何 GOT 表项
  • 链接阶段,链接器已经算出该 TLS 变量相对于 TP (% fs) 的最终固定偏移,直接硬编码进指令立即数。

x86‑64 汇编示意:

# 变量a,LE模型,offset是链接时算死的常数 mov eax, fs:[offset]

关键点:

  1. 零 GOT、无重定位开销,性能最高;
  2. 只能访问主程序内部的 TLS 变量,不能访问任何 DSO 的 TLS;
  3. 如果你强行给‑shared 库传‑ftls-model=local‑exec,GCC/ld 会直接降级回 GD;
  4. 主程序默认模型就是 LE。

end

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/28 5:44:00

量化交易策略全流程解析:从数据分析、模型构建到回测评估

1. 项目概述&#xff1a;一次竞赛题目的深度拆解之旅每年年初&#xff0c;对于全球数以万计的数模爱好者来说&#xff0c;美国大学生数学建模竞赛&#xff08;MCM/ICM&#xff09;的赛题发布都是一场盛事。2022年的C题&#xff0c;以其独特的背景和开放性的要求&#xff0c;再次…

作者头像 李华
网站建设 2026/8/28 5:43:45

蓝桥杯国赛真题解析:纯质数算法优化与Python实现

1. 项目概述&#xff1a;从一道国赛真题看质数算法的实战优化最近在复盘蓝桥杯的历年真题&#xff0c;第十二届国赛的这道“纯质数”题目让我印象挺深。它表面上是一道经典的数论问题&#xff0c;核心是筛选质数&#xff0c;但题目给出的数据范围和“纯质数”这个特殊定义&…

作者头像 李华
网站建设 2026/8/28 5:39:05

PaddleOCR训练自己数据集

目录 1. 新建文件夹2. 准备数据3. 数据集的划分 注意1注意2 4. 下载预训练模型5. 训练文字检测模型6. 训练文字识别模型7. 测试8. 转换为推理模型9. 检测模型和识别模型推理 1. 新建文件夹 进入PPOCRLabel源码目录&#xff0c;在上一层目录新建文件夹train_data&#xff0c;…

作者头像 李华
网站建设 2026/8/28 5:37:01

三协议认证MCU模块:BLE、Zigbee、OpenThread选型与实战

在智能家居圈子&#xff0c;你翻开任何一款多协议通信模块的规格书&#xff0c;大概率会看到一句话&#xff1a;本模块已通过BLE、Zigbee、OpenThread认证。MCU模块拿到这三项认证&#xff0c;看似只是规格表上的一行例行描述&#xff0c;但在实际项目里&#xff0c;它直接影响…

作者头像 李华
网站建设 2026/8/28 5:34:03

FIPS ‘n‘ Chips 2026 公布PQC专题讨论会成员名单

2026 年 8 月 17 日 没有哪个话题比后量子密码学&#xff08;post-quantum cryptography, PQC&#xff09;更能说明&#xff0c;FIPS n Chips 2026的举办恰逢其时。 数十年来&#xff0c;互联网安全在很大程度上依赖于非对称密码&#xff08;如RSA、Diffie-Hellman&#xff0…

作者头像 李华