SystemTap

提供: Wikinote
移動: 案内検索

ついに伝家の宝刀 (でも諸刃の剣(いろんな意味で)) を抜く時が来たようだ…。

参考 URL

  • SystemTap とりあえず公式ページの Documentation と Wiki を見れば事足りる。

覚え書き

  • 構造体のメンバへのアクセスは、ポインタであれ実体であれ -> を使う
  • atomic_t へのアクセスは、->counter を追加すれば可能

基本形

文法は、awk に似ている。begin/end はオプション。 詳細は man stap を参照のこと。

#!/usr/bin/stap -v

function my_function:string(arg1:long, arg2) {
    return value
}

probe begin {
    ...
}

probe kernel.function("sys_*") {
    printf("%s[%d] called %s()\n", execname(), pid(), probefunc())
}

probe end {
    ...
}

引き数を取る

以下のように、$# で引き数の数、$N で数値、@N で文字列を取ることができる。

probe begin {
    printf("$# = %d, $1 = %d, @2 = %s\n", $#, $1, @2)
    exit()
}

実行すると、こんな具合。

# stap args.stp 10 hoge
$# = 2, $1 = 10, @2 = hoge

プローブポイント

詳細は、man stapprobes を参照のこと。 SystemTap のバージョンにより使えるものがかなり違うので注意すること。

begin/end/error
名前の通り。
timer.[ jiffies | s | ms | us | ns | hz ](N)
インターバル指定。例えば、timer.s(10) で 10 秒ごとに動作する。
syscall.*[.return]
システムコール関数。特別に以下の変数を利用可能。
  • argstr — 引き数の値のリスト
  • name — 関数名
  • retstr — 返り値 (.return のみ)
kernel.function("PATTERN")[.return]
任意のカーネル関数。ワイルドカード (*, ?, [ ]) による指定も可能。以下ように変数へのアクセスが可能。
  • $var — ソース中の変数にアクセス
  • $var->field
  • $return — 返り値 (.return のみ)
  • $var[N]
  • $$vars — sprintf("parm1=%x ... var1=%x ...", parm1, ..., var1, ...) と等価 (変数の文字列展開)
  • $$locals — ローカル変数を文字列展開
  • $$perms — 引き数を文字列展開
  • $$return — 返り値を文字列展開 (.return のみ)
kernel.statement("PATTERN")
カーネルの任意の位置。以下のような指定が可能。
  • kernel.function("*@kernel/timer.c:123")
  • kernel.statement("sys_adjtimex@kernel/time.c+3")
module("MPATTERN").function("PATTERN")
module("MPATTERN").statement("PATTERN")
モジュール内の関数などを指定する。例えば、module("ext3").function("ext3_invalidatepage") など。
kernel.trace("TRACEPOINT")
カーネルの tracepoint にフックする。どこでフックできるかは、以下のコマンドで確認できる。
# stap -L 'kernel.trace("*")' | head -n 5
kernel.trace("socket_sendmsg") $sock:struct socket* $msg:struct msghdr* $size:size_t $ret:int
kernel.trace("net_dev_xmit") $skb:struct sk_buff* $rc:int
kernel.trace("block_unplug_io") $q:struct request_queue*
kernel.trace("mm_anon_cow") $mm:struct mm_struct* $address:long unsigned int $page:struct page*
kernel.trace("sched_process_wait") $pid:pid_t

組み込み関数

使えそうなものを挙げておく。詳細は man stapfuncs を参照のこと。

  • cpu() — 現在の CPU 番号
  • execname() — 現在のプロセス名
  • exit() — SystemTap セッションを終了
  • pid() — 現在のプロセスの PID
  • gettimeofday_[ s | ms | us | ns ]() — UNIX epoch からの経過時間
  • ctime() — ctime(gettimeofday_s()) で現在の時刻を文字列に変換
  • pp() — プローブポイントの情報
  • probefunc() — プローブした関数名
  • probemod() — プローブしたモジュール名
  • print_backtrace() — コールスタックを表示する
  • print_regs() — レジスタ情報を表示する

出力例

print_regs() と print_backtrace() のサンプル

ソースコード <toggledisplay>

#!/usr/bin/stap -v
global count = 0
probe kernel.function("de_put") {
    printf("%s[%d] called %s\n", execname(), pid(), probefunc())
    printf("print_regs():\n")
    print_regs()
    printf("print_backtrace():\n")
    print_backtrace()
    count++

    if (count == 10) {
        exit()
    }
}

</toggledisplay> この例では 10 回分出力されるが、1 つ抜き出すとこんな感じ。

gzip[7858] called de_put
print_regs():
EIP: c049ea7e
ESP: c049ed89
EAX: f7ffbd40 EBX: f7ffbd40 ECX: 00000000 EDX: 00000000
ESI: f4376b78 EDI: f3dea380 EBP: f4376b78 DS: 007b ES: 007b
CR0: 8005003b CR2: 08061000 CR3: 0b5104c0 CR4: 000006f0
print_backtrace():
 0xc049ea7e : de_put+0x1/0x51 [kernel]
 0xc049ed89 : proc_delete_inode+0x59/0x64 [kernel]
 0xc0487055 : generic_delete_inode+0xa5/0x10f [kernel]
 0xc0486af9 : iput+0x64/0x66 [kernel]
 0xc0485ae9 : dput+0xd5/0xed [kernel]
 0xc047218a : __fput+0x13f/0x167 [kernel]
 0xc046fad5 : filp_close+0x4e/0x54 [kernel] (inexact)
 0xc0470d01 : sys_close+0x71/0xa8 [kernel] (inexact)
 0xc0404f17 : sys_sigreturn+0x1f8/0xf2d [kernel] (inexact)

Tips

カーネルを加速させる

RHEL4.7, RHEL5.1 以降で導入された tick_divider を利用して、カーネルを加速させる方法。 実機だと、2 〜 100 倍くらいまでは問題なさそう。 ただし、定期的にログを書き出すようなサービスがあると I/O 負荷が高まるので注意する必要がある。 また、ntpd も止めておくべきだろう。

プローブポイントを確認する

-l オプションでプローブポイントの一覧を出力できる。

[root@lab ~]# stap -v -l 'kernel.function("add_to_*")'
Pass 1: parsed user script and 45 library script(s) in 300usr/10sys/318real ms.
kernel.function("add_to_page_cache@mm/filemap.c:439")
kernel.function("add_to_page_cache_lru@mm/filemap.c:462")
kernel.function("add_to_swap@mm/swap_state.c:147")
kernel.function("add_to_swap_cache@mm/swap_state.c:99")
Pass 2: analyzed script: 4 probe(s), 0 function(s), 0 embed(s), 0 global(s) in 290usr/300sys/591real ms.

RHEL 4 の stap コマンドには -l オプションがない。 この場合は、-p2 オプションでプローブポイント確認後に動作を止めればよい。

[root@centos47 stap]# stap -e 'probe kernel.function("add_to_*"){}' -p2
# probes
kernel.function("add_to_page_cache@mm/filemap.c:343") /* pc=0x59caf */ /* <- kernel.function("add_to_*") */
kernel.function("add_to_page_cache_lru@mm/filemap.c:367") /* pc=0x59d61 */ /* <- kernel.function("add_to_*") */
kernel.function("add_to_swap@mm/swap_state.c:142") /* pc=0x712b2 */ /* <- kernel.function("add_to_*") */
kernel.function("add_to_swap_cache@mm/swap_state.c:95") /* pc=0x71635 */ /* <- kernel.function("add_to_*") */

参考:http://d.hatena.ne.jp/mhiramat/20081004

Vim の SystemTap 用シンタックスを追加する

アーカイブ などからソースを落としてきて、 以下のページに書かれている手順を実行する。

カーネル変数にアクセスする

stap コマンドを -g オプション付きで実行すると、Guru モードというあやしげなモードになり、C 言語を埋め込めるようになる。 ネットで調べてもあまりドキュメントがなく使い方がよくわからなかったのだが、/usr/share/systemtap/tapset に C 言語を使ったコードが多数あるので、これらを参考にしてみた。

例えば、jiffies 値も取得できるようになる。

#!/usr/bin/stap -vg ★g が必要
%{
#include <linux/jiffies.h>
%}
function get_jiffies() %{
    THIS->__retvalue = jiffies;
%}
probe timer.s(1) {
    printf("jiffies = %d\n", get_jiffies())
}

実行すると・・・

[root@lab stap]# ./jiffies.stp 
Pass 1: parsed user script and 52 library script(s) in 340usr/10sys/352real ms.
Pass 2: analyzed script: 1 probe(s), 1 function(s), 1 embed(s), 0 global(s) in 10usr/0sys/6real ms.
Pass 3: using cached /root/.systemtap/cache/51/stap_51d9dc13ad57fe556d3fd725f4c74545_656.c
Pass 4: using cached /root/.systemtap/cache/51/stap_51d9dc13ad57fe556d3fd725f4c74545_656.ko
Pass 5: starting run.
jiffies = 1561628
jiffies = 1562628
jiffies = 1563628
jiffies = 1564628
jiffies = 1565627
Pass 5: run completed in 0usr/20sys/5730real ms.

CentOS 5 なので、HZ=1000 で間違いない。すばらしい。 これでまた応用範囲が広がった・・・ぐへへ。

※注 Guru モードは簡単に Oops やらパニックやら発生するので注意されたし。

カーネルモジュールを作成する

作成したスクリプトを stap コマンドで実行すると、毎回コンパイルされるので実行開始に時間がかかる。 いつも同じスクリプトを実行するのであれば、コンパイルされた状態のカーネルモジュールを作成しておくことで、 コンパイルにかかる時間を省略できる。

stap コマンドを -m オプション付きでモジュール名を指定して実行すると、カレントディレクトリにカーネルモジュールが作成される。 また、-p4 オプションを付けておけば、実行フェーズまでいかないのでコンパイルのみを実行できる。

[root@lab stap]# stap -v -p4 -m hoge hoge.stp
Pass 1: parsed user script and 52 library script(s) in 350usr/10sys/374real ms.
 Pass 2: analyzed script: 1 probe(s), 1 function(s), 0 embed(s), 0 global(s) in 0usr/0sys/6real ms.
Pass 3: translated to C into "/tmp/stapvEuRJi/hoge.c" in 0usr/0sys/0real ms.
hoge.ko
Pass 4: compiled C into "hoge.ko" in 3450usr/460sys/4174real ms.
[root@lab stap]# ls hoge*
hoge.ko  hoge.stp

作成されたカーネルモジュールを使って実行する場合は、staprun コマンドを使う。

[root@lab stap]# staprun -v hoge.ko 
stapio:cleanup_and_exit:371 detach=0
hoge
stapio:cleanup_and_exit:388 closing control channel

注意事項

  • probe kernel.function("*") (すべてのカーネル関数をフック) をやってはいけない。この中で何か出力しようものなら、あっという間にシステムがストールしてしまう (実証済み…)。何も出力しなければ or screen 上で実行しなければ問題ないのかもしれないが、もう怖くてできない。。