理論上來說,多線程程序在鏈接時應該加上-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中的符號。
一切都符合預期。我猜是因為@@的效果。