3.2. SystemTap スクリプト

SystemTap スクリプトはそのほとんどにおいて、各 SystemTap セッションのベースになっています。SystemTap スクリプトが SystemTap に対してどのタイプの情報を収集するか、収集後に何をするかを指示します。
3章SystemTap の作動方法 の説明にあるように、SystemTap スクリプトは イベントハンドラー という 2 つのコンポーネントで構成されています。SystemTap セッションが開始されたら、SystemTap はオペレーティングシステムで指定されたイベントを監視し、イベントが発生したらハンドラーを実行します。

注記

イベントとそれに対応するハンドラーは、合わせて プローブと呼ばれます。SystemTap スクリプトには複数のプローブを備えることができます。プローブのハンドラーは一般的に プローブボディと呼ばれます。
アプリケーションの開発という面では、イベントとハンドラーの使用は診断プリントステートメントをコマンドのプログラムシーケンスに挿入するというコードのインストルメント化に似ています。診断プリントステートメントを使用すると、プログラムの実行後に発行されたコマンドの履歴をみることができます。
SystemTap スクリプトでは、コードを再コンパイルすることなくインストルメンテーションコードの挿入が可能で、ハンドラーに関する柔軟性が広がります。イベントは、ハンドラー実行の引き金となります。ハンドラーは指定されたデータを記録して、特定の方法でプリントするよう指定できます。
書式

SystemTap スクリプトは .stp ファイル拡張子を使用し、以下の書式のプローブが含まれます。

probe event {statements}
SystemTap は 1 つのプローブにつき複数のイベントをサポートしており、複数イベントはコンマ (,) で区切ります。1 つのプローブで複数のイベントが指定された場合、SystemTap は指定されたイベントが発生するとそのハンドラーを実行します。
プローブにはそれぞれ、対応する ステートメントブロックがあります。このステートメントブロックは中括弧 ({ }) で囲まれており、イベントごとに実行されるステートメントが含まれています。SystemTap はこれらのステートメントを順番に実行し、通常は複数のステートメントを分ける特別なセパレーターやターミネーターは必要ありません。

注記

SystemTap スクリプト内のステートメントブロックは、C プログラミング言語と同じ構文とセマンティクスを使用します。ステートメントブロックは、別のステートメントブロック内の入れ子状態にすることができます。
Systemtap では、多くのプローブが使用するコードを外に括り出して関数を作成することができます。つまり、複数のプローブで同じステートメントを何度も繰り返し書くのではなく、以下のように function 内に指示を配置することができます。
function function_name(arguments){statements}
probe event {function_name(arguments)}
function_name 内の statements は、event のプローブの実行時に実行されます。arguments は、function に渡されるオプションの値です。

重要

「SystemTap スクリプト」 では、SystemTap スクリプトの基本を説明しています。SystemTap スクリプトについてさらに理解を深めるには、4章便利な SystemTap スクリプト を参照してください。この章の各セクションでは、スクリプト、イベント、ハンドラー、および予測される出力について詳細に説明されています。

3.2.1. イベント

SystemTap のイベントは大まかに、同期非同期 に分けられます。
同期イベント

同期イベントは、プロセスがカーネルコード内の特定の場所で指示を実行する際に発生します。これは他のイベントの参照ポイントとなり、ここからさらにコンテキストデータが入手可能になります。

同期イベントの例には以下のようなものがあります。
syscall.system_call
システムコール system_call へのエントリー。システムコールの終了を希望する場合は、.return をイベントに追加すると、システムコールの終了を監視するようになります。たとえば、close システムコールのエントリーと終了を指定するには、それぞれ syscall.closesyscall.close.return を使用します。
vfs.file_operation
仮想ファイルシステム (VFS) の file_operation イベントへのエントリー。syscall イベントと同様に、イベントに .return を追加すると、file_operation 動作の終了を監視します。
kernel.function("function")
function カーネル関数へのエントリー。たとえば kernel.function("sys_open") は、sys_open カーネル関数がシステム内のスレッドに呼び出される際に発生する イベント を指します。sys_open カーネル関数の return を指定するには、kernel.function("sys_open").return のように return 文字列をイベントステートメントに追加します。
プローブイベントを定義する際には、アスタリスク (*) をワイルドカードに使用できます。また、カーネルソースファイル内の関数のエントリーと終了も追跡可能です。以下の例を見てみましょう。

例3.1 wildcards.stp

