Linux程序链接时-lpthread对程序正确性的影响

理论上来说,多线程程序在链接时应该加上-lpthread或者-pthread。实际上很多时候忘记加这个也能链接过去,

最近我线上的一个重要服务经常卡死,CPU使用率很高。用pstack看,经常是停留在这样的地方:
# 0x0000003a21e0e054 in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x0000003a21e0bca1 in pthread_cond_signal@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
#2 0x00007f04f8e0696d in __db_pthread_mutex_unlock () from /usr/lib64/libdb-4.7.so
#3 0x00007f04f8e0655d in __db_tas_mutex_unlock () from /usr/lib64/libdb-4.7.so
#4 0x00007f04f8ea6b8e in __db_cursor_int () from /usr/lib64/libdb-4.7.so
#5 0x00007f04f8ebd9af in __db_cursor () from /usr/lib64/libdb-4.7.so
#6 0x00007f04f8ebe2c0 in __db_get () from /usr/lib64/libdb-4.7.so
#7 0x00007f04f8ebe63b in __db_get_pp () from /usr/lib64/libdb-4.7.so
大部分CPU都被__db_tas_mutex_unlock和__db_tas_mutex_lock这两个函数占去了。按理说unlock一个mutex不该占用太多cpu才对。(后来我发现这是bdb的mutex的实现太畸形太挫了)
我在网上发现有个工程师遇到了和我类似的问题
http://www.jimmo.org/threads-blocked-in-pthread_cond_signal-on-linux/ 他说如果忘记链接到pthread库,可能导致条件变量所依赖的mutex没有被正确初始化,而导致程序死锁等。理论上来说是这样的,但是实际上我没有办法重现作者的实验。
我发现libdb-4.7.so中pthread的符号和我预期的不一样

$ readelf -a /usr/lib64/libdb-4.7.so |grep pthread_cond_signal
000000370f88  000f00000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_cond_signal + 0
     15: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND pthread_cond_signal@GLIBC_2.3.2 (3)

我自己如果编译一个小程序,例如

#include <pthread.h>

int func(){
  pthread_cond_signal(NULL);
  return 0;
}
$ gcc -o libt.so test.c -shared -fPIC
$ readelf -a libt.so  |grep pthread
000000201018  000300000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_cond_signal + 0
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND pthread_cond_signal@GLIBC_2.3.2 (2)
    45: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND pthread_cond_signal@@GLIB

它的符号表中应该有两条记录。不知道为什么bdb中只有一条。
后来查了下文档终于搞明白,带@的是versioned symbol。weak symbol是给静态库用的,动态库没法用weak symbol。
glibc中的pthread的mutex等的实现是空的,这是为了提高单线程程序的执行效率。当某个程序真的需要使用多线程的时候,得让libpthread.so把正确的symbols填充进去。静态库可以通过weak symbol做到这一点,而动态库可以直接覆盖,也可以用versioned symbol。
$ nm /lib64/libc.so.6 | grep pthread_mutex
00000000000f8110 T pthread_mutex_destroy
00000000000f8140 T pthread_mutex_init
00000000000f8170 T pthread_mutex_lock
00000000000f81a0 T pthread_mutex_unlock

注意,是T,不是W。 (cond的输出更有所不同。稍后叙述)
当编译一个不带-pthread的程序的时候,
$ ldd t
linux-vdso.so.1 => (0x00007fff5f4e2000)
libstdc++.so.6 => /usr/lib64/libstdc++.so.6 (0x00007fbef59f9000)
libm.so.6 => /lib64/libm.so.6 (0x00007fbef5775000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fbef555e000)
libc.so.6 => /lib64/libc.so.6 (0x00007fbef51ca000)
/lib64/ld-linux-x86-64.so.2 (0x00007fbef5d2f000)

当编译一个带-pthread的程序之后
$ ldd t
linux-vdso.so.1 => (0x00007fff805fe000)
libstdc++.so.6 => /usr/lib64/libstdc++.so.6 (0x00007f72c5a26000)
libm.so.6 => /lib64/libm.so.6 (0x00007f72c57a2000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f72c558b000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f72c536e000)
libc.so.6 => /lib64/libc.so.6 (0x00007f72c4fda000)
/lib64/ld-linux-x86-64.so.2 (0x00007f72c5d5c000)

