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 | /* Size of the static TLS block. Giving this initialized value |
也就是说_dl_tls_static_size
是预分配静态TLS区域的大小,只有在IE-model(也就是initial-exec,下面会介绍)的TLS下才生效
那么如何看当前的私有变量区使用大小呢?
可以readelf打印符号,也就是-sW
,然后在用awk匹配TLS类型,并且由于readelf打印全部符号的时候,动态符号打印两次,所以使用awk去重,最后用大小进行排序
1 | ~ readelf -sW libpytafcall.so | awk '$4 == "TLS" { if (!seen[$2]++) print }' | sort -k3,3n |
可以看到,是开源库jaegertracing的某个私有变量超限制了,mt19937_64是一个特别复杂的数据结构(标准中定义约为 312 个 uint32_t 的状态,占用约 2.5 KB 内存)
1 | thread_local std::mt19937_64 _randomNumberGenerator = [] { |
这个好改,改成私有变量指针就行了
1 | thread_local std::unique_ptr<std::mt19937_64> _randomNumberGenerator = [] { |
大部分的线程私有变量都可以通过这种方式压缩到很小,但是这种方案显然算不上完美,如果有海量的线程私有变量呢?
开启静态TLS的触发条件
根据搜索资料,以下方法可以判断是否开启静态TLS
1 | ~ readelf -d libhello.so |grep 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-dynamic
、local-dynamic
、initial-exec
或local-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-dynamic
或local-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
__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_TLStaf框架的代码就是这种情况,由于协程代码大量使用汇编编写,导致自动开启了STATIC_TLS无法关闭
解决办法
减少线程私有变量大小,换成指针
这个已经在上文说明了
使用LD_PRELOAD
gcc的文档说了,initial-exec模式是设计用于动态库和主程序一起启动的,这种情况下,动态链接器会计算额外的线程私有变量所需的区域
测试代码
动态库:
1 | //hello.cpp |
主程序:
1 |
|
编译运行
1 | ~ g++ -std=c++14 -fPIC -shared -o libhello.so hello.cpp -ftls-model=initial-exec |
参考资料
cannot allocate memory in static TLS block问题记录
Things learned from "cannot allocate memory in static TLS block"
-
2024-05-01
最近实在太忙啦,好几个月没写博客了,趁着五一放假补一篇
最近运维同学调整了告警策略,将连续coredump才告警,改成了每次coredump必告警
业务部门顿时向我报障了taf框架的coredump
一开始core在了tcmalloc,因为tcmalloc不会第一时间coredump,所以内存问题会跑一段时间才出现
-
2025-01-12
截止25年1月12日,打算使用dlmopen来装载serverless平台第三方动态库的计划,在挣扎了2周后正式宣布破产
总的来说dlmopen虽然已经有几十年历史了,但是在当前还是非常不成熟的,有很多细节问题没有解决
想用上dlmopen,需要深入理解glibc的实现原理,和patches的可能bug斗智斗勇,这个投入产出比很低
背景
serverless平台的设计原理属于机密,略过不提