Linux進程函數棧列印工具gstack源碼解讀、運用及擴展編程
每天讀一篇一線開發者原創好文
一.需求場景
近期在工作中需要分析一個Linux伺服器程序長期運行時的處理流程,想知道程序都執行了哪些函數調用鏈?比如,假設程序中有數千個函數,有時會觸發func1() -> func2() -> func3()調用鏈;有時會觸發func5() -> func2() -> func9()調用鏈;有時會觸發func107() -> func999() -> fun3() -> func557() -> func 123()調用鏈;等等。通過分析函數調用鏈,有助於加深理解程序運行流程,便於重點分明地分析和走查源代碼,提高工作效率,好處諸多。
二.思路分析
我們知道gdb的bt(backtrace)可以列印函數調用棧,但需要手動敲命令執行,不能批量多次運行,似乎不太方便。有沒有更好的工具和方法搞定這個需求呢?有的,gstack就是一款用於方便查看函數調用棧的工具。gstack的用途是「print a stack trace of a running process」,即列印一個正在運行的進程的函數調用棧。下面以一個正在運行的redis-server進程為例,執行gstack `pidof redis-server`即可看到該進程當前正在運行的3個線程各自的函數調用棧。這其實與gdb bt看到的差不多,而且我們的目標是只需要函數名,而不需要地址信息,那麼gstack有沒有什麼參數可以去掉每行的地址,以精簡列印呢?
如下所示,gstack竟然沒有幫助,這不像是一個正常的程序啊。用file `which gstack`查看,果然,它只是一個腳本,並不是一個正常的程序。
查看/usr/bin/gstack腳本源代碼,發現它其實只是包裝了gdb bt,並用sed對gdb bt的輸出結果做了過濾而已。如下給出gstack腳本源代碼的解讀,該腳本分為五部分:
第一部分:檢查是否提供一個入參,如果入參數量不是1,則列印用法提示,並退出腳本。
第二部分:檢查入參必須是一個當前正在運行的程序的PID,如果不是,則退出腳本。
第三部分:判斷內核是否支持gdb列印所有線程函數棧,如果不支持,則後續會將「bt」命令輸入gdb中;如果支持,則後續會將「thread apply all bt」命令輸入gdb中。
第四部分:執行gdb,通過「gdb [options] [executable-file] [process-id]」方式附著到指定PID的進程上,通過
第五部分:用sed去掉gdb輸出的無效行,只提取含有線程信息、函數信息的行。
通過上述對gstack腳本源代碼的分析可知,gstack只是gdb bt的簡單封裝,與我們的目標還有一定差距。看來需要自己編寫一些擴展腳本或程序,才能進一步達成目標。
首先,需要編寫一個腳本,重複運行多次gstack,採集目標程序足夠多次函數調用棧;其次,需要進一步凈化數據,比如函數地址信息就需要過濾掉;還有,需要歸併出不同的函數調用棧,找到不同的函數調用鏈,因為gstack輸出的函數棧是用Thread行分隔的,可以編寫一個程序來解析Thread行,將每個Thread塊(多行)放到哈希桶中排重(即,排除重複項),從而得到唯一不同的函數調用鏈。
三.擴展編程
首先,編寫一個makefile腳本,用shell for循環不斷調用gstack,將輸出結果追加到臨時文本文件中。
仍以redis-server為例,執行 make gstack_log PID=`pidof redis-server` NN=5,即可對redis-server連續運行5次gstack,並將結果保存到一個臨時文件tmp_gstack_1353.txt中。在正式採集時,可以將NN設置為很大,比如NN=2000次,以採集到足夠多的不同的函數調用棧信息。
然後,查看一下輸出的臨時文件的內容,即多次gstack輸出結果的羅列。下一步需要將每個Thread行所分隔的塊(多行),如塊1、塊2、塊3、塊4、、、進行凈化和排重。
編寫一個Node.js小程序gstack_data_format.js,用於對gstack輸出結果凈化並排重。程序讀入gstack結果文件(如:tmp_gstack_1353.txt),一行一行地讀入並累加到一個字元串變數中,遇到Thread行則停止累加,並將該字元串作為KEY添加到一個HASH桶中,因為HASH KEY天然不會重複,利用這個特點進行排重;遇到Thread行後,清空該字元串變數,重新開始累加;依次往複,直到讀完整個文件。程序基本流程如下,具體源代碼請見本文附錄。
如下給出gstack_data_format.js的運行效果。該gstack結果文件為1186行,採集到237個函數棧,進行凈化、排重後,得到2個唯一不同的函數調用棧。
四.總結
正如本文我們一步一步所做,對運行的目標進程,執行多次gstack,獲得大量函數調用棧,使用Node.js程序凈化並排重,準確獲得少量的唯一不同的函數調用棧。繼而只需要依照這少量函數棧的信息,走查相關源代碼。這個方法可以幫助我們快速理清程序的主要流程、便於我們快速走讀走查源代碼,提高學習和工作的效率。
附:gstack_data_format.js源代碼(用於對gstack結果凈化排重)
varfs=require("fs");
varasync=require("async");
varg_iLineMinLen=4;
varg_strFilename="tmp_gstack.txt";
if(process.argv.length
console.log("HELP: ",process.argv[],process.argv[1],"");
process.exit();
return;
}
g_strFilename=process.argv[2];
console.log("INPUT GSTACK_FILENAME: >");
varg_strFileContent=fs.readFileSync(g_strFilename,"utf-8");
varg_strLineAry=newArray();
g_strLineAry=g_strFileContent.split("
");
g_strFileContent=null;
varg_iAllLineCount=g_strLineAry.length;
console.log("GOT",g_iAllLineCount,"LINES FROM >");
varg_iNotNullCounter=;
varg_iThreadCounter=;
varg_strRecord="";
varg_strRecordLineCount=;
varg_iIsRecordOK=1;
varg_Hash=newArray();
async.forEachSeries(g_strLineAry,funcGetOneLine,funcGotAllLines);
functionfuncGetOneLine(strLine,callback){
if(!strLine){
callback();
return;
}
varstrLeft=strLine.substr(,7);
if(strLeft=="Thread "){
if(g_iThreadCounter!==){
if(g_strRecord&&g_iIsRecordOK){
g_Hash[g_strRecord]=g_strRecordLineCount;
}
}
g_iThreadCounter++;
g_strRecord=" ";
g_strRecordLineCount=;
g_iIsRecordOK=1;
}else{
varstrNew=strLine.replace(/(.*)/g," ");
strNew=strNew.replace(/0x.*? in /g,"");
if(strNew.length
g_strRecord=null;
g_iIsRecordOK=;
}else{
g_strRecord+=strNew+"
";
g_strRecordLineCount++;
}
}
process.nextTick(function(){
g_iNotNullCounter++;
callback();
});
}
functionfuncGotAllLines(err){
if(err){
throwerr;
}
console.log(" DONE TOTAL LINE :",g_iAllLineCount);
console.log(" DONE NOT NULL LINE:",g_iNotNullCounter);
console.log(" DONE THREAD LINE :",g_iThreadCounter);
variFuncKeyCount=;
for(strFuncKeying_Hash){
iFuncKeyCount++;
}
console.log(" DONE RECORD COUNT :",iFuncKeyCount);
console.log();
console.log("DETAILED RECORD INFORMATION:");
iFuncKeyCount=;
for(strFuncKeying_Hash){
iFuncKeyCount++;
console.log("FUNCTION CALLING STACK: NO."+iFuncKeyCount);
console.log(strFuncKey);
}
process.exit();
return;
}
TAG:中興開發者社區 |