libpthread.so.0一定是出现在libc.so.6之上。它也提供了同样的符号
$ nm /lib64/libpthread.so.0 | grep pthread_mutex_init
0000000000008d70 T __pthread_mutex_init
0000000000008d70 t __pthread_mutex_init_internal
0000000000008d70 T pthread_mutex_init
默认情况下,链接器是按顺序优先选择第一个找到的。所以它会使用libpthread.so.0中的符号替换libc.so.6中的。
条件变量要更复杂一些。

$ nm /lib64/libc.so.6 | grep pthread_cond_init
00000000000f7ff0 t __pthread_cond_init
0000000000127c30 t __pthread_cond_init_2_0
00000000000f7ff0 T pthread_cond_init@@GLIBC_2.3.2
0000000000127c30 T pthread_cond_init@GLIBC_2.2.5

libc中提供了两个版本的条件变量的实现,@@后面是版本号。一个是GLIBC_2.2.5,一个是GLIBC_2.3.2。其中GLIBC_2.3.2是基于NPTL的。由于它定义了多个版本的实现,所以就应该有一个默认实现。带@@的就是默认实现。

我没看出来libpthread和libc中的cond vars的实现有什么区别。
另外我又重复了一下网上那篇帖子中的实验

test.c:

#include <pthread.h>

static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

int func(){
   pthread_mutex_lock(&mutex);
   pthread_mutex_unlock(&mutex);
   return 0;
}

main.c:

extern int func();

int main(){
   func();
   return 0;
}

$ gcc -shared -fPIC -o libt1.so test.c -g
$ gcc -o m main.c -g -lt1 -L. -Wl,-rpath,.

两次编译我都故意没有加-pthread,然后发现pthread_mutex_lock确实使用的是空实现。
但是动态库的符号是这样写的:

$ nm libt1.so  |grep pthread
U pthread_mutex_lock@@GLIBC_2.2.5
U pthread_mutex_unlock@@GLIBC_2.2.5

$ readelf -a libt1.so | grep pthread
000000200888  000500000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_mutex_lock + 0
000000200890  000600000007 R_X86_64_JUMP_SLO 0000000000000000 pthread_mutex_unlock + 0
      5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND pthread_mutex_lock@GLIBC_2.2.5 (2)
      6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND pthread_mutex_unlock@GLIBC_2.2.5 (2)
     58: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND pthread_mutex_lock@@GLIBC
     60: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND pthread_mutex_unlock@@GLI

当我修改主程序的链接参数后:
$ gcc -o m main.c -g -lt1 -L. -Wl,-rpath,. -pthread
$ ldd ./m
linux-vdso.so.1 => (0x00007fff710bf000)
libt1.so => ./libt1.so (0x00007fc37216f000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fc371f47000)
libc.so.6 => /lib64/libc.so.6 (0x00007fc371bb3000)
/lib64/ld-linux-x86-64.so.2 (0x00007fc372371000)
由于在它启动的时候,就已经链接到了pthread,所以也就没有问题。它会使用pthread的实现,无需修改so的链接参数。

然后我又试了一下dlopen。
我把main函数改成这样

#include <dlfcn.h>
#include <stdio.h>


int main(){
   int (*func)();
   void* handle =dlopen("./libt1.so", RTLD_NOW);
   if (!handle) {
        fprintf(stderr, "%s\n", dlerror());
        return -1;
    }
   func = (int (*)()) dlsym(handle, "func");
   func();
   return 0;
}

$gcc -o m main.c -g -pthread -ldl

经gdb调试,依然使用的是/lib64/libpthread.so.0中的符号。
一切都符合预期。我猜是因为@@的效果。

此博客中的热门博文

少写代码,多读别人写的代码

在windows下使用llvm+clang

tensorflow distributed runtime初窥