probe kernel.function("*@net/socket.c") { }
probe kernel.function("*@net/socket.c").return { }
この例では、最初のプローブのイベントは net/socket.c カーネルソースファイル内の全関数のエントリーを指定しています。2 つ目のプローブでは、これら全関数の終了を指定しています。この例では、ハンドラーにステートメントがないことに注意してください。このため、情報が収集されず、表示されることもありません。
kernel.trace("tracepoint")
tracepoint の静的プローブ。最近のカーネル (2.6.30 およびそれ以降) には、カーネル内の特定イベント用のインストルメンテーションが含まれています。これらのイベントは、トレースポイントで静的にマークが付けられています。SystemTap で利用可能なトレースポイントの例としては、kernel.trace("kfree_skb") があります。これは、カーネル内でネットワークバッファーが解放されると合図します。
module("module").function("function")
モジュール内の関数のプローブを可能にします。例を示します。

例3.2 moduleprobe.stp

probe module("ext3").function("*") { }
probe module("ext3").function("*").return { }
例3.2「moduleprobe.stp」 の最初のプローブは、ext3 モジュールの関数のエントリーを指しています。2 つ目のプローブは、同じモジュールの全関数の終了を指しています。.return 接尾辞の使用は、kernel.function() の場合と同様です。例3.2「moduleprobe.stp」 のプローブハンドラーにはステートメントがないことに注意してください。このため、有用なデータは表示されません (例3.1「wildcards.stp」 の場合と同様)。
システムのカーネルモジュールは通常 /lib/modules/kernel_version にあります。ここでの kernel_version は、現在読み込まれているカーネルのバージョンを指します。モジュールは、ファイル名拡張子 .ko を使用します。
非同期イベント

非同期イベントは、コード内の特定の指示や場所に関連付けられていません。このタイプのプローブポイントは、主にカウンターやタイマー、または同様のコンストラクトで構成されています。

非同期イベントの例には以下のようなものがあります。
begin
SystemTap セッションの開始です。つまり、SystemTap スクリプトの実行と同時です。
end
SystemTap セッションの終了。
timer イベント
ハンドラーの定期実行を指定するイベント。例を示します。

例3.3 timer-s.stp

probe timer.s(4)
{
  printf("hello world\n")
}
例3.3「timer-s.stp」 では、プローブが 4 秒ごとに hello world をプリントします。以下のような timer イベントも使用できます。
  • timer.ms(ミリ秒)
  • timer.us(マイクロ秒)
  • timer.ns(ナノ秒)
  • timer.hz(ヘルツ)
  • timer.jiffies(jiffies)
timer イベントを情報を収集する他のプローブと併せて使用すると、定期的な更新が表示でき、その情報の変遷が分かります。

重要

SystemTap supports the use of a large collection of probe events. For more information about supported events, see the stapprobes(3) manual page. The 『SEE ALSO』 section of stapprobes(3) also contains links to other manual pages that discuss supported events for specific subsystems and components.

3.2.2. Systemtap ハンドラー/ボディ

以下のサンプルスクリプトについて見ていきましょう。

例3.4 helloworld.stp

probe begin
{
  printf ("hello world\n")
  exit ()
}
例3.4「helloworld.stp」 では、begin イベント (セッションの開始) が { } で囲まれているハンドラーを始動させます。これは単に hello world をプリントして改行し、終了するものです。

注記

SystemTap スクリプトは、exit() 関数が実行されるまで継続されます。スクリプトの実行を停止したい場合は、手動で Ctrl+C と入力すると中断できます。
printf ( ) ステートメント

printf() ステートメントは、データをプリントする最も簡単な関数の 1 つです。printf() を以下の書式で使用すると、多くの SystemTap 関数を使用するデータを表示できます。

printf ("format string\n", arguments)
format string では、arguments のプリント方法を指定します。例3.4「helloworld.stp」 の format string は、単に SystemTap に hello world のプリントを指示するだけで、書式は指定していません。
引数によっては、%s (文字列用) や %d (数字用) といった書式指定子を format string に使用することもできます。format string には複数の書式指定子を使用することが可能で、それぞれを対応する引数に一致させます。複数の引数はコンマ (,) で区切ります。

注記

シマンテックの面では、SystemTap printf 関数は、C 言語の関数に似ています。上記の SystemTap の printf 関数における構文と書式は、C 言語の printf と同一のものです。
以下のプローブの例を見てみましょう。

例3.5 variables-in-printf-statements.stp

probe syscall.open
{
  printf ("%s(%d) open\n", execname(), pid())
}
例3.5「variables-in-printf-statements.stp」 では、SystemTap がシステムコール open への全エントリーをプローブするように指示しています。各イベントでは、現行の execname() (実行可能ファイル名の付いた文字列) と pid() (現行のプロセス ID 番号) に続けて open という単語をプリントします。このプローブ出力の抜粋は以下のようになります。
vmware-guestd(2206) open
hald(2360) open
hald(2360) open
hald(2360) open
df(3433) open
df(3433) open
df(3433) open
hald(2360) open
SystemTap 関数

