问题描述
进程kill后迟迟不退出,pstack看到一直在等锁:
Thread 1 (Thread 0x2b3db1e9d900 (LWP 83917)):
#0 0x00002b3db6193f4d in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x00002b3db61915ae in _L_lock_39 () from /lib64/libpthread.so.0
#2 0x00002b3db61914e8 in pthread_cond_destroy@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
#3 0x00002b3dbd7a9736 in CSequenceRuntime::~CSequenceRuntime (this=0x2b3dbd9af340 <CSequenceRuntime::m_cInstance>, __in_chrg=<optimized out>) at mf_util.cpp:95
#4 0x00002b3db7aaae69 in __run_exit_handlers () from /lib64/libc.so.6
#5 0x00002b3db7aaaeb5 in exit () from /lib64/libc.so.6
#6 0x00002b3db7a93b1c in __libc_start_main () from /lib64/libc.so.6
#7 0x00000000004022b9 in _start ()
检查
很明显,在执行exit时调用__run_exit_handlers
执行全局变量/静态变量的析构函数。全局变量的析构函数中destroy一个条件变量。条件变量释放时一直在等锁,按理来说没有其他线程拿到条件变量的锁,而且条件变量的几个接口pthread_cond_signal
,pthread_cond_wait
, pthread_cond_broast
等都不会长时间拿着条件变量内部的锁。
用gdb查看一下pthread_cond_t
的信息(这里只贴出来了条件变量的数据):
m_signalCond = {__data = {__lock = 2, __futex = 2, __total_seq = 18446744073709551615, __wakeup_seq = 1, __woken_seq = 1,
__mutex = 0x2b65fb6eb360 <CSequenceRuntime::m_cInstance+32>, __nwaiters = 0, __broadcast_seq = 1},
__size = "\002\000\000\000\002\000\000\000\377\377\377\377\377\377\377\377\001\000\000\000\000\000\000\000\001\000\000\000\000\000\000\000`\263n\373e+\000\000\000\000\000\000\001\000\000", __align = 8589934594},
注意看,条件变量的__total_seq
的值非常大,是uint64_t(-1)。BUG主机环境上的glibc版本是2.17,通过代码查看只有在执行过pthread_cond_destroy
之后才会设置为-1。这样就有一个猜想,是不是这个条件变量已经释放过一次,或者包含这个条件变量的对象已经释放过一次,再进一步,就是全局变量两次释放,或者有多次释放的问题。
因为这个场景很容易重现,就用gdb跟踪了一下,确实存在两次释放,而且在第二次释放的时候会hang在这里。
全局变量正常情况下只会释放一次,只有在不同的链接库中存在相同名字的全局对象时,才会释放两次。
用一个脚本把所有的链接库都检查一遍,看是否有两个链接库包含这个全局变量即可。pstack信息中展示调用pthread_cond_destroy
的全局对象是CSequenceRuntime::m_cInstance
,用个脚本检查一下:
for file in `ls *.so`
do
res=`nm $file | grep _ZN16CSequenceRuntime11m_cInstanceE`
if [ "${res}x" != "x" ]
then
echo `ls -l $file` $res
fi
done
_ZN16CSequenceRuntime11m_cInstanceE
这个名字是我知道在哪个链接库中包含这个符号,先用nm查出来的:
lnbillkj%nm libxxxxD.so | grep CSequenceRuntime | grep m_cInstance
000000000020f300 B _ZN16CSequenceRuntime11m_cInstanceE
通过上面的脚本遍历检查,发现有个链接库本来应该是软链接,结果变成了真实文件。应该是操作人员把ln命令写成了cp。
输出结果:
-rwxr-xr-x 1 bill aigrp 472728 Sep 26 09:26 libmdb_frame_baseD.so 000000000020f340 B _ZN16CSequenceRuntime11m_cInstanceE
-rwxr-xr-x 1 bill aigrp 3789018 Sep 26 09:26 libmdb_in_baseD.so U _ZN16CSequenceRuntime11m_cInstanceE
-rwxr-xr-x 1 bill aigrp 3789018 Sep 26 09:25 libMdbInD.so U _ZN16CSequenceRuntime11m_cInstanceE
-rwxr-xr-x 1 bill aigrp 472728 Sep 26 09:24 libMFBaseD.so 000000000020f340 B _ZN16CSequenceRuntime11m_cInstanceE
链接库名字后面的字母U
表示符号未定义,只是在链接库中引用了,B
表示符号在未初始化的数据区中(BSS)。更多信息请看考man nm
。
网上也看到两个文章介绍全局变量多次析构的问题,不过与本例稍有不同,可以参考涨姿势:
- 排错经历:全局变量被多次析构
- 技巧:多共享动态库中同名对象重复析构问题的解决方法
这两篇文章对全局变量多次析构的原理进行了阐述,阅读非常有益。第1篇文章中介绍说如果同一个实现文件(cpp文件)被多个链接库编译进去,而程序同时引用这两个链接库,就会出现两个重名的全局变量。第2篇介绍在头文件中定义了全局变量(其实大型程序中绝对不会允许这么做),多个链接库引用了这个头文件,而可执行程序同时引用了这些链接库,就会出现多个同名链接库。
另外,除了上面两篇文章介绍的造成一个全局符号在两个链接库中定义,最近还发现一个情况,就是extern Class::static_member
。
比如:
/// test.h
class A
{
public:
int a;
static A sa;
};
cpp实现:
test.cpp
#include "test.h"
extern A A::sa; // 按照常规理解 extern只是变量声明,不带实现,然而事实并非如此
编译:
g++ test.cpp -o libtest.so -shared -fPIC -std=c++11
检查符号:
nm libtest.so | grep sa
输出结果:
[~]$ nm libtest.so | grep sa
0000000000201054 B _ZN1A2saE # B表示该符号已经定义
c++filt _ZN1A2saE
是 A::sa
。
呆若木鸡。