Python
调用动态库运行时报静态TLS
块不能分配内存根因分析报告缩写 | 全称 | 描述 |
---|---|---|
Python | Python | 一种跨平台的计算机程序设计语言 |
C++ | C++ | 一种静态数据类型检查的、支持多重编程范式的通用程序设计语言 |
jemalloc |
jemalloc |
一种内存分配器 |
TLS |
Thread-Local Storage | 一种实现线程私有全局变量的机制 |
Initial Executable | Initial Executable | 一种在程序启动后从加载的共享对象中引用TLS 变量的模型 |
【背景】
基于Python、C++,通过白盒测试手段完成对某些重要功能在代码层面的测试
【复现步骤】
1.Python
通过ctypes
第三方库调用C++ 动态库so文件中的函数
2.运行时报libjemalloc.so.2 cannot allocate memory in static TLS block
【期望】
可正常调用共享库so中的函数并得到正确的结果。
【环境】
硬件环境:X86
平台、ARM
平台(龙芯平台、申威平台未依赖jemalloc
)
python:3.7
jemalloc:5.1.0-3
在X86
平台和ARM平台上,Python调用C++动态库so中函数并运行的流程如下图所示,Python程序使用第三方库ctypes
加载libTestlib
、并通过jemalloc
进行内存分配。
。
【分析】
加载动态库jemalloc
时报不能在静态TLS
块中分配内存,可以猜测导致出现报错的原因有两种:
ctypes
调用C++动态库存在问题。jemalloc
内部在分配内存时有一些特性导致加载C++动态库存在问题。对第一种情况进行分析
使用ctypes.cdll.LoadLibrary
的方式加载依赖jemalloc
的动态库与加载不依赖jemalloc
的动态库,验证Python是否能正常加载so文件:
1.通过ctypes
加载不依赖jemalloc
的共享库:ldd libfibo.so
列出该库依赖的其他共享库
2.通过ctypes
加载依赖jemalloc
的共享库:ldd libfjemalloc.so
列出该库依赖的其他共享库
根据调用两个共享库的对比结果可知,Python通过ctypes
第三方库加载C++共享库的方案是可行的。
对第二种情况进行分析
jemalloc
是一个能够快速分配/回收内存,减少内存碎片,对多核友好,具有可伸缩性的内存分配器。
TLS
线程局部缓存,将数据和执行的特定的线程连续起来,在线程内部,各个函数可以像使用全局变量一样调用它,但它对线程外部的的其他线程是不可见的。
TLS
有4种访问模型,每个TLS
的引用均遵循其中之一,另外,可以从较一般的访问模型转换为更优化的访问模型。
访问类型 | 解释 |
---|---|
General Dynamic (GD) - dynamic TLS |
此模型允许从共享对象或动态可执行文件引用所有TLS 变量,当TLS 块首次从特定线程引用时,该模型还支持延迟分配TLS 块 |
Local Dynamic (LD ) - dynamic TLS of local symbols |
该模型是对GD模型的优化。编译器可以确定变量是在本地绑定的,还是在正在构建的对象中受保护的 |
Initial Executable (IE) - static TLS with assigned offsets |
这个模型只能引用作为初始静态TLS 模板一部分的TLS 变量。这个模板由进程启动时可用的所有TLS 块以及一个小的备份预留块组成。(通过固定预留块来满足有限数量TLS 的访问) |
Local Executable (LE) - static TLS |
这个模型只能引用作为动态可执行文件的TLS 块的一部分的TLS 变量 |
查看jemalloc
源码可知,jemalloc
默认打开initial-exec模型并定义了JEMALLOC_TLS_MODEL
通过JEMALLOC_TLS_MODEL
,找到了tsd.h
,里面的头文件清晰的解释了定义与不定义JEMALLOC_TLS_MODEL
的区别,如果默认开启了initial-exec模型,即定义了JEMALLOC_TLS_MODEL
,则使用tsd_tls.h
,如果没有,则使用tsd_generic.h
查阅tsd_tls.h
源码:直接将变量赋值给了已经存在的内存地址
查阅tsd_generic.h
:先申请内存空间,再将变量赋值给申请的内存地址
综上分析,jemalloc
默认开启initial-exec模型,直接将静态TLS
变量指向已有的内存空间,但Python是使用dlopen
的方式加载动态库,即没有分配好的内存空间,因此相关报错大概率与jemalloc
的initial-exec模型特性有关。
根据分析结果,相关报错大概率与jemalloc
的initial-exec模型特性有关。那么可以通过在编译jemalloc
时启用和禁用该功能来判断该分析结论是否正确。
【影响分析】
根据问题分析中的源码分析可知,禁用与不禁用initial-exec的不同在于变量的内存空间分配上,不会对原有代码的功能逻辑产生影响。
【实验环境】
系统:uos 20 1030
python:3.7
jemalloc:5.1.0-3
【实验设计】
实验操作:
操作1:编译一个依赖jemalloc
的动态库libtest.so
。
操作2:对jemalloc
进行重新编译禁用initial-exec。
操作3:进行操作2前,使用dlopen
加载libtest.so
,记录运行结果。
操作4:进行操作2后,使用dlopen
加载libtest.so
,记录运行结果。
实验步骤:
操作1->操作3->操作2->操作4
实验期望: $$ 对jemalloc重新编译后,可通过dlopen方式正常加载test.so $$
【实验验证】
步骤一:首先完成操作1,编译一个依赖jemalloc
的动态库libtest.so
。
使用如下命令进行编译
g++ -fPIC -shared test.cpp -o libtest.so -ljemalloc
步骤二:使用Python直接调用libtest.so
,运行结果如下:
步骤三:重新编译jemalloc
,禁用initial-exec
步骤四:使用Python直接调用libtest.so
,运行结果如下:
步骤五:运行结果对比
根据实验结果,未禁用initial-exec时运行程序会报错、禁用initial-exec后程序可正常运行,即禁用前,需要提前预留内存并将其分配给相应的静态TLS
使用,禁用后,则不再需要提前分配内存,因此数据符合期望值。
最终得出结论:
Python调用动态库报错的根本原因是:jemalloc
默认打开了Initial Executable (IE)
模型,而Python是通过dlopen
的方式加载动态库,导致没有预留的内存可分配给相应的静态TLS
使用。
根据上述实验可知,在程序启动后,加载包含静态TLS
的共享对象时,会给这些静态TLS
分配内存,由于Python的第三方库ctypes
是使用dlopen
的方式加载动态库(以dlopen的方式加载动态库只能在程序运行时进行内存分配,不能提前分配内存
)导致报不能在静态TLS
中分配内存的错误。
方案一:更换动态库的依赖对象
既然调用依赖jemalloc
的动态库,会报不能在静态TLS
中分配内存的错误,那么可以换一种内存分配器来规避该问题,常见的内存分配器有tcmalloc
、jemalloc
、ptmalloc
,如果我们的动态库在编译时没有指定jemalloc
,则默认链接到ptmalloc
,即经过上面的问题分析可知,使用ptmalloc
内存分配器不会出现相关报错。
方案二:禁用jemalloc
的Initial Executable模型特性
使用jemalloc
会报相关错误的原因是jemalloc5
新增了一个新特性,会在加载包含静态TLS
的动态库时给静态TLS
分配内存,但由于使用dlopen
的方式加载动态库,没有静态存储区可用,因此我们可以通过禁用jemalloc
的这个特性来解决这个问题,可使用shell脚本来实现该操作的自动化。
方案比较
从可行性、可操作性、稳定性等方面进行考虑可知,由于我们是通过Python加载动态库的方式对开发的C++源码进行测试,而开发构建出来的动态库在X86
平台和ARM平台默认依赖jemalloc
(龙芯和申威未依赖jemalloc
),如果要更换内存分配器,则潜在影响是未知且巨大的,方案一从可操作性、稳定性方面被否决;方案二目前能解决该问题,且可操作性很强,影响也较小。
方案一 | 方案二 | |
---|---|---|
可行性 | 可行 | 可行 |
易操作性 | 较差 | 较好 |
跨平台性 | 不可行 | 可行 |
稳定性 | 不稳定 | 稳定 |
影响评估:
jemalloc
默认开启Initial Executable模型特性的优点:在对静态TLS
进行内存分配时可直接赋值,不需要每次都重新申请内存空间,可使性能有一定的提升。
jemalloc
禁用Initial Executable模型特性的影响:首先,在性能方面会有一定的影响,其次,在动态申请内存时会存在一定的风险,例如可能会遇到没有内存可用的极端情况等。本次使用场景为基于Python
、对C++动态库的白盒测试,因此在性能及其他方面的影响可忽略。
最终结论:
最终选择方案二来解决该问题,即禁用jemalloc
的Initial Executable模型特性。
问题描述:Python通过ctypes
第三方库调用C++ 动态库so文件中的函数,运行时报libjemalloc.so.2 cannot allocate memory in static TLS block
。
根因查找步骤:现象->分析->假设->实验->验证->结论。
根本原因:jemalloc5
有一个新特性,在加载共享对象时会给静态TLS
分配预留内存。由于Python使用dlopen
的方式加载共享对象,不能提前预分配内存导致报错。
解决方案:重新编译jemalloc
禁用Initial Executable模型特性。
优化/改进:可向在jemalloc
源码中自动判定是否需要开启TLS
的Initial Executable特性的方向探索
收获与启示:在平时工作过程中,需要多多学习系统底层相关理论知识,在原理上实践。