SystemTap は、printf() 引数として使用可能な多くの関数をサポートしています。例3.5「variables-in-printf-statements.stp」では、SystemTap 関数 execname() (カーネル関数を呼び出した、またはシステムコールを実行したプロセス名) および pid() (現行プロセス ID) を使用しています。

一般的に使用される SystemTap 関数を以下に挙げます。
tid()
現行スレッドの ID。
uid()
現行ユーザーの ID。
cpu()
現行の CPU 番号。
gettimeofday_s()
Unix epoch (1970 年 1 月 1 日) からの秒数。
ctime()
UNIX epoch からの秒数を日にちに換算。
pp()
現在処理されているプローブポイントを記述する文字列。
thread_indent()
この関数はプリント結果をうまく整理するので、便利なものです。この関数はインデント差分の引数を取ります。これは、スレッドの「インデントカウンター」に追加する、またはそこから取り除くスペースの数を示すものです。その後、適切なインデントスペースの数と一般的な追跡データの文字列を返します。
ここで返される一般的なデータに含まれるのは、タイムスタンプ (スレッドの thread_indent() への最初のコールからのマイクロ秒)、プロセス名、およびスレッド ID です。これによりどの関数がコールされたか、誰がコールしたか、各関数コールの長さが特定できます。
各コールが終わり次第、次のコールが始まれば、エントリーと終了の一致は容易ですが、ほとんどの場合では最初の関数コールのエントリーがなされた後、これが終了する前に他の複数のコールが開始、終了したりすることがあります。インデントカウンターがあることで、最初のコールが終了していない場合、次の関数コールをインデントして、エントリーとそれに対応する終了が一致しやすくなります。
以下で thread_indent() の使用例を見てみましょう。

例3.6 thread_indent.stp

probe kernel.function("*@net/socket.c") 
{
  printf ("%s -> %s\n", thread_indent(1), probefunc())
}
probe kernel.function("*@net/socket.c").return 
{
  printf ("%s <- %s\n", thread_indent(-1), probefunc())
}
例3.6「thread_indent.stp」 では、各イベントでの thread_indent()probe の関数を以下の書式でプリントします。
0 ftp(7223): -> sys_socketcall
1159 ftp(7223):  -> sys_socket
2173 ftp(7223):   -> __sock_create
2286 ftp(7223):    -> sock_alloc_inode
2737 ftp(7223):    <- sock_alloc_inode
3349 ftp(7223):    -> sock_alloc
3389 ftp(7223):    <- sock_alloc
3417 ftp(7223):   <- __sock_create
4117 ftp(7223):   -> sock_create
4160 ftp(7223):   <- sock_create
4301 ftp(7223):   -> sock_map_fd
4644 ftp(7223):    -> sock_map_file
4699 ftp(7223):    <- sock_map_file
4715 ftp(7223):   <- sock_map_fd
4732 ftp(7223):  <- sys_socket
4775 ftp(7223): <- sys_socketcall
このサンプル出力には、以下の情報が含まれています。
  • スレッドの最初の thread_indent() コールからの時間 (マイクロ秒単位)。
  • 関数コールを実施したプロセス名 (およびその対応 ID)。
  • コールがエントリー (<-) か終了 (->) かを示す矢印。インデントがあることで、コールのエントリーと終了が一致しやすくなります。
  • プロセスが呼び出した関数名。
name
特定のシステムコールの名前を識別します。この変数は、イベント syscall.system_call を使用するプローブでのみ、使用可能です。
target()
以下の 2 つのコマンドのいずれかと併せて使用します。
stap script -x process ID stap script -c command
プロセス ID またはコマンドの引数を取るスクリプトを指定したい場合、スクリプト内で参照先となる変数として target() を使用します。例を示します。

例3.7 targetexample.stp

probe syscall.* {
  if (pid() == target())
    printf("%s/n", name)
}
例3.7「targetexample.stp」 を引数 -x process ID と実行すると、(syscall.* イベントで指定された) すべてのシステムコールを監視し、指定されたプロセスで実行された全システムコールの名前をプリントします。
これは、特定のプロセスをターゲットとしたい場合に毎回 if (pid() == process ID) と指定することと同様の効果があります。ただし、target() を使用するとスクリプトの再利用が容易になり、スクリプトの実行時に引数としてプロセス ID を渡すだけで済みます。例を示します。
stap targetexample.stp -x process ID
For more information about supported SystemTap functions, see stapfuncs(3).