cannot allocate memory in static TLS block

最近业务报障,使用taf框架编译成动态库,从3.4.5.8-notrace版本换成3.4.6.0-notcmalloc版本后

dlopen打开动态库报错cannot allocate memory in static TLS block,业务认为是不带tcmalloc导致的

我的最终结论是taf框架的动态库编译强制开启了静态TLS,这个线程私有变量区是存在上限的,trace代码有超大的线程私有变量,导致dlopen失败

总结了一下何时会开启静态TLS,并且如何解决

初步定位并解决问题

根据https://stackoverflow.com/questions/78277202/how-to-determine-the-maximum-static-tls-thread-local-storage-block-size-on-lin

在源码中https://github.com/zerovm/glibc/blob/3f07350498160f552350dc39f6fe6d237c7c3b03/csu/libc-tls.c#L99C1-L101C76

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Size of the static TLS block.  Giving this initialized value
preallocates some surplus bytes in the static TLS area. */
size_t _dl_tls_static_size = 2048;

static inline void
init_static_tls (size_t memsz, size_t align)
{
/* That is the size of the TLS memory for this object. The initialized
value of _dl_tls_static_size is provided by dl-open.c to request some
surplus that permits dynamic loading of modules with IE-model TLS. */
GL(dl_tls_static_size) = roundup (memsz + GL(dl_tls_static_size),
TLS_TCB_ALIGN);
...省略
}

也就是说_dl_tls_static_size是预分配静态TLS区域的大小,只有在IE-model(也就是initial-exec,下面会介绍)的TLS下才生效

那么如何看当前的私有变量区使用大小呢?

可以readelf打印符号,也就是-sW,然后在用awk匹配TLS类型,并且由于readelf打印全部符号的时候,动态符号打印两次,所以使用awk去重,最后用大小进行排序

1
2
3
4
5
6
7
8
9
10
11
~ readelf -sW libpytafcall.so | awk '$4 == "TLS" { if (!seen[$2]++) print }' | sort -k3,3n
...省略一部分
1547: 00000000000000b0 48 TLS LOCAL DEFAULT 17 _ZZN3taf13TracerManager9getTracerERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE12_localTracer
2153: 0000000000000230 104 TLS GLOBAL DEFAULT 17 _ZN3taf11Transceiver8_bytesInE
4284: 00000000000001c8 104 TLS GLOBAL DEFAULT 17 _ZN3taf11Transceiver9_bytesOutE
3033: 00000000000002a0 112 TLS <OS specific>: 10 DEFAULT 17 _ZZN3taf15PThreadSwitcher6clsRefEvE3pts
6912: 00000000000000e8 112 TLS GLOBAL DEFAULT 17 _ZN3taf11Transceiver13_bytesPerSendE
8294: 0000000000000158 112 TLS GLOBAL DEFAULT 17 _ZN3taf11Transceiver13_bytesPerRecvE
2055: 0000000000000438 136 TLS LOCAL DEFAULT 17 _ZZN3taf12LoggerBuffer18getTlsLoggerBufferEvE3buf
2057: 0000000000000320 272 TLS LOCAL DEFAULT 17 _ZZN3taf12LoggerBuffer13getTlsOStreamEvE2os
5677: 00000000000004f8 2504 TLS <OS specific>: 10 DEFAULT 17 _ZZNK13jaegertracing6Tracer8randomIDEvE22_randomNumberGenerator

可以看到,是开源库jaegertracing的某个私有变量超限制了,mt19937_64是一个特别复杂的数据结构(标准中定义约为 312 个 uint32_t 的状态,占用约 2.5 KB 内存)

1
2
3
4
5
6
thread_local std::mt19937_64 _randomNumberGenerator = [] {
std::mt19937_64 ret;
std::random_device device;
ret.seed(device());
return ret;
}();

这个好改,改成私有变量指针就行了

1
2
3
4
5
6
thread_local std::unique_ptr<std::mt19937_64> _randomNumberGenerator = [] {
std::unique_ptr<std::mt19937_64> ret(new std::mt19937_64{});
std::random_device device;
ret->seed(device());
return ret;
}();

大部分的线程私有变量都可以通过这种方式压缩到很小,但是这种方案显然算不上完美,如果有海量的线程私有变量呢?

开启静态TLS的触发条件

根据搜索资料,以下方法可以判断是否开启静态TLS

1
2
~ readelf -d libhello.so |grep STATIC_TLS 
0x000000000000001e (FLAGS) BIND_NOW STATIC_TLS

那么何时开启静态TLS呢,根据文档https://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html#index-ftls-model

Alter the thread-local storage model to be used (see Thread-Local Storage). The model argument should be one of ‘global-dynamic’, ‘local-dynamic’, ‘initial-exec’ or ‘local-exec’. Note that the choice is subject to optimization: the compiler may use a more efficient model for symbols not visible outside of the translation unit, or if -fpic is not given on the command line.

将线程局部存储(Thread-Local Storage, TLS)模型更改为要使用的模型(参见 线程局部存储)。model 参数应为以下之一:global-dynamiclocal-dynamicinitial-execlocal-exec需要注意的是,这个选择是可以被优化的:对于在翻译单元(Translation Unit)外不可见的符号,或者如果命令行中未指定 -fpic 参数,编译器可能会使用更高效的存储模型。

