Systemtapは"DTrace"と"AWK”に触発された、GNU/Linuxカーネル内部を動的にトレースするためのツールです。
オフィシャルサイトで公開されている"Systemtap tutorial"は2007年の日付で、若干古いですが、PDF版の本文12ページの中で基本的な機能についてよくまとめられていると思います。
Reference Manualの方は2009年11月13日の日付で更新されているので、全ての機能について確認する事ができます。
使うだけなら基本的なサンプルだけをコピーして切り貼りしても問題ないと思いますが、
せっかく読んだのでリファレンスやマニュアルページの情報も加えてサンプルだけだと読み解くのが難しそうな点を中心にメモを残しておきます。
全体の構成
チュートリアルは以下のセクションに分けられています。
この流れに沿ってまとめていきます。
- Tracing
- Analysis
- Tapsets
- Further information
Tracing
Systemtapはカーネルが処理を行なう途中に"probe"を仕掛けることで、割り込む事ができます。
あたかも電子回路上にオシロスコープのプローブを当てるようなイメージと一致するのだと思います。
使うプローブの種類を変えれば、違うタイミングや内容を知る事ができるといったところでしょうか。
そういった様々なプローブはSystemtapがbuilt-inで提供していますが、その他に、
"tapset"と呼ばれるライブラリスクリプトの中でユーザーが任意のprobeを組み合せて新たなprobeを定義する事が可能になっています。
後でも出てきますが、このライブラリスクリプト群は通常"/usr/share/systemtap/tapset/"以下に配置されています。
例えば、あらかじめ準備されている"probe begin"や"probe end"を使えばstapが処理を始めたり、終えたりするタイミングで変数の初期化やレポートの出力といった処理を行なわせる事ができます。
ただエラーによって終了する場合は"probe end"は呼ばれないため、"probe error"を使う必要があります。
どのようなプローブ(probe points)が存在するのかは、stapprobes manual pageを参照してください。
ユーザーが記述しなければいけないプログラム本体の流れは、1.データの取得、2.若干の加工、3.出力、になると思います。
"データの取得"について、tid()、execname()等いくつかはチュートリアルに書かれていますが、全体はstapfuncs manual pageにまとめられています。
"データの加工"についての具体的な方法は、あまりチュートリアルには書かれていません。
C言語やAWKで一般的な範囲の四則演算やビット演算、条件式は準備されていて、詳細はリファレンスマニュアルにまとめられています。
"出力"については、とりあえずC言語の"printf"関数と似た構文を提供しています。
Ruby等では"%b"というと数値を2進数表記で表示してくれますが、systemtapではバイト列をそのまま表示してくれます。
"%1b"で1バイト分、"%2b"で2バイト分のバイナリを出力する事ができます。
使えるとは思えないけれど、”あ”をUTF8で表示しようと思ったら、次のようなコードになります。
probe begin {
i = 8552931
printf("%4b\n", i)
exit()
}
Analysis
おそらく斜め読みをして、if,while,forなどのループ、式はC言語と似ているなぁというぐらいが分かると思います。
systemtapがawkを強く意識しているだけに、Perl同様に文字列の連結は"."を使って行なう点は少し注意が必要そうです。
あとは使う変数は基本的に全てスコープローカルになるので、probeやfunctionを跨って参照したい変数は、"global"によって宣言する必要があります。
さらに"global"宣言された変数は自動的にread, writeロックがかかるため、不用意に使う事は避けたいところです。
Analysis::Target variables
probeの中でだけ、特殊な変数"Target variables"が参照できます。
これはprobeを仕掛けてた個所のコンテキストにあるカーネル内部の変数にアクセスができるという機能です。
リファレンスマニュアルを見る限りでは、そのコンテキストにあるポインタ(*file)に"$file"のようにアクセスができるようになっています。ローカル変数へのアクセスは難しそうですね。
ここでは"target variables"ではない、"$"を使ってコマンドラインの引数にアクセスする方法が書かれています。
数値の場合は"$1"ですが、文字列の場合は"@1"のように"@"を使って書く必要があります。
// print_name_age.stp
probe begin {
printf("name: %s, age: %d\n", @1, $2)
exit()
}
$ sudo stap print_name_age.stp hoge 28
name: hoge, age: 28
"$<pointer>"と"$<数値>"は似ていても、"target variables"は"$<pointer>"だけなので、注意が必要です。
Analysis::Functions
functionについて、ここまで明記されていませんでしたが、probeの中に書く処理をまとめて記述するために使います。
ただし、前記の"Target variables"へのアクセスはできないため、functionの引数に指定するなどして渡す必要があります。functionの引数は値渡し(pass by value)なので必要に応じて結果はfunctionの戻り値として受け取ります。
Analysis::Arrays
次がarrayについてで、連想配列が使えますが、必ずglobalで宣言する必要があり、サイズはコンパイル時に決定されている必要があります。
宣言時に配列のサイズを省略すると、MAXMAPENTRIESのデフォルト値、2048で配列が確保されます。
arrayについての仕様でおもしろそうなところは、foo[4,"hello"]のようにindexに数値や文字列の組み合せが使えるというところでしょうか。
使ってしまえば、特にどうという事はないんですけどね、実用的な仕様だと思います。
わかりずらいのは"foreach ([a,b] in foo)"のところでしょうか。
説明は"simple loop in arbitrary sequence"とありますが、事前にfooがfoo[4,"helloworld"] = bar
のように宣言されている必要があります。
例1:ちゃんと動くシンプルな例
#!/usr/bin/stap
global ary
function print_ary() {
foreach (k in ary) {
printf("ary[%d] => %d\n",k, ary[k])
}
}
probe begin {
ary[0] = 3
ary[1] = 5
ary[2] = 1
print_ary()
exit()
}
例2:チュートリアルと似ていて、なおかつ"l+"と書く事でindexの第2要素を昇順(小さい→大きい)にソートしている。
#!/usr/bin/stap
global ary
function print_ary() {
foreach ([k,l+] in ary) {
printf("a[%d,%s] => %d\n", k, l, ary[k,l])
}
}
probe begin {
ary[0,"a"] = 3
ary[1,"d"] = 5
ary[2,"b"] = 1
print_ary()
exit()
}
上記のforeach()にある"k","l","ary"の各変数に"+"や"-"を追加する事で昇順や降順にソートさせる事ができます。具体的に動かして観察するのが良いでしょう。小さい工夫ですが、収集するデータによっては便利だと思います。
Analysis::Aggregates
データ収集用の特殊なデータ型です。
特殊にしたおまけにデータの解析、収集に便利そうな機能が付いてきます。
簡単にまとめると次のようになります。
- データを格納する変数はarray型を使うため、おのずとglobalで宣言する必要がある
- データを格納する時に"<<<"オペレータを使う
- データへのアクセスは"@avg()"など'@'で始まる特殊な関数を使う
リファレンスをみても使える特殊関数は"@count", "@sum", "@min", "@max", "@avg", "@hist_linear", "@hist_log"だけのようです。
Analysis::Safety
いろいろな上限値などに触れられていますが、システムのデフォルト値はstap manual pageの"Safety and Security"の項目に書かれています。
Tapsets
"tap" + "sets"ぐらいの意味だと思いますが、/usr/share/systemtap/tapsetにインストールされているライブラリスクリプトについての解説です。
ユーザーが作成したスクリプトの中に未定義のprobeやfunctionがあると、/usr/share/systemtap/tapset/*.stpフィアルを順番に調べていき、対応する名前をみつけると自動的に読み込みます。
ただし、該当するprobeやfunctionがみつかったファイル全体の内容を読み込みます。
また複数のprobeを組み合せて一つのprobe名にする、aliasの機能があります。
応用すると、前処理をさせるといった目的に使う事もできます。
あとは*.stpファイルの中にC言語のコードを埋め込めるという"Embedded C"の機能でしょうか。
具体的な例はtapsetディレクトリを除く方が確実そうです。他に”embedded C"を使う際の注意点が列挙されています。
便利そうですが、あまりにも怖いのでVMWare上でテスト環境を作って試そうと思います。
Tapsets::Naming conventions
前述のとおりsystemtapはtapsetにあるファイルを全て確認して、必要に応じてファイル全体を取り込みます。
この仕様のため大勢がバラバラに開発したライブラリスクリプトで共通のglobal宣言した変数を使うなどした場合には、名前のバッティングが発生する可能性があります。
そこでガイドラインが掲載されていますが、常識的な個所を除くとだいたい次のような感じです。
- 作成したtapset固有の名前を付ける (以下、TAPSET、とします)
- function名など外部からアクセス可能なところは、"TAPSET_"で始まる名前をつける
- global変数など内部的にアクセスする変数などには、"__TAPSET_"で始まる名前をつける
さいごに
結局はリファレンスに当たらないといけない事ばかりでしたが、systemtapは1.0もでてようやく安定するのでしょうか。
バージョン間の違いには詳しくありませんが、systemtap-1.0/NEWSファイルをみるとUbuntu 8.04 LTSのパッケージとして提供されている0.6.0から多くの機能が加えられ、変更されていることがわかります。
ずっと昔に大学の演習でSunOS 4のパフォーマンスアナライザーを使った時は、C言語用のライブラリだけが提供されていてsystemtapが裏でやっているような事を手作業でやっていたように記憶しています。
教育目的としても手軽に使えそうですね。