【轉】systemtap原理及使用
SystemTap的架構
SystemTap用於檢查執行的核心的兩種方法是 Kprobes和 返回探針。但是理解任何核心的最關鍵要素是核心的對映,它提供符號資訊(比如函式、變數以及它們的地址)。有了核心對映之後,就可以解決任何符號的地址,以及更改探針的行為。
Kprobes從 2.6.9
版本開始就新增到主流的 Linux 核心中,並且為探測核心提供一般性服務。它提供一些不同的服務,但最重要的兩種服務是 Kprobe
和 Kretprobe。Kprobe特定於架構,它在需要檢查的指令的第一個位元組中插入一個斷點指令。當呼叫該指令時,將執行鍼對探針的特定處理函式。執行完成之後,接著執行原始的指令(從斷點開始)
Kretprobes有所不同,它操作呼叫函式的返回結果。注意,因為一個函式可能有多個返回點,所以聽起來事情有些複雜。不過,它實際使用一種稱為 trampoline的簡單技術。您將向函式條目新增一小段程式碼,而不是檢查函式中的每個返回點。這段程式碼使用 trampoline地址替換堆疊上的返回地址 —— Kretprobe 地址。當該函式存在時,它沒有返回到呼叫方,而是呼叫 Kretprobe(執行它的功能),然後從 Kretprobe返回到實際的呼叫方。
圖 1展示了 SystemTap
的基本流程,涉及到 3 個互動實用程式和 5
個階段。該流程首先從 SystemTap 指令碼開始。您使用 stap
實用程式將 stap指令碼轉換成提供探針行為的核心模組。stap
流程從將指令碼轉換成解析樹開始 (pass 1)。然後使用細化(elaboration)步驟 (pass 2)中關於當前執行的核心的符號資訊解析符號。接下來,轉換流程將解析樹轉換成
C
原始碼 (pass 3)
並使用解析後的資訊和 tapset指令碼(SystemTap
定義的庫,包含有用的功能)。stap 的最後步驟是構造使用本地核心模組構建程序的核心模組 (pass 4)。
1. SystemTap流程
有了可用的核心模組之後,stap
完成了自己的任務,並將控制權交給其他兩個實用程式 SystemTap:staprun
SystemTap 的一個有趣特性是快取指令碼轉換的能力。如果安裝後的指令碼沒有更改,您可以使用現有的模組,而不是重新構建模組。圖 2顯示了 user-space 和 kernel-space元素以及基於 stap 的轉換流程。
3 Systemtap工作原理
tapsets是一個指令碼庫,包含了許多tapset,每一個tapset一般為某一核心子系統或特定的功能塊預定義了一套探測點、輔助函式或全域性變數供使用者指令碼或其它的tapset引用,它定義的一些資料能夠被每一個探測點處理函式或指令碼使用,這些資料通常通過使用處理函式語句塊(HSB Handler Statement Block)來出口,HSB語句塊中的變數就是被出口的資料。tapset一般由該核心子系統的開發者或對子系統非常瞭解的開發者編寫,既使用了指令碼語言,也使用了C語言,並且它已經被測試和驗證,可以安全使用。tapsets屬於Systemtap發行包的一部分。
Systemtap實現了一個指令碼轉換器/翻譯器,當用戶執行一個Systemtap指令碼時,Systemtap將首先對它進行分析和一些安全檢查,如果它引用了Systemtap預定義的指令碼庫提供的函式,Systemtap也將讀取指令碼庫得到相應的程式碼,對於一些核心變數或符號的引用,它必須根據核心除錯資訊來解析到相應的地址。然後,它被轉換成C程式碼,在這個轉換中,Systemtap將根據需要增加必要的鎖和安全檢查程式碼。探測點之間共享的變數將被轉換成恰當的靜態宣告並有鎖保護,每組本地變數被轉換到一個合成的呼叫幀結構中以避免消耗核心的棧空間。關聯到探測點的處理函式被封裝成一個介面函式,那呼叫恰當的kprobe介面函式來註冊該探測點。
產生的C程式碼包含了一些對執行時tapset的引用,執行時tapset庫提供了許多Systemtap介面函式,如通用的查詢表、受限記憶體管理、啟動、關閉、I/O操作以及其它一些函式。生成的C程式碼編譯連結之後生成一個可載入的核心模組。為了快速得到執行結果,Systemtap使用了relayfs,當載入生成的核心模組後,該模組的初始化函式初始化自身,然後呼叫kprobe介面函式註冊指令碼中定義的探測點。當核心執行到註冊的探測點時,相應的處理函式被呼叫,使用者在處理函式中的輸出語句將呼叫relayfs介面函式輸出結果資料,使用者在處理函式也可以呼叫一些核心的效能測量函式。當用戶主動停止或指令碼設定的條件滿足時,模組將呼叫退出函式解除安裝已經註冊的探測點並做一些清理處理就解除安裝模組自身。
Systemtap在執行時啟動了一個程序,它專門負責通過relayfs讀去模組的輸出資料並即時地輸出給使用者。(注:要probe的程式碼必須有除錯資訊,可以檢視探測點的上下文變數)
SystemTap實用例項
監控函式的效能
#show exec time of function(@2) and the tid who execute it
global t, call_time
probe process(@1).function(@2).call
{
t = gettimeofday_ns();
}
probe process(@1).function(@2).return
{
call_time <<< gettimeofday_ns()-t;
}
probe timer.s(1)
{
if(@count(call_time)>0)
{
printf("avg time %d total :%d/n", @avg(call_time), @count(call_time));
delete call_time;
}
else
{
println("no call");
}
}
probe begin
{
println("Ready");
}
該指令碼用於測試由引數傳遞的函式執行時所佔用的時間,即每秒列印一次平均執行時間,以及呼叫次數。通過這樣一個指令碼可以大概的確定我們要probe的函式的效能以及它相對於執行緒的使用時間。“<<<”表示陣列操作,把上面的function改為statement,可probe行範圍。@avg(call_time)計算陣列內所有元素和的平均值,count(call_time)計算陣列元素個數。
監控所有的系統呼叫
global syscalllist
probe begin {
printf("System Call Monitoring Started (10 seconds).../n")
}
probe syscall.*
{
syscalllist[pid(), execname()]++
}
probe timer.ms(10000) {
foreach ( [pid, procname] in syscalllist ) {
printf("%s[%d] = %d/n", procname, pid, syscalllist[pid, procname] )
}
exit()
}
統計10s內的所有系統呼叫及次數。
監控某個程式的所有系統呼叫
global syscalllist
probe begin {
printf("Syslog Monitoring Started (10 seconds).../n")
}
probe syscall.*
{
if (execname() == @1) {
syscalllist[name]++
}
}
probe timer.ms(10000) {
foreach ( name in syscalllist ) {
printf("%s = %d/n", name, syscalllist[name] )
}
printf("------------------------/n");
}
收集網路包
global recv, xmit
probe begin {
printf("Starting network capture (Ctl-C to end)/n")
}
probe netdev.receive {
recv[dev_name, pid(), execname()] <<< length
}
probe netdev.transmit {
xmit[dev_name, pid(), execname()] <<< length
}
probe end {
printf("/nEnd Capture/n/n")
printf("Iface Process........ PID.. RcvPktCnt XmtPktCnt/n")
foreach ([dev, pid, name] in recv) {
recvcount = @count(recv[dev, pid, name])
xmitcount = @count(xmit[dev, pid, name])
printf( "%5s %-15s %-5d %9d %9d/n", dev, name, pid, recvcount, xmitcount )
}
delete recv
delete xmit
}
count計算某個程序接收和傳送的報文數,通過avg可計算收到(傳送)的報文平均長度,sum可計算收到(傳送)的報文的總長度。該收集的資料與dstat有點相似,但它又能夠指出具體的程序使用情況,比dstat要詳細些。
監聽某個signal:sig_send.stp
probe signal.send{
if (sig_name == "SIGKILL")
printf("%s was send to %s(pid:%d) by %s uid:%d", sig_name, pid_name, sig_pid, execname(),uid())
}
$stap -L signal.send
$sudo stap sig_send.stp
監測某個函式的某個區間執行情況(行),對於行經常出現行地址錯誤,所以多調整一下用-L單獨測試
probe process(@1).statement(@2)
{
num++;
t = gettimeofday_ns();
}
probe process(@1).statement(@3)
{
call_time = gettimeofday_ns()-t;
printf("\n%s exec time:%d", @3,call_time);
}
sudo stap insline.stp "libexec/mysqld" "*@/home/xiangzhong.wxd/mysql-5.1.48/sql/sql_select.cc:11388" "*@/home/xiangzhong.wxd/mysql-5.1.48/sql/sql_select.cc:11391"
通過使用-L可以測試某條語句是否是正確的:
sudo stap -L 'process("/usr/local/mysql/libexec/mysqld").function("apply_event")'
sudo stap -L 'process("libexec/mysqld").statement("*@/home/xiangzhong.wxd/mysql-5.1.48/sql/sql_select.cc:11388")'
參考: