安裝與配置
在ubuntu下直接用apt-get install之后不能正常使用,提示缺少調試信息或者編譯探測代碼時有問題。
1. 采用官網上的解決方法
2. 可以自己重新編譯一次內核,然后再手工編譯一次systemtap。這樣就可以正常使用了。
Systemtap的編譯說明,除了下載地址並沒有說太多東西。選擇一個版本,自己選擇了最新的2.7.
下載后解壓,執行
./configure
一般來說會提示缺少組件。Systemtap最先應該是redhat開發的,所以需要的包名稱ubuntu不能直接用來apt-get
列出幾個自己碰到的依賴問題:
configure: error: missing gnu /usr/bin/msgfmt
apt-get install gettext
configure: error: missing elfutils development headers/libraries (install elfutils-devel, libebl-dev, libdw-dev and/or libebl-devel)
可以通過apt-get install libdw-dev解決
configure: error: in `/root/systemtap-2.7': configure: error: C++ preprocessor "/lib/cpp" fails sanity check
安裝apt-get install g++
使用用例
基本使用
詳見systemtap 官方tutorial。這里做個筆記。
hello world
把systemtap腳本轉換編譯為內核模塊然后執行預定義的動作,定義的動作由一系列的事件觸發。用戶可以指定在哪些事件上觸發哪些指定的動作。下面是一個systemtap的helloworld,在模塊裝載即在腳本運行前執行一次
root@userver:~# stap hello-world.stp hello world root@userver:~# cat hello-world.stp probe begin { print ("hello world\n") exit () }
如果打開-v選項的話,可以看到執行的詳細步驟:
root@userver:~# stap -v hello-world.stp Pass 1: parsed user script and 106 library script(s) using 66544virt/37432res/4324shr/33908data kb, in 120usr/10sys/127real ms. Pass 2: analyzed script: 1 probe(s), 1 function(s), 0 embed(s), 0 global(s) using 67204virt/38136res/4512shr/34568data kb, in 0usr/0sys/4real ms. Pass 3: translated to C into "/tmp/stapyZxhXI/stap_847497c1de7927412685a2282f37c57d_881_src.c" using 67204virt/39028res/5232shr/34568data kb, in 0usr/0sys/0real ms. Pass 4: compiled C into "stap_847497c1de7927412685a2282f37c57d_881.ko" in 1000usr/590sys/1582real ms. Pass 5: starting run. helloworld Pass 5: run completed in 10usr/20sys/472real ms.
如果多次運行同一個腳本的話快很多,因為systemtap直接使用了已經編譯好的緩存模塊文件。
還可以定時運行一定時間:
root@userver:~# stap strace-open.stp cat(24307) open ("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) cat(24307) open ("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) cat(24307) open ("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) cat(24307) open ("iw.c", O_RDONLY) root@userver:~# cat strace-open.stp probe syscall.open { printf("%s(%d) open (%s)\n", execname(), pid(), argstr); } probe timer.ms(4000) # after 4 seconds { exit() }
在systemtap運行期間執行了一個cat命令得到的結果,腳本記錄了執行系統調用open的進程信息。
如何跟蹤
跟蹤點選擇
begin |
The startup of the systemtap session. |
end |
The end of the systemtap session. |
kernel.function("sys_open") |
The entry to the function named sys_open in the kernel. |
syscall.close.return |
The return from the close system call. |
module("ext3").statement(0xdeadbeef) |
The addressed instruction in the ext3 filesystem driver. |
timer.ms(200) |
A timer that fires every 200 milliseconds. |
timer.profile |
A timer that fires periodically on every CPU. |
perf.hw.cache_misses |
A particular number of CPU cache misses have occurred. |
procfs("status").read |
A process trying to read a synthetic file. |
process("a.out").statement("*@main.c:200") |
Line 200 of the a.out program. |
全局:probe begin {}, probe end {}用於整個跟蹤過程的開頭和結尾。
函數: kernel.function("sys_open"){}用於在某個指定的內核函數中執行定義的動作,sys_open可以換成其他的函數如ext4_release_file(在文件close時會執行)
系統調用:syscall.close在執行close調用時執行,其他系統調用也是類似。因為系統調用的函數是通過宏定義實現的
修飾:
內聯:kernel.function("xx").inline {} 指定函數被內聯時進入
調用:kernel.function("xx").call {} 指定函數被調用是進入(不含內聯)
返回:kernel.function("").return{}可以在該函數返回時執行。
格式輸出
printf %s表示字符串,%d表示數值類型
tid() |
The id of the current thread. |
pid() |
The process (task group) id of the current thread. |
uid() |
The id of the current user. |
execname() |
The name of the current process. |
cpu() |
The current cpu number. |
gettimeofday_s() |
Number of seconds since epoch. |
get_cycles() |
Snapshot of hardware cycle counter. |
pp() |
A string describing the probe point being currently handled. |
ppfunc() |
If known, the the function name in which this probe was placed. |
$$vars |
If available, a pretty-printed listing of all local variables in scope. |
print_backtrace() |
If possible, print a kernel backtrace. |
print_ubacktrace() |
If possible, print a user-space backtrace. |
1. print_backtrace比較實用可以打印內核的調用棧
2. gettimeofday_s用於獲得以秒為單位的時間,gettimeofday_ms則是以毫秒為單位的時間,gettimeofday_us
3. thread_indent用於進程/線程輸出時的縮進相當於一個thread_local變量,參數表示作用在該變量上的一個增量,進入一個函數時參數為正值,退出時為負值,就可以產生函數調用的縮進效果,下面是一個類似tutorial上的示例:
probe kernel.function("*@fs/open.c").call { printf("%s -> %s(%s)\n", thread_indent(4), ppfunc(), $$parms); } probe kernel.function("*@fs/open.c").return { printf("%s <- %s\n", thread_indent(-4), ppfunc()); }
部分輸出:
0 prltoolsd(1230): -> SyS_open(filename=0x40fa78 flags=0x0 mode=0x6118a0) 7 prltoolsd(1230): -> do_sys_open(dfd=0xffffffffffffff9c filename=0x40fa78 flags=0x8000 mode=0x18a0) 20 prltoolsd(1230): -> finish_open(file=0xffff88002ff1a000 dentry=0xffff88009a978840 open=0x0 opened=0xffff88002f89fdec) 24 prltoolsd(1230): -> do_dentry_open(f=0xffff88002ff1a000 open=0x0 cred=0xffff880140efe300) 29 prltoolsd(1230): -> generic_file_open(inode=0xffff88002f71a820 filp=0xffff88002ff1a000) 31 prltoolsd(1230): <- generic_file_open 33 prltoolsd(1230): <- do_dentry_open 34 prltoolsd(1230): <- finish_open 36 prltoolsd(1230): -> open_check_o_direct(f=0xffff88002ff1a000) 38 prltoolsd(1230): <- open_check_o_direct 41 prltoolsd(1230): <- do_sys_open 42 prltoolsd(1230): <- SyS_open 0 prltoolsd(1230): -> SyS_close(fd=0x5) 6 prltoolsd(1230): -> filp_close(filp=0xffff88002ff1a000 id=0xffff880148cb5c80) 15 prltoolsd(1230): <- filp_close 17 prltoolsd(1230): <- SyS_close
分析執行
變量默認的都是局部變量,即每個處理函數內的變量是不共享的。使用全局變量的話,要在開始使用global關鍵字進行定義。變量是弱類型的,可以相互轉換但是要手工顯式進行。字符串使用.連接和php與perl一樣。流程控制語句和C語言基本一致。下面是tutorial中的一個例子:
global count_jiffies, count_ms; probe timer.jiffies(100) { count_jiffies++; } probe timer.ms(100) { count_ms++; } probe timer.ms(10000) { hz = (1000 * count_jiffies) / count_ms; printf("jiffies:ms ratio: %d:%d = %d\n", count_jiffies, count_ms, hz); }
目標變量
這些變量在跟蹤點處理函數所在的上下文種獲取,可以直接使用被跟蹤函數的參數變量等。下面是一個示例:
probe kernel.function("filp_close") { printf("%s %d: %s(%s:%d)\n", execname(), pid(), ppfunc(), kernel_string($filp->f_path->dentry->d_iname), $filp->f_path->dentry->d_inode->i_ino); }
輸出如下:
bash 1724: filp_close(:24831) bash 1724: filp_close(:24831) bash 31781: filp_close(:24831) bash 31781: filp_close(:24831) a.out 31781: filp_close(1:4) a.out 31781: filp_close(ld.so.cache:788003) a.out 31781: filp_close(libc-2.19.so:3936539) a.out 31781: filp_close(data.out:1460185) a.out 31781: filp_close(1:4) a.out 31781: filp_close(1:4) a.out 31781: filp_close(1:4)
函數
函數定義function name(arg1, arg2) { return somthing},跟javascript里差不多。
數組
systemtap里的數組實際上就是一個hashmap,還支持多維hash(hashmap[key1, key2...] = value),但是需要預先定義容量,當已有的元素超過容量時會報錯:
global hashmap[3] global multimap[3] global countmap[5] probe begin { hashmap[1] = "a"; hashmap[3] = "c"; hashmap[100] = "last"; # # ERROR: Array overflow, check size limit (3) near identifier 'hashmap' at array-demo.stp:8:2 # hashmap[222] = "excced." # multimap[1,"init"] = "important" multimap[0, "swap"] = "more import" for (i = 0; i<5; i++) { countmap[i] = i * 10; } } probe timer.ms(1000) { exit(); } probe end { printf("-----------------------------\n") printf("exist: %s, %s, %s\n", hashmap[1], hashmap[3], hashmap[100]); printf("!exist: %s\n", hashmap[121]); printf("-----------------------------\n") printf("exist: %s\n", multimap[1, "init"]); printf("!exist: %s\n", multimap[1, "haha"]); printf("--------------sorted by key inc[default]-------------\n") foreach([a] in countmap) { printf("countmap[%d] = %d\n", a, countmap[a]); } printf("--------------sorted by key desc-------------\n") foreach([a-] in countmap) { printf("countmap[%d] = %d\n", a, countmap[a]); } printf("--------------sorted by value desc-------------\n") foreach([a] in countmap-) { printf("countmap[%d] = %d\n", a, countmap[a]); } }
foreach 語法默認對hashmap中的key進行一個升序的迭代,如果要改變方向可以在key后加個減號,如果需要按值升降序迭代則在hashmap數組名稱后加符號。單個key時foreach中的[]可以省略。
統計聚合
聚合變量操作可以使用<<<對變量進行增量,按照tutorial的解釋這個變量是分布在各個CPU特有的關聯空間所以可以減少競爭,然后使用@avg(增量值的平均),@sum(增量值的累加),@count(增量執行次數)函數進行聚合,不能直接訪問。
global hitcount[10000]; probe kernel.function("__schedule") { hitcount[execname()] <<< 1; } probe timer.ms(10000) { exit(); } probe end { foreach (prog in hitcount) { printf("%15s : %-6d\n", prog, @count(hitcount[prog])); } }
運行結果:
root@userver:~/stp# stap schedule-stat.stp swapper/0 : 2 rs:main Q:Reg : 40 rcu_sched : 7 kworker/0:1H : 6 kworker/0:2 : 10 watchdog/0 : 2 rcuos/1 : 3 kworker/1:1 : 10 systemd-udevd : 3 swapper/1 : 579 rcuos/0 : 2 kworker/u64:0 : 12 jbd2/sda1-8 : 7 migration/1 : 1 khugepaged : 1 prltoolsd : 40 watchdog/1 : 2 in:imklog : 446 stapio : 51 ksoftirqd/1 : 27
每次執行都是+1的話不能體現出這些聚集函數的作用,對於每次增量是不同的需求,聚合函數就非常的有用。另外一個例子用來統計調用vfs_read的數據量:
global data_count[10000] probe begin { print("start profiling."); } probe kernel.function("vfs_read") { data_count[execname()] <<< $count; } probe timer.ms(1000) { print("."); } probe timer.ms(20000) { # 20 seconds exit(); } probe end { print("\n"); foreach (prog in data_count) { printf("%15s : avg:%-8d cnt:%-12d sum:%-12d\n", prog, @avg(data_count[prog]), @count(data_count[prog]), @sum(data_count[prog])); } }
輸出:
root@userver:~/stp# stap vfs-read-stat.stp start profiling..................... top : avg:1050 cnt:1034 sum:1086553 in:imklog : avg:8095 cnt:3398 sum:27510134 sshd : avg:16384 cnt:20 sum:327680 stapio : avg:129057 cnt:122 sum:15745032 acpid : avg:24 cnt:46 sum:1104 bash : avg:88 cnt:9 sum:793 systemd-udevd : avg:128 cnt:2 sum:256
Tapset
tapset是一些systemtap腳本文件,存在於/usr/share/systemtap/tapset。
符號選擇
當用戶執行腳本時如果發現符號沒定義那么會在tapset內進行搜索,其中還有些文件夾,其名稱代表了kernel體系架構名稱或者kernel版本名稱。搜索匹配是具體到泛化的過程,跟路由IP選擇一樣,如果有精確的選擇則優先選擇可以精確匹配的,不行則在采用一般腳本中的定義,如都沒找到則報錯。不過不知為什么按着tutorial上的做依然提示找不到。。。
跟蹤點別名
global groups probe syscallgroup.io = syscall.open, syscall.close, syscall.read, syscall.write { groupname = "io"; } probe syscallgroup.process = syscall.fork, syscall.execve { groupname = "process" } probe syscallgroup.* { groups[pid(), execname() . "/" . groupname]++; } probe end { foreach ([id, eg+] in groups) { printf("%5d %-20s %d\n", id, eg, groups[id, eg]) } }
嵌入C代碼
用戶腳本中嵌入C語言的腳本,在運行時需要使用-g選項。
- Do not dereference pointers that are not known or testable valid. (不要隨意對指針解引用)
- Do not call any kernel routine that may cause a sleep or fault. (不要調用那些會引起阻塞或者睡眠的函數)
- Consider possible undesirable recursion, where your embedded C function calls a routine that may be the subject of a probe. If that probe handler calls your embedded C function, you may suffer infinite regress. Similar problems may arise with respect to non-reentrant locks. (不要調用會引起自身腳本無限循環的調用)
- If locking of a data structure is necessary, use a
trylock
type call to attempt to take the lock. If that fails, give up, do not block.(獲取鎖時先用trylock類型的調用嘗試)
頭文件可以使用
%{
%}方式在腳本開頭引入。
function get_msg:string (id:long) %{ snprintf(STAP_RETVALUE, MAXSTRINGLEN, "helloworld %ld\n(%d)\n", (long)STAP_ARG_id, MAXSTRINGLEN); %} probe begin { print(get_msg(123)); }
輸出:
# stap -g embedded-c.stp helloworld 123 (512)