歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

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的實現有什麼區別。

另外我又重復了一下網上那篇帖子中的實驗


$ 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中的符號。
一切都符合預期。我猜是因為@@的效果。

Copyright © Linux教程網 All Rights Reserved