简单理解 Thread Local Storage

无论是百度还是谷歌,如果直接搜索 “thread local”,出来的页面大部分都是讲 Java 的 ThreadLocal<T> 类,然后讲变量是存储在了 ThreadLocalMap 里。但是,本文要介绍的是由编译器、链接器和操作系统等联合提供的 thread local storage(TLS)功能 ,由于和硬件,操作系统有关,只关注 x86-64 为基础的 Linux ELF 格式。

1. C++中使用TLS

基本思想和 Java 的 ThreadLocal 是一样的,声明 TLS 变量会使每个线程拥有各自的变量实例。

#include <thread>
#include <iostream>

thread_local int tls_var = -1;

void task(std::string thread_name)
{
    int id = thread_name[4] - '0';
    printf("%s init tls value %d\n", thread_name.c_str(), tls_var);
    printf("%s tls address %p\n", thread_name.c_str(), &tls_var);
    tls_var = id;
    printf("%s new value %d\n", thread_name.c_str(), tls_var);
}

int main() {

    std::thread t1(task, "task1");
    std::thread t2(task, "task2");
    t1.join();
    t2.join();
}

一次输出结果为

task1 init tls value -1
task1 tls address 0x7f47bd7f16fc
task1 new value 1
task2 init tls value -1
task2 tls address 0x7f47bcff06fc
task2 new value 2

可以看到,两个线程中,tls_var 的初始值都是-1,但地址不同,因此在线程1中对 tls_var 的修改只在线程1中有效。

2. 数据定义

由于每个线程都需要一份变量的实例,因此 TLS 变量不能再放入常规的 .bss 或 .data 段,而是放进了 .tbss 和 .tdata 段。且 .tbss 和 .tdata 的 section attribute flags 只比 .bss 和 .data 多了一位 SHF_TLS (可以用 objdump -h 查看). 实际上,只要类型为 SHT_PROGBITS 或者 SHT_NOBITS (可通过 readelf -WS a.out 查询)的 section 带有SHF_TLS 标记,都和 .tbss 和 .tdata 一样被处理。

.tdata 的数据不像 .data 那样被直接使用,可能会被 dynamic linker 在重定位时被修改,之后再也不变,作为 initialization image 使用。每个线程会复制这一段内容到新内存,保证了每个线程看到的初始值相同。由于 TLS 变量的地址在此时不能确定,因此其符号表的st_value 值为变量在所属 section的偏移位置 (第一个 TLS 变量的 st_value 就是0了,可以通过 readelf -s a.out 查看)

readelf -Wl a.out # 查看所有的program headers,一个program header可以包含一个或多个section

3. Run-Time Handing of TLS

既然每个线程有自己的一份变量实例,那么变量地址就一定需要间接寻址。x86系统中使用 FS 寄存器(Linux) 指向 Thread Control Block (TCB),然后再通过变量所属的module ID 和 变量的偏移量来找到位置。

img

如上图所示,\(tp_t\) 就是 FS 寄存器指向的位置,\(dtv_{t,1}\) 指向 module ID 为1 (main executable 的编号永远是1) 的 TLS Block,再通过上一节中描述的变量偏移即可在对应的 TLS Block中提取出变量的位置。

简单理解到此为止,更多细节可以在有需要时进一步了解。

参考文档:

  1. https://uclibc.org/docs/tls.pdf
  2. https://chao-tic.github.io/blog/2018/12/25/tls