https://www.ibm.com/developerworks/cn/aix/library/1206_yudh_unixproblemsolve/
定位 UNIX 上常見問題的經驗總結
2012 年 6 月 18 日
本文主要對 UNIX 平臺常見的問題進行了分類,介紹一些常見問題分析時使用的方法和命令,對以下三種常見問題的分析方法做了簡單介紹:UNIX 下 Crash 問題的分析方法、UNIX 下內存泄露問題的分析方法和 UNIX 下 performance 問題的分析方法。同時通過對下面兩個例子的介紹,鞏固了上面問題分析的介紹:
- 一個多線程應用的性能問題的分析
- 一個 crash 問題的分析
UNIX 程序常見問題分類
UNIX 下運行程序,經常會遇到以下幾類問題 :
- Crash
- 內存泄露
- 句柄泄露
- 進程不響應
- 性能不滿足預期
- 邏輯錯誤
UNIX 程序常見問題的分析方法
UNIX 下 Crash 問題的分析方法
crash 原理和 core 文件生成原因 ( 信號的介紹 )
Crash 是進程崩潰,是由於應用進程做了錯誤的操作 ( 例如,數組拷貝越界導致對系統內存進行了寫操作,使用了錯誤的指針地址 ), 操作系統嚮應用進程發送了信號,如果應用進程沒有做特殊處理,應用進程將 core dump 在進程當前的工作目錄下生成一個 core 文件,core 文件複製了該進程的存儲圖像,是一個內存映像。
不是所有的信號默認行爲都是 crash, 常見默認 crash 信號主要有:
SIGABRT SIGBUS SIGSEGV SIGILL SIGPIPE
可以通過 kill –l (適用所有 UNIX 平臺)查看信號的信息。
查看針對某個進程的所有信號的默認行爲(例如:在 Solaris 平臺使用 psig pid 命令查看,其他平臺的命令略有不同,請參考各自平臺用戶手冊).
[root@svs4qa09 SunOS a]# psig 25040 25040: /qatest/ModelerServer/5.0.0.0.64/modelersrv_15_0 -server HUP caught 0x10002958c 0 INT caught 0x100029580 0 QUIT default ILL default TRAP default ABRT default EMT default FPE default KILL default BUS default SEGV default SYS default PIPE ignored ALRM default TERM caught 0x100029580 0 USR1 default USR2 default CLD caught 0x100067f44 NOCLDSTOP
下面列舉一些常見信號的默認操作以及可能產生的原因:
例如:Solaris 平臺如下。下面的信息參考 Solaris 內核結構第 2 版第二章(Solaris 進程模型) 第 75 頁,其他平臺基本相同,請參考各自平臺用戶手冊:
信號 值 處理動作 發出信號的原因
SIGHUP 缺省的動作是終止進程 終端掛起或者控制進程終止
SIGINT 缺省的動作是終止進程 鍵盤中斷(如 break 鍵被按下)
SIGQUIT 缺省的動作是終止進程並進行內核映像轉儲(dump core)鍵盤的退出鍵被按下
SIGILL 缺省的動作是終止進程並進行內核映像轉儲(dump core)非法指令
SIGABRT 缺省的動作是終止進程並進行內核映像轉儲(dump core)由 abort(3) 發出的退出指令
SIGFPE 缺省的動作是終止進程並進行內核映像轉儲(dump core)浮點異常
SIGKILL 9 AEF Kill 信號 終止信號
SIGSEGV 缺省的動作是終止進程並進行內核映像轉儲(dump core)無效的內存引用
SIGPIPE 缺省的動作是終止進程 管道破裂 : 寫一個沒有讀端口的管道
SIGALRM 缺省的動作是終止進程 由 alarm(2) 發出的信號
SIGTERM 缺省的動作是終止進程 終止信號
SIGUSR1 缺省的動作是終止進程 用戶自定義信號 1
SIGUSR2 缺省的動作是終止進程 用戶自定義信號 2
SIGCHLD 缺省的動作是忽略此信號 子進程結束信號
SIGSTOP DEF 終止進程
SIGBUS 缺省的動作是終止進程並進行內核映像轉儲(dump core)總線錯誤 ( 錯誤的內存訪問 )
core 文件分析一般思路
首先使用 file 命令(所有 UNIX 平臺適用)查看 core 文件生成的源程序
bash-3.00$ file core core: ELF 64-bit MSB core file SPARCV9 Version 1, from 'qatest'
從以上結果可以看出,該 core 文件是由 64 位程序 qatest 生成的。
然後使用 gdb( 或者 dbx) 對 core 文件進行分析:
bash-2.05$ dbx ./qatest ./core
再使用 where 命令查看 core 的位置:
t@1 (l@1) program terminated by signal BUS(invalid address alignment) Current function is MCXML_700::MCSetting::MCSetting 87 fpValue = s.GetValue()->Clone(); (dbx) where
從這個 core 文件可以看到,它收到了 BUS 信號,crash 的位置在 = s.GetValue()->Clone() 函數。
更多有關 gdb,dbx 的使用請參考 gdb,dbx 用戶手冊。
core 文件無法生成常見原因
當程序崩潰時,並不是總會生成 core 文件。經常有下面的情況導致 core 文件沒有產生:
- 對 core 文件大小做了限制,可以通過 ulimit(所有 UNIX 平臺適用)的命令進行查看:
bash-3.00$ ulimit -a core file size (blocks, -c) unlimited data seg size (kbytes, -d) unlimited file size (blocks, -f) unlimited open files (-n) 256 pipe size (512 bytes, -p) 10 stack size (kbytes, -s) unlimited cpu time (seconds, -t) unlimited max user processes (-u) 29995 virtual memory (kbytes, -v) unlimited
建議使用下面的命令將這個限制改爲 unlimited
bash-3.00$ ulimit –c unlimited
- 磁盤空間是否充足,通過 df 命令(所有 UNIX 平臺適用)查看 Available 的空間是否充足。
bash-3.00$ df -k Filesystem 1024-blocks Used Available Capacity Mounted on / 0 40975117 99394509 30% / /dev 140369626 40975117 99394509 30% /dev
- 查看信號是否被捕獲(例如:Solaris 平臺使用 psig 進行查看,見上面的例子,其他平臺的命令略有不同,請參考各自平臺用戶手冊)。
如果上面的情況導致 core 文件沒有生成,請修改它。
- 沒有 core 文件產生,如何分析 crash
有時候經常發現進程 crash 了,但是 core dump 文件沒有產生。這時可以使用 dbx,gdb 等調試工具首先 attach 到運行的進程上,然後再執行業務,如果進程 crash,dbx 或者 gdb 將終止在 crash 的位置,我們便可以根據這個堆棧信息對 crash 進行分析,與分析 core 文件相同。
UNIX 下內存泄露問題分析方法
內存泄露簡單的說就是申請了一塊內存空間,使用完畢後沒有釋放掉。它的一般表現方式是程序運行時間越長,佔用內存越多,最終用盡全部內存,整個系統崩潰。
封裝 new 和 delete 對內存泄漏進行分析
通過對 new 和 delete 的封裝,將 new 和 delete 的過程通過日誌文件的保存記錄下來。然後對日誌文件進行分析,是否 new 和 delete 是匹配的,有哪些內存申請,但是沒有釋放。
下面通過一個簡單的測試程序(此代碼使用 C++ 語言實現,目前沒有考慮申請數組的情況)進行演示:
這個測試程序申請了 pTemp1,pTemp2,pTemp3 的三塊內存,但是僅僅釋放了 pTemp3,存在 pTemp1 和 pTemp2 的內存泄露。
程序解釋:
在每次內存申請時,將內存申請的信息註冊到 MAP 表中,在每次內存釋放時,將對應的內存信息從註冊表中刪除,這樣註冊表中將保存未釋放的內存信息,按照一定的規則將註冊表中的信息輸出(定時或者進程退出等)。然後我們從輸出信息中便可以分析出內存泄漏點。
通過自定義宏 DEMONEW 和 DEMODELETE 申請內存和釋放內存,在這兩個宏中,我們將內存的申請和釋放做了記錄,從而可以得到未釋放內存的信息,請參考下面的程序實現流程圖:
圖 1. 內存申請釋放流程:
圖 2.DEMONEW 實現流程:
圖 3.DEMODELETE 實現流程:
測試程序代碼:
#include <map> #include <iostream> #include <string> #include <fstream> // 申請內存時,存儲 new 位置的數據結構 typedef struct { std::string filename; int line; } MEMINFO; // 輸出文件 std::ofstream loginfo("//tmp/memory.log"); typedef std::map<long long, MEMINFO> MemMap; // 存儲內存申請記錄(在每次內存申請時,將內存申請的地址作爲鍵值, // 內存申請操作所在的文件名和行號作爲內容,存儲到下面的數據結構 memmap 中) MemMap memmap; // 註冊內存申請信息到上面的 map 容器中,輸入的參數分別爲內存地址,文件名,行號 void RegMemInfo(long long addr, const char *fname, long long lnum) { MEMINFO info; if (fname) { info.filename = fname; } info.line = lnum; memmap.insert(MemMap::value_type(addr, info)); }; // 卸載內存申請信息從上面的 map 容器中,輸入的參數爲內存地址 void UnRegMemInfo(long long addr) { if (memmap.end() != memmap.find(addr)) { memmap.erase(addr); } } // 定義宏 DEMONEW,封裝了內存申請的操作,在內存申請成功後,調用 RegMemInfo 功能, // 將內存信息註冊到 map 容器中 #define DEMONEW(p, ptype)\ do \ {\ p = new ptype;\ if (p)\ {\ RegMemInfo((long long)p, __FILE__, __LINE__);\ }\ else\ {\ std::cout<<"NEW failed"<<std::endl;\ }\ }\ while(0) // 定義宏 DEMODELETE,封裝了內存釋放的操作,在內存釋放時,調用 UnRegMemInfo // 功能,將內存信息從 map 容器中刪除 #define DEMODELETE(p) \ do\ {\ if (p)\ {\ UnRegMemInfo((long long)p);\ delete p;\ p = 0;\ }\ }while(0) // 寫信息流內容到文件 void WriteString(std::string buf) { loginfo << buf <<std::endl; } // 將整數轉換爲字符串 std::string Int2Str(int value) { char buf[16] = {0}; sprintf(buf, "%d", value); return buf; } // 輸出 map 容器中存儲的內存沒有釋放的信息 void Output() { loginfo.clear(); if (memmap.empty()) { WriteString("No Memory leak."); return; } MemMap::iterator iter; WriteString("The Memory leak is below:"); for (iter = memmap.begin(); iter != memmap.end(); ++iter) { std::string buf; std::string sAddr = Int2Str(iter->first); std::string sLine = Int2Str(iter->second.line); buf += "memory Address "; buf += sAddr; buf += ": FILE "; buf += iter->second.filename; buf += ", LINE "; buf += sLine; buf += " no freed"; WriteString(buf); } } // 測試程序主入口函數 int main(int argc, char* argv[]) { char* pTemp1 = 0; DEMONEW(pTemp1, char); char* pTemp2 = 0; DEMONEW(pTemp2, char); char* pTemp3 = 0; DEMONEW(pTemp3, char); DEMODELETE(pTemp1); Output(); loginfo.close(); return 0; }
上面測試程序的輸出是:
[dyu@xilinuxbldsrv ~]$ vi /tmp/memory.log The Memory leak is below: memory Address 280929008: FILE test.cpp, LINE 109 no freed memory Address 280929152: FILE test.cpp, LINE 111 no freed
輸出分析:
從輸出結果我們可以發現,此測試程序在 test.cpp 文件的 109 和 111 行各有一處內存泄漏,查看源代碼,它們分別是 pTemp1 和 pTemp2。
使用 Purify(適用所有 UNIX 平臺)或者 valgrind(適用 Linux 平臺)工具對內存泄漏進行分析
- 使用 Purify 對內存泄漏進行分析
Purify 是 IBM Rational PurifyPlus 的工具之一, 是一個面向 VC、VB 或者 Java 開發的測試 Visual C/C++ 和 Java 代碼中與內存有關的錯誤的工具,它確保整個應用程序的質量和可靠性。在查找典型的 C/C++ 程序中的傳統內存訪問錯誤, Rational Purify 可以大顯身手。在 UNIX 系統中,使用 Purify 需要重新編譯程序。通常的做法是修改 Makefile 中的編譯器變量。
例如定義 CC 變量爲 purify gcc
CC=purify gcc
首先運行 Purify 安裝目錄下的 purifyplus_setup.sh 來設置環境變量,然後運行 make 重新編譯程序。需要指出的是,程序必須編譯成調試版本。在編譯器命令(例如 Solaris 的 CC 編譯器,Linux 的 gcc 編譯器等)後,也就是必須使用"-g"選項。在重新編譯的程序運行結束後,Purify 會打印出一個分析報告。
測試程序(此代碼使用 C++ 語言實現):
#include <stdlib.h> void func1() { //char* pBuf = new char; } void func2() { char* pBuf = new char; } void func3() { char* pBuf = new char; } int main() { func1(); func2(); func3(); return 0; }
編譯程序:
[dyu@xilinuxbldsrv purify]$ purify g++ -g tst.cpp -o tst1
Purify 輸出:
[dyu@xilinuxbldsrv purify]$ ./tst1 16:50:59 (rational) OUT: "PurifyPlusUNIX" dyu@xilinuxbldsrv **** Purify instrumented ./tst1 (pid 530 at Fri Apr 6 16:50:59 2012) * Purify 7.0.0.0-014 090319 Linux (64-bit) (C) Copyright IBM Corporation. 1992, * 2009 All Rights Reserved. * For contact information type: "purify -help" * For Purify Viewer output, set the DISPLAY environment variable. * License successfully checked out. * Command-line: ./tst1 * Options settings: -g++=yes -purify \ -purify-home= /home/dyu/purify/PurifyPlus.7.0.0.0-014/Rational/releases/\ purify.i386_linux2.7.0.0.0-014 -process-large-objects=yes -gcc3_path=/usr/bin/g++ \ -cache-dir= /home/dyu/purify/PurifyPlus.7.0.0.0-014/Rational/releases/\ purify.i386_linux2.7.0.0.0-014\ /cache **** Purify instrumented ./tst1 (pid 530) **** Current file descriptors in use: 5 FIU: file descriptor 0: <stdin> FIU: file descriptor 1: <stdout> FIU: file descriptor 2: <stderr> FIU: file descriptor 26: <reserved for Purify internal use> FIU: file descriptor 27: <reserved for Purify internal use> **** Purify instrumented ./tst1 (pid 530) **** Purify: Searching for all memory leaks... Memory leaked: 2 bytes (100%); potentially leaked: 0 bytes (0%) MLK: 1 byte leaked at 0xa457098 * This memory was allocated from: malloc [rtlib.o] operator new(unsigned long) [libstdc++.so.6] operator new(unsigned long) [rtlib.o] func2() [tst.cpp:9] main [tst.cpp:20] __libc_start_main [libc.so.6] _start [crt1.o] MLK: 1 byte leaked at 0xa457138 * This memory was allocated from: malloc [rtlib.o] operator new(unsigned long) [libstdc++.so.6] operator new(unsigned long) [rtlib.o] func3() [tst.cpp:14] main [tst.cpp:21] __libc_start_main [libc.so.6] _start [crt1.o] Purify Heap Analysis (combining suppressed and unsuppressed blocks) Blocks Bytes Leaked 2 2 Potentially Leaked 0 0 In-Use 0 0 ---------------------------------------- Total Allocated 2 2
Purify 圖形輸出:
安裝 Xmanager 等工具,設置 DISPLAY 爲本機 IP,見下圖:
[dyu@xilinuxbldsrv purify]$ export DISPLAY=9.119.131.33:0
輸出分析:
從 purify 的輸出可以看出,此測試程序存在兩處內存泄漏,它分別是 func2 和 func3,在 tst.cpp 文件的第 9 和第 14 行。
- 使用 valgrind(現在僅僅支持 Linux 平臺)對內存泄漏進行分析
Valgrind 是一套 Linux 下,開放源代碼(GPL V2)的仿真調試工具的集合。Valgrind 由內核(core)以及基於內核的其他調試工具組成。內核類似於一個框架,它模擬了一個 CPU 環境,並提供服務給其他工具;而其他工具則類似於插件 (plug-in),利用內核提供的服務完成各種特定的內存調試任務。Valgrind 在對程序進行偵測的時候,不需要對程序進行重新編譯。
下面使用 valgrind 對一個簡單的測試程序進行。
測試程序:
同 Purify 的測試程序相同。
編譯程序:
[dyu@xilinuxbldsrv purify]$ g++ -g tst.cpp -o tst
valgrind 輸出:
[dyu@xilinuxbldsrv purify]$ valgrind --leak-check=full ./tst ==25396== Memcheck, a memory error detector ==25396== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al. ==25396== Using Valgrind-3.5.0 and LibVEX; rerun with -h for copyright info ==25396== Command: ./tst ==25396== ==25396== ==25396== HEAP SUMMARY: ==25396== in use at exit: 2 bytes in 2 blocks ==25396== total heap usage: 2 allocs, 0 frees, 2 bytes allocated ==25396== ==25396== 1 bytes in 1 blocks are definitely lost in loss record 1 of 2 ==25396== at 0x4A0666E: operator new(unsigned long) (vg_replace_malloc.c:220) ==25396== by 0x4005C7: func2() (tst.cpp:9) ==25396== by 0x4005DB: main (tst.cpp:20) ==25396== ==25396== 1 bytes in 1 blocks are definitely lost in loss record 2 of 2 ==25396== at 0x4A0666E: operator new(unsigned long) (vg_replace_malloc.c:220) ==25396== by 0x4005AF: func3() (tst.cpp:14) ==25396== by 0x4005E0: main (tst.cpp:21) ==25396== ==25396== LEAK SUMMARY: ==25396== definitely lost: 2 bytes in 2 blocks ==25396== indirectly lost: 0 bytes in 0 blocks ==25396== possibly lost: 0 bytes in 0 blocks ==25396== still reachable: 0 bytes in 0 blocks ==25396== suppressed: 0 bytes in 0 blocks ==25396== ==25396== For counts of detected and suppressed errors, rerun with: -v ==25396== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 4 from 4) [dyu@xilinuxbldsrv purify]$
輸出分析:
從 valgrind 的輸出可以看出,此測試程序存在兩處內存泄漏,它分別是 func2 和 func3,在 tst.cpp 文件的第 9 和第 14 行,與 purify 的檢測結果相同。
UNIX 下進程性能問題分析方法
- 檢查 CPU 佔用情況(包含單個線程的 CPU 使用情況)
下面分別對 Solaris 和 Linux 平臺做了舉例,其他平臺的命令略有不同,請參考各自平臺用戶手冊。
例如:在 Solaris 平臺
使用 prtdiag 命令查看系統 CPU 信息:
[/rnd/homes/builder1/purify >prtdiag
輸出信息:
SystemConfiguration:SunMicrosystemssun4uSunFireV210 Systemclockfrequency:167MHZ Memorysize:8GB ==================================== CPUs ==================================== E$ CPU CPU Temperature CPU Freq Size Implementation Mask Die Amb. Status Location --- -------- ---------- -------------------- ----- ---- ---- ------ -------- 0 1002 MHz 1MB SUNW,UltraSPARC-IIIi 2.3 - - online MB/P0 1 1002 MHz 1MB SUNW,UltraSPARC-IIIi 2.3 - - online MB/P1
輸出分析:
從上面的輸出可以發現,此服務器有兩個 CPU,以及各個 CPU 的信息。
使用 prstat 命令進程中每個線程的 CPU 使用情況:
[/rnd/homes/builder1/purify >>prstat -Lu root
輸出信息:
PID USERNAME SIZE RSS STATE PRI NICE TIME CPU PROCESS/LWPID 230 root 3896K 3072K sleep 59 0 0:04:37 0.0% nscd/18 26229 root 201M 118M sleep 59 0 4:38:06 0.0% java/4
輸出分析:
LWPID 雖然不是線程 ID,但是在 Solaris10 版本與線程 ID 是一一對應關係,所以可以通過 LWPID 進行分析。
例如:在 Linux 平臺
查看 CPU 個數 ( 使用 top 命令,然後按 1 鍵可顯示 CPU 的個數以及每個 CPU 的負載情況 ):
[dyu@xilinuxbldsrv purify]$top
輸出信息:
Tasks: 205 total, 7 running, 196 sleeping, 1 stopped, 1 zombie Cpu0 : 92.7%us, 7.3%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu1 : 94.2%us, 5.8%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Mem: 2033948k total, 1867908k used, 166040k free, 20088k buffers Swap: 4095992k total, 393420k used, 3702572k free, 389476k cached PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 3088 root 19 0 136m 73m 6960 R 57.2 3.7 0:00.89 cc1plus 3082 root 21 0 40580 33m 4732 R 52.7 1.7 0:00.75 cc1plus 3068 root 25 0 232m 163m 10m R 45.2 8.3 0:03.69 cc1plus 3085 root 19 0 98.8m 33m 5696 R 28.6 1.7 0:00.24 cc1plus 2901 root 25 0 89732 83m 4508 R 15.1 4.2 0:09.40 cc1plus 3069 dyu 15 0 10884 1120 768 R 1.5 0.1 0:00.04 top 1 root 15 0 10372 380 348 S 0.0 0.0 0:03.19 init 2 root RT -5 0 0 0 S 0.0 0.0 0:00.00 migration/0 3 root 34 19 0 0 0 S 0.0 0.0 0:00.15 ksoftirqd/0 4 root RT -5 0 0 0 S 0.0 0.0 0:00.00 watchdog/0
輸出分析:
從上面輸出可以得到,此服務器有兩個 CPU,均爲滿負荷工作,空閒的 CPU 使用均爲 0%。
查看進程中每個線程的 CPU 使用情況:
[dyu@xilinuxbldsrv purify]$ ps -e -o user,pid,ppid,tid,time,%cpu,cmd --sort=%cpu
--sort=%cpu 命令輸出要求按 CPU 的使用多少進行排序輸出。
輸出信息:
[dyu@xilinuxbldsrv ~]$ ps -e -o user,pid,ppid,tid,time,%cpu,cmd --sort=%cpu USER PID PPID TID TIME %CPU CMD root 2 1 2 00:00:00 0.0 [migration/0] root 3 1 3 00:00:00 0.0 [ksoftirqd/0] root 4 1 4 00:00:00 0.0 [watchdog/0] root 7 1 7 00:00:00 0.0 [watchdog/1] root 10 1 10 00:00:00 0.0 [khelper] root 27 1 27 00:00:00 0.0 [kthread] root 34 27 34 00:00:00 0.0 [kacpid]
輸出分析:
從上面輸出可以根據每個線程的 CPU 使用情況分析,性能瓶頸在哪個線程。
檢查 IO使用 iostat 命令可以查看系統 IO 狀態等信息。
例如:Solaris 平臺 iostat 輸出:
/rnd/homes/builder1/purify >>iostat 1 tty sd0 sd1 nfs1 nfs2 cpu tin tout kps tps serv kps tps serv kps tps serv kps tps serv us sy wt id 0 6 12 2 9 0 0 27 0 0 0 4 0 8 2 1 0 97 0 234 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 99 0 80 1 1 11 0 0 0 0 0 0 0 0 0 0 5 0 95 0 80 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 99
上面是 iostat 的一個簡單輸出,可以查看 iostat 的幫助(man iostat)瞭解更多信息。
- 使用 Quantify 對程序性能進行分析
Rational Quantify 是 IBM Rational PurifyPlus 的工具之一,可以按多種級別(包括代碼行級和函數級)測定性能,並提供分析性能改進所需的準確和詳細的信息,使您可以覈實代碼性能確實有所提高。使用 Rational Quantify,您可以更好地控制數據記錄的速度和準確性。您可以按模塊調整工具收集信息的級別: 對於應用程序中感興趣的那部分,可以收集詳細信息;而對於不太重要的模塊,您可以加快數據記錄的速度並簡化數據收集。使用“運行控制工具欄”,可以直接、實時地控制性能數據的記錄。既可以收集應用程序在整個運行過程中的性能數據,也可以只收集程序執行過程中您最感興趣的某些階段的性能數據。
下面是一個使用 Quantify 對程序性能進行分析的例子
測試程序(此代碼使用 C++ 語言實現):
#include <stdlib.h> #include <iostream> void func1() { for (int i = 0; i < 10000; ++i) { char* pBuf = new char[1024]; delete []pBuf; } } void func2() { for (int i = 0; i < 10000; ++i) { char* pBuf = new char[1024]; delete []pBuf; } } void func3() { for (int i = 0; i < 100; ++i) { std::cout << "Hello World" << std::endl; } } int main() { func1(); func2(); func3(); return 0; }
編譯程序:
[dyu@xilinuxbldsrv purify]$ quantify g++ -g performance.c -o performancetst
Quantify 輸出:
**** Quantify instrumented ./performancetst (pid 18503 at 20:12:14) Quantify 7.0.0.0-014 090319 Linux (64-bit) (C) Copyright IBM Corporation. 1992, 2009 All Rights Reserved. * For contact information type: "quantify -help" * License successfully checked out. Quantify: Sending data for 178 of 5713 functions from ./performancetst (pid 18772).........done. Quantify: Resource Statistics for ./performancetst (pid 18772) * cycles secs * Total counted time: 56984465 0.031 (100.0%) * Time in your code: 5599024 0.003 ( 9.8%) * Time in system calls: 51252884 0.027 ( 89.9%) * Dynamic library loading: 132557 0.000 ( 0.2%) * * Note: Data collected assuming a machine type of Intel-Core with * clock rate of 1.867 GHz. * Note: These times exclude Quantify overhead and possible memory effects. * * Elapsed data collection time: 0.383 secs * * Note: This measurement includes Quantify overhead. * To view your saved Quantify data, type: quantify -view /home/dyu/purify/performancetst.18772.0.qv
Quantify 的圖形輸出:
安裝 Xmanager 等工具,設置 DISPLAY 爲本機 IP,見下圖:
[dyu@xilinuxbldsrv purify]$ export DISPLAY=9.119.131.33:0
輸出分析:
從 Quantify 的輸出可以對程序的性能進行初步分析,func1 時間花費爲 43.14%,func2 爲 42.59%,func3 爲 14.27%。
示例演示
通過兩個實例去演示多線程性能問題和產品不兼容導致 crash 的問題。
一個多線程互斥導致性能瓶頸問題
- 問題描述:
對某個多線程程序,當單線程的情況下,執行任務 1 花費 70s,但是當配置爲 16 個線程時,執行任務 1 仍然花費時間大約 70s。
- 問題分析:
首先查看單個線程或多個線程的 CPU 使用情況:
PID USERNAME THR PRI NICE SIZE RES STATE TIME CPU COMMAND 11248 czhou 7 0 0 556M 555M cpu/1 0:17 6.25% CFTestApp
當多線程情況下,查看每個線程的 CPU 佔用情況: PID USERNAME SIZE RSS STATE PRI NICE TIME CPU PROCESS/LWPID 11248 czhou 3606M 3605M cpu1 0 0 0:07:11 6.25% CFTestApp/12 11248 czhou 3606M 3605M cpu1 0 0 0:07:11 0% CFTestApp/1 11248 czhou 3606M 3605M cpu1 0 0 0:07:11 0% CFTestApp/5 11248 czhou 3606M 3605M cpu1 0 0 0:07:11 0% CFTestApp/7
從上面可以發現。當多線程的情況下,在 16 個線程中僅僅一個線程的 CPU 佔用 6.25%,其他線程佔用均爲 0%。可以斷定大多數的線程被 block 住。然後需要查看這個進程的堆棧信息,得到每個線程的函數調用關係。
Pstack 11248 ----------------- lwp# 1 / thread# 1 -------------------- ff11af60 ???(ffbff8e8, ffbff8e0) ff15e328 dynamic_cast(258, 7, 10f88, 0, 139c0, 21508) + 58 0001110c main (1, ffbff9d4, ffbff9dc, 21400, 0, 0) + fc 00010b58 _start (0, 0, 0, 0, 0, 0) + 108 ----------------- lwp# 7 -------------------------------- ff11af60 ???(fef7ff30, fef7ff28) ff15e328 dynamic_cast(1, ff010224, 0, 0, 0, 0) + 58 00010fd8 __1cLwork_thread6Fpv_0_ (0, 0, 0, 0, 0, 0) + 8 ff165e48 _lwp_start (0, 0, 0, 0, 0, 0) Then I remove the dynamic_cast, the problem was resolved.
從上面的線程堆棧信息,我們可以看到,大部分的線程幾乎都 block 在 dynamic_cast 函數。
-
(3)問題解決:
針對上面的分析對這個性能瓶頸代碼進行修正。
一個由於產品不兼容導致 crash 的問題
- 問題描述:
在 Linux 平臺,產品 A 使用編譯器 icpc 編譯,產品 B 使用編譯器 g++ 編譯。進程 C 會同時加載產品 A 與產品 B,當進程 C 運行時調用產品 A 中的函數 funcA 時 crash 發生。
- 問題分析:
從 core 文件,我們可以得到下面的信息:
(gdb) where #0 std::_Rb_tree<int, std::pair<int const, int>, std::_Select1st<std::pair<int const, int> >, std::less<int>, std::allocator<std::pair<int const, int> > > ::_M_end (this=0x602a20) at /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../include/c++/4.1.2/bits/stl_tree.h:477 #1 0x0000000000400d3c in std::_Rb_tree<int, std::pair<int const, int>, std::_Select1st<std::pair<int const, int> >, std::less<int>, std::allocator<std::pair<int const, int> > > ::lower_bound (this=0x602a20, __k=@0x7fffffffe76c) at /usr/lib/gcc/x86_64-redhat-linux/4.1.2/include/c++/4.1.2/bits/stl_tree.h:1368 #2 0x0000000000400dbd in std::map<int, int, std::less<int>, std::allocator< std::pair<int const, int> > >::lower_bound ( this=0x602a20, __x=@0x7fffffffe76c) at /usr/lib/gcc/x86_64-redhat-linux/4.1.2/include/c++/4.1.2/bits/stl_map.h:576 #3 0x0000000000401a0c in std::map<int, int, std::less<int>, std::allocator<std::pair<int const, int> > >::operator[] ( this=0x602a20, __k=@0x7fffffffe76c) at /usr/lib/gcc/x86_64-redhat-linux/4.1.2/include/c++/4.1.2/bits/stl_map.h:345 #4 0x0000000000400a1b in funcA () at map.cpp:7
查找產品 A 的依賴庫,我們可以得到下面的信息
dyu@ xilinuxbldsrv> ldd libA.so linux-vdso.so.1 => (0x00007fffa2eb9000) libm.so.6 => /lib64/libm.so.6 (0x00002b6783a80000) libstdc++.so.5 => /usr/lib64/libstdc++.so.5 (0x00002b6783cd6000) libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00002b6783fb9000) libc.so.6 => /lib64/libc.so.6 (0x00002b67841d1000) libdl.so.2 => /lib64/libdl.so.2 (0x00002b678452f000) /lib64/ld-linux-x86-64.so.2 (0x00002b6783626000)
查找產品 B 的依賴庫,我們可以得到下面的信息
[dyu@xilinuxbldsrv]$ ldd libB.so linux-vdso.so.1 => (0x00007fff02dfc000) libstdc++.so.6 => /usr/lib64/libstdc++.so.6 (0x0000003d2c600000) libm.so.6 => /lib64/libm.so.6 (0x0000003d1a400000) libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x0000003d27e00000) libc.so.6 => /lib64/libc.so.6 (0x0000003d19c00000) /lib64/ld-linux-x86-64.so.2 (0x0000003d19800000)
這個 crash 的位置是在 stl 的 map 數據結構中,從上面的線程棧調用可以發現,4.1.2 爲 libstdc++.so.6,但是從 A 的依賴庫看,產品 A 依賴 libstdc++.so.5 而不是 libstdc++.so.6,所以 funcA 應該調用 libstdc++.so.5。
從上面我們可以發現 crash 的原因是由於 libstdc++.so 的調用錯誤導致的。
- 問題解決:
在編譯選項中增加 -fvisibility=hidden ,在 API 聲明中增加
__attribute__ ((visibility ("default")))。
UNIX 程序問題分析常用命令
- ulimit -- 設置和查看用戶的使用的資源限制情況
- nm -- 顯示目標文件的符號表信息
- ldd –顯示動態庫的依賴信息
- pstack(Solaris, Linux), procstack(AIX)-- 打印十六進制地址和符號名稱
- pmap(Solaris, Linux), procmap(AIX) –打印地址空間映射
- pldd(Solaris), procldd(AIX) —列出進程加載的庫
- pfiles(Solaris), procfiles(AIX)-- 報告有關的所有文件描述符
- prstat(Solaris), ps -e -o user,pid,ppid,tid,time,%cpu,cmd --sort=%cpu(Linux)-- 檢查每個線程的處理器
- ps –報告進程的狀態
- iostat –報告中央處理單元(中央處理器)統計和輸入 / 輸出設備和分區統計
- pwdx(Linux,Solaris) pid 顯示當前工作目錄
- top(Linux,Solaris,HP),topas(AIX)
總結
本文簡單介紹了作者在 UNIX 平臺的一些經常遇到的問題以及一些基本命令的使用,希望對讀者能有幫助。良好的基礎知識(C/C++ 語言,操作系統,內核結構等)是分析問題解決問題的基礎,同時一些 debug 工具以及一些第三方工具的熟練使用對問題分析也能很好的幫助作用。另外良好的編程習慣(例如申請的變量要初始化等)是避免問題產生的有效手段,在軟件開發前期的缺陷控制應該是我們的目標。
參考資料
學習
- 有關 Rational Purify 的使用方法,可以參考 Rational Purify使用及分析實例http://www.ibm.com/developerworks/cn/rational/r-cail/。
- 內存檢測工具 Virgrind 的介紹和使用方法,可以參考 Virgrind 主頁。
- AIX and UNIX 專區:developerWorks 的“AIX and UNIX 專區”提供了大量與 AIX 系統管理的所有方面相關的信息,您可以利用它們來擴展自己的 UNIX 技能。
- AIX and UNIX 新手入門:訪問“AIX and UNIX 新手入門”頁面可瞭解更多關於 AIX 和 UNIX 的內容。
- AIX and UNIX 專題彙總:AIX and UNIX 專區已經爲您推出了很多的技術專題,爲您總結了很多熱門的知識點。我們在後面還會繼續推出很多相關的熱門專題給您,爲了方便您的訪問,我們在這裏爲您把本專區的所有專題進行彙總,讓您更方便的找到您需要的內容。
- AIX and UNIX 下載中心:在這裏你可以下載到可以運行在 AIX 或者是 UNIX 系統上的 IBM 服務器軟件以及工具,讓您可以提前免費試用他們的強大功能。
- IBM Systems Magazine for AIX 中文版:本雜誌的內容更加關注於趨勢和企業級架構應用方面的內容,同時對於新興的技術、產品、應用方式等也有很深入的探討。IBM Systems Magazine 的內容都是由十分資深的業內人士撰寫的,包括 IBM 的合作伙伴、IBM 的主機工程師以及高級管理人員。所以,從這些內容中,您可以瞭解到更高層次的應用理念,讓您在選擇和應用 IBM 系統時有一個更好的認識。