當前位置:
首頁 > 最新 > Linux進程函數棧列印工具gstack源碼解讀、運用及擴展編程

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程序凈化並排重,準確獲得少量的唯一不同的函數調用棧。繼而只需要依照這少量函數棧的信息,走查相關源代碼。這個方法可以幫助我們快速理清程序的主要流程、便於我們快速走讀走查源代碼,提高學習和工作的效率。


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:中興開發者社區 |