关于 pthread_key_t 导致的 Android Crash 的探索
此前我负责的 SDK 已集成多个司内业务,一切运行正常,最近在接入到一些游戏项目中的时候发现存在比较多关于 libc.so 的 crash,在游戏中某个场景会使用SDK 进行逻辑处理,在部分手机会在短时间就直接 Crash,且集中在性能比较好的手机中。经过一番折腾,最后被定位在了一个跟 SDK 没有什么关系的地方:pthread_key_t
Crash 表现
在 Crash 上报平台中收到诸多的 Crash 上报,调用的形式多种多样,异常名都是signal 6 (SIGABRT)
但崩溃调用栈最终都停留在
/apex/com.android.runtime/lib64/bionic/libc.so pc (abort+168)
以及中间都会经过:/apex/com.android.runtime/lib64/bionic/libc.so (pthread_once+136)
难以复现的问题
由于我们的项目依赖于其他业务的SDK,最终的 SDK 打包合并在 Unity 的游戏中,我们不能直接使用游戏侧代码逻辑进行编译打包进行调试,这为问题的排查增大了一定的难度,只能在 Unity 的 demo 工程具体的表现为:
1、部分性能好的手机(如小米14 pro)才会出现 Crash,而且在对应的游戏中必现,有些游戏又不会复现
2、SDK里面同样的代码逻辑在测试 App 工程中完全不会复现
3、SDK里面同样的代码逻辑在 Unity 测试游戏 demo 中也完全不会复现
4、使用了业务方(游戏侧)的 Unity 的各种配置,依然没有复现
5、崩溃栈中有涉及到 thread 相关的关键词,怀疑是线程相关问题,但在原生层开辟N个线程也没有复现
6、其他各种尝试都没有复现:开辟大量内存、Unity 与 Android 调用方式调整……
解决线索与方案
一开始是怀疑业务方的环境与 SDK 运行环境有冲突,毕竟 SDK 已经在诸多业务中上线并正常运行了很久,不应该是 SDK 本身代码逻辑不对导致的才对。但没过多久,我们在另一个业务中也发现了这个问题,那说明并不是一个游戏环境导致。
解决问题直接看对应的崩溃栈,其崩溃栈都是使用相关的组件导致的 Crash,询问了相关的开发大佬之后并没有得到解决办法,原因是我们使用的版本相对较老,经历了比较久的迭代,逻辑改掉了很多。二是有可能这个问题在新版本中已经修复掉了。于是我们进行了一大波改造升级,经过一段时间后,再次集成到业务方,原以为这个问题就此解决了,调用了一下创编 SDK 之后依然 crash,此时心拔凉拔凉……
但这时候比较能确定的是,这个 crash 跟依赖的SDK 没有直接关系,可能是由其他的环境问题什么。
问题线索 pthread_key
在最开始的排查问题过程中一直在关注在环境的差异上面,经过一番折腾依然没有效果,方向错误了,于是再次回到 Crash 栈中来,在崩溃栈中都含有:pthread_once
、emutls_get_address
、cxa_get_globals
、emutls_init
相关的关键词,由于平时完全没有接触过这几个函数,对他们的了解比较少。但经过一番搜索之后,他们都有提到一个关键的术语:TLS (thread-local storage)
以及对几个函数调用的源码进行查看,发现这几个函数最终涉及到的都是 pthread
使用或者创建相关的
其中在 cs.android的 emutls.c 源码里有:
static void emutls_init(void) { |
这里基本上可以和崩溃栈对应上了,正是这里执行的 abort()
,那么原因是否是由 pthread_key_create()
引起的呢?继续对 pthread_key_create
研究,原来在 Bionic 中,能够被开发者所使用的 Pthread Key 数量,是 PTHREAD_KEYS_MAX
宏所定义的 128 个。
那我们遇到的问题是否也是同一个问题呢?得到答案最好的方式是验证,想办法做一个验证,用代码把系统能提供的 pthread_key 耗尽然后再使用我们创编SDK的功能,使用如下代码创建 PTHREAD_KEYS_MAX
个 pthread_key_t
,再直接使用创编 SDK,果不其然 Crash了,而且 crash 栈与上报的数据比较的一致(没有完全一致,毕竟一些场景还是会有点差异)。
以下的代码会耗尽目前程序中的 key,只创建 pthread_key,而不释放掉
void available_key() { |
从打印的日志里面看,在 Unity demo App 里面大概创建到 60 多的时候就创建失败了,也就是说Unity 本身可能就使用了很多 key,留给应用层开发的就只有几十个 key 了。
虽然尝试是Crash了,但怎么能证明这个就是导致业务方 Crash 就是这个原因呢?以及怎么解释有的手机为什么会Crash,有的手机不会Crash呢?
我们继续,从目前的推论来看,我们的创编SDK需要使用 pthread_key_t
, 可能数量不够了,也就说创编SDK需要使用一定数量的key,那我们将刚才代码里面的 i < PTHREAD_KEYS_MAX;
进行调整,我们预留足够的 key 空间给创编SDK,i < target_number;
于是在之前 crash 的手机和未 crash 的手机做了一次对比。
以下是对部手机的测试结果,记录日志前面的数字就是代码里面的 target_number
从对比结果看,两部手机他们可以供应用层使用的 key 的数量是不同的,之前会 crash 的手机它可以使用的 key 明显是少于之前未 crash 手机的数量的,这也就能解释为什么有的手机为什么会 crash,有的手机不会 crash 了。以及,可以推测出来创编SDK使用了5个key左右。
这里提一下在解决问题之初,我们发现 crash 的手机基本上都是市面上比较好的手机,且手机的 GPU 都集中于 Adreno 比较新的型号,一度误以为是相关底层 SDK 未进行兼容性适配导致。为什么性能更好的手机使用的 pthread_key_t
会更多?猜测可能是好的手机 Unity 运行相关的东西或者优化(这里的优化指的是游戏特效或者功能玩法)更多,所以消耗的资源就更多一点,当然这里只是个人猜测,具体原因还需要深入了解。
还剩下一个问题:业务方的 App 为什么会Crash?于是将上面的 available_key()
方法进行一次包装,并将其打包集成进游戏侧测试,从日志里面看到留给我们创编SDK使用的 key 只有3个了!而我们的 SDK 需要5个左右,问题原因基本就是这个了,那如何解决呢?
方案解决
究其根本原因是 Android 系统的 pthread_key_t
的使用数量的限制,那么最直接的解决方式那就是降低对 pthread_key_t
的使用,但是由于我们依赖使用其他地方的 SDK,对其项目直接优化更改可能成本相对较高,直接修改源码解决的话一时半会儿无法解决。这里先对 pthread_key_t
数量限制相关的问题进行一些研究总结:
在 Android 官方源码 pthread.h#pthread_key_create() 里面有提到:
There is a limit of
PTHREAD_KEYS_MAX
keys per process, but most callers should just use the C or C++thread_local
storage specifier anyway. When targeting new enough OS versions, the compiler will automatically use ELF TLS; when targeting old OS versions the emutls implementation will multiplex pthread keys behind the scenes, using one per library rather than one per thread-local variable. If you are implementing the runtime for a different language, you should consider similar implementation choices and avoid a direct one-to-one mapping from thread locals to pthread keys.
Returns 0 on success and returns an error number on failure.
int pthread_key_create(pthread_key_t* _Nonnull **key_ptr, void (* _Nullable **key_destructor)(void* _Nullable)); |
可以看到官方建议使用 thread_local
去实现 TLS,以及在新的系统版本中会使用 ELF TLS
对 pthread_key_t
将不直接依赖,
但条件相对比较高,参考官方更新:需要 miniSDK>29 和NDK r26
ELF TLS (Available for API level >= 29)
Android supports ELF TLS starting at API level 29. Since NDK r26, clang will automatically enable ELF TLS forminSdkVersion 29
or higher. Otherwise, the existing emutls implementation (which usespthread_key_create()
behind the scenes) will continue to be used. This means that convenient C/C++ thread-local syntax is available at any API level; at worst it will perform similarly to “roll your own” thread locals usingpthread_key_create()
but at best you’ll get the performance benefit of ELF TLS, and the NDK will take care of the details.
最后我们的解决方式是依据上面 pthread_key_create
提到的
There is a limit of
PTHREAD_KEYS_MAX
keys per process…..
重点是:per process,每个进程有 PTHREAD_KEYS_MAX
,这个PTHREAD_KEYS_MAX
被定义在 limits.h 现在的 Android 基本上都是定义为128。那那我们将我们的SDK 使用的时候放在一个单独的进程不就ok了?事实是的,由于我们的SDK向业务只是提供一个 素材输入=>视频输出的功能,中间过程是一个黑盒,那么这个场景使用多进程是完全OK的,使用多进程还有一个好处就是能与游戏进程相独立,尽量减少两者之间的依赖。但多进程也带来了一些门槛,但这相比与改渲染 SDK 底层的源码来说是相对简单很多的,最终经过一番折腾我们将创编SDK得渲染放在了一个单独的进程,后试验运行在之前 Crash 过的游戏业务上一切正常。
pthread_key 检测工具
为了以后接入其他游戏前不再发生类似的Crash问题,在接入业务前做一些技术评估,pthread_key_t
可用数量可能也需要成为一个考量指标,可用数的不同,可能需要不同的技术方案,我专门写了一个小工具,可方便查询业务项目目前使用了多少 pthread_key_t,能帮助项目排查当前问题是否是由于 pthread_key_t
占满导致的相关问题。
不过我更想做一个能够检测项目里面有消耗过 pthread_key_t 的地方,将其 hook 住,打印出来对应的调用栈,这样就能方便业务排查。未来,随着 Android 业务的复杂化,这种问题可能会变成更多大型项目将会遇到。调研发现 Tencent 对外开源项目 Tencent/matrix 已经有针对 pthread_key 做了相关的hook,业务侧也可以直接使用 matrix 进行检测,但其项目相对比较庞大,以及使用的方式较复杂。于是将其精简到一个小工具内,整体大小只有1MB 不到。
源码地址:PthreadKeyDetect
总结
本文主要记录了i创作SDK出现大佬了关于 libc.so
的 Crash,经过调查,问题被定位在 pthread_key_t
资源耗尽的问题上,并对其进行了相关研究,最后并解决了该问题的过程。