IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

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

A programmer's life 2015-01-05 23:26:49 累计浏览 3,161 次
本机暂存

   理论上来说,多线程程序在链接时应该加上-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的实现有什么区别。

另外我又重复了一下网上那篇帖子中的实验

$ cat 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;
}
 
$ cat 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中的符号。
一切都符合预期。我猜是因为@@的效果。

同分类推荐文章

  1. 等了十年的 Go 链式管道,终于来了:seq 让你像写 Scala 一样写 Go (2026-06-25 18:38:18)
  2. Go 实验特性详解 (2026-06-21 10:05:27)
  3. amd64 微架构级别对 Go 程序性能提升多少? (2026-06-21 09:38:49)

查看更多 后端 文章 →

建议继续学习

  1. Linux如何统计进程的CPU利用率 (累计阅读 16,307)
  2. 我的 RHCA 之路 (累计阅读 14,011)
  3. Linux内存点滴 用户进程内存空间 (累计阅读 13,228)
  4. 给程序员新手的一些建议 (累计阅读 13,087)
  5. Linux 性能监控、测试、优化工具 (累计阅读 13,010)
  6. 关于linux内存free的一些事情 (累计阅读 12,865)
  7. ps - 按进程消耗内存多少排序 (累计阅读 12,685)
  8. Google怎么用linux (累计阅读 12,580)
  9. Linux Used内存到底哪里去了? (累计阅读 11,866)
  10. find命令的一点注意事项 (累计阅读 11,863)