The default without -fpic is ‘initial-exec’; with -fpic the default is ‘global-dynamic’.

  • 默认情况(未指定 -fpic):TLS 模型为 initial-exec
  • 默认情况(指定了 -fpic):TLS 模型为 global-dynamic

根据https://www.ibm.com/docs/ro/xl-c-and-cpp-linux/16.1.0?topic=descriptions-ftls-model-qtls

四种TLS模型分别为:

  • global-dynamic

    这种模型是最通用的,可以用于所有线程局部变量(thread-local variables)。

  • initial-exec

    这种模型相比 global-dynamiclocal-dynamic 模型提供了更好的性能,并且可以用于定义在线程局部变量的动态加载模块中,但前提是这些模块与可执行程序同时加载。也就是说,它只能在所有线程局部变量均定义于 非通过 dlopen 加载的模块时使用。

  • local-dynamic

    这种模型相比 global-dynamic 模型提供了更好的性能,并且可以用于定义在线程局部变量的动态加载模块中。然而,它仅能在所有对线程局部变量的引用均位于 定义这些变量的同一模块 时使用。

  • local-exec

    这种模型在所有模型中提供了 最佳性能,但仅在所有线程局部变量均由主程序(main executable)定义并引用时才可使用。

根据我的在gcc-9.4.0的测试,和这个文档有一些出入

  • initial-exec就是当前遇到的静态TLS,他不是完全不能用,是静态区域存在上限2048

  • local-dynamic在跨模块的时候依然可以正常编译

  • local-exec确实不能在包含线程私有变量的动态库编译中使用

    1
    2
    3
    4
    5
    __thread int foo_var;  // 声明 libfoo.so 中的 TLS 变量

    int access_foo_var() {
    return foo_var; // 尝试引用线程局部变量
    }

    报错提示不太对

    1
    2
    3
    ~ gcc -fPIC -shared -o libbar.so libbar.c -ftls-model=local-exec
    /usr/bin/ld: /tmp/ccFmJ6Jj.o: relocation R_X86_64_TPOFF32 against symbol `foo_var' can not be used when making a shared object; recompile with -fPIC
    collect2: error: ld returned 1 exit status

刚才gcc的文档说明,在需要注意的是,这个选择是可以被优化的:对于在翻译单元(Translation Unit)外不可见的符号,或者如果命令行中未指定 -fpic 参数,编译器可能会使用更高效的存储模型。

由于x86_64是无法非-fpic编译的,因此只有两种可能会开启静态TLS(initial-exec):

  • 编译动态库时手动设置-ftls-model=initial-exec

  • 在翻译单元(Translation Unit)外不可见的符号,根据我的测试,引入绝对地址的汇编代码,即使指定了global-dynamic,也会自动转为静态TLS

    1
    2
    3
    4
    5
    6
    7
    #include <stddef.h>

    __thread int *xxx = NULL;
    size_t func(void) {
    size_t ret;
    asm("movq xxx@gottpoff(%%rip),%0" : "=r"(ret));
    }

    编译和检查如下

    1
    2
    3
    ~ gcc -fPIC -shared -o libfoo.so foo.c -ftls-model=global-dynamic
    ~ readelf -d libfoo.so |grep STATIC_TLS
    0x000000000000001e (FLAGS) STATIC_TLS

    taf框架的代码就是这种情况,由于协程代码大量使用汇编编写,导致自动开启了STATIC_TLS无法关闭

解决办法

减少线程私有变量大小,换成指针

这个已经在上文说明了

使用LD_PRELOAD

gcc的文档说了,initial-exec模式是设计用于动态库和主程序一起启动的,这种情况下,动态链接器会计算额外的线程私有变量所需的区域

测试代码

动态库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//hello.cpp
#include <stdio.h>
#include <algorithm>
#include <random>

thread_local std::mt19937_64 _randomNumberGenerator = [] {
std::mt19937_64 ret;
std::random_device device;
ret.seed(device());
return ret;
}();
uint64_t say_hello() {
printf("Hello from shared library!\n");
return _randomNumberGenerator();
}

主程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <link.h>

int main() {
// 加载动态库到新的命名空间
void *handle = dlopen("./libhello.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "Error loading library: %s\n", dlerror());
return 1;
}

// 获取函数指针
void (*say_hello)() = dlsym(handle, "_Z9say_hellov");
if (!say_hello) {
fprintf(stderr, "Error finding symbol: %s\n", dlerror());
dlclose(handle);
return 1;
}

// 调用函数
say_hello();

// 关闭动态库
dlclose(handle);
return 0;
}

编译运行

1
2
3
4
5
6
~ g++ -std=c++14 -fPIC -shared -o libhello.so hello.cpp -ftls-model=initial-exec
~ gcc -o main main.c -ldl
~ ./main
Error loading library: ./libhello.so: cannot allocate memory in static TLS block
~ LD_PRELOAD=./libhello.so ./main
Hello from shared library!

参考资料

cannot allocate memory in static TLS block问题记录

Things learned from "cannot allocate memory in static TLS block"