Systemtap Begginers Guide 3章のメモ

Understanding How Systemtap Works

SystemTapは実行中のLinuxシステムを、単純なスクリプトで詳細に調査出来るように設計されている。

SystemTapスクリプトの裏側にある主なアイデアは、イベントとハンドラである。
SystemTapスクリプトを実行すると、SystemTapはイベントを監視し、
そしてイベントが発生するとハンドラを高速なサブルーチンとして実行し、そしてまた監視に戻る。

SystemTapは関数の出入、タイマーの発火、セッションの切断など、多様なイベントを監視出来る。
ハンドラは、一種のスクリプト言語として、イベント発生時の処理を記述出来る。

3.1 Architecture

SystemTapスクリプトの実行は、以下のように進行する。

  1. スクリプトをチェックして、tapsetsをtapsetライブラリに置き換える。
  2. スクリプトC言語に変換して、カーネルモジュールを作る。
  3. モジュールをロードして、SystemTapが機能するのようにする。これはsystemtap-runtimeパッケージのstaprunが行う。
  4. イベント発生時に、対応するハンドラを実行する。
  5. SystemTapセッションの終了時に、一連の処理を無効にして、カーネルモジュールをアンロードする。

これらの処理はstapコマンドで起動する。詳細はman stapを見よ。

3.2 SystemTap Scripts

SystemTapSystemTapスクリプトに記述されたように動作して、情報を集める。
SystemTapスクリプトは、イベントとハンドラという2つの要素によって構成される。
SystemTapが実行されると、イベントを監視して、発生したイベントに対するハンドラを実行する。

イベントと、そのイベントに対応したハンドラをまとめてプローブ(probe)と呼ぶ。
SystemTapスクリプトは複数のプローブを持つ。
プローブのハンドラは、プローブボディ部として参照される。
フォーマット

SystemTapスクリプトのファイル拡張子は.stpを使う。
プローブは以下のフォーマットで記述する。

probe イベント {手続き}

イベントは、カンマで区切る事で列挙出来る。
イベントを列挙すると、それらイベントの全てでハンドラが実行される。

イベント発生時に実行する手続きはブレース({})内に記述する。
手続きを;で区切る必要はない。
SystemTapの文法はCに習っている。このため手続きブロックのネストも可能である。

SystemTapでは、以下のフォーマットでサブルーチンも書く事が出来る。

function サブルーチン名(引数) {手続き}

3.2.1 イベント

SystemTapのイベントは同期と非同期に分けられる。

同期イベント

プロセスがカーネルの特定の箇所を実行する毎に、同期イベントが発生する。

システムコール名で指定されたシステムコールに入った時に発生するイベント。
システムコールから抜ける時に発生するイベントは syscall.システムコール名.return で指定する。

  • vfs.ファイル操作名

仮想ファイルシステムに対するファイル操作に入った時に発生するイベント。
.returnでファイル操作終了時のイベントを指定出来る。

  • kernel.function("関数名")

関数名で指定されたカーネル関数に入った時に発生するイベント。
これも.returnでカーネル関数から出る時に発生するイベントを指定出来る。

アスタリスク(*)でワイルドカードを使うことも出来る。
特定のソースファイルで定義されている全ての関数の出入りのイベントを指定する場合には以下のように記述出来る。

probe kernel.function("*@net/socket.c") { }
probe kernel.function("*@net/socket.c").return { }

この例ではnet/socket.cで定義されている全ての関数のイベントを指定している。

  • kernel.trace("トレースポイント名")

カーネルに組み込まれたトレースポイントを指定することも出来る。

  • module("モジュール名").function("関数名")

カーネルモジュールとモジュール内の関数を指定出来る。
モジュールext3の全ての関数の出入りをイベントとして指定する場合には以下のようにする。

probe module("ext3").function("*") { }
probe module("ext3").function("*").return { }
非同期イベント

非同期イベントはコードの特定の箇所と結びついていない。
非同期イベントには以下のようなものがある。

  • begin

SystemTapセッションの開始時に発生する。

  • end

SystemTapセッションの終了時に発生するイベント。

  • タイマーイベント

周期的に発生を指定出来るイベント。
例えば4秒周期でHello, worldを出力する場合には、以下のように指定する。

probe timer.s(4)
{
  printf("hello world\n")
}

以下のタイマーイベントもある。

  • timer.ms(ミリ秒)
  • timer.us(マイクロ秒)
  • timer.ns(ナノ秒)
  • timer.hz(ヘルツ)
  • timer.jiffies(jiffies)
重要
SystemTapではたくさんのプローブイベントをサポートしている。
詳細はman stapprobesを参照せよ。
また、man stapprobesのSEE ALSOには、他のサブシステムやコンポーネントがサポートしているイベントに関するリンクがある。

3.2.2 SystemTap Handller/Body

SystemTapスクリプトで、ハンドラは{}に記述する。
SystemTapスクリプトはexit()が実行されるまで、続行する。
Ctrl+cで停止することも出来る。

printf() Statements

printfはC言語のprintfと同じように使える。
書式指定子に%sを指定すれば文字列を、%dを指定すれば整数値を出力出来る。

SystemTap functions

printf以外にもSystemTapには様々な関数が用意されている。
よく使われる関数を紹介する。

  • tid()

現在のスレッドIDを返す。

  • uid()

現在のユーザーIDを返す。

  • cpu()

現在のCPU番号を返す。

  • gettimeofday_s()

UNIXエポック(January 1, 1970)で時間を返す。単位は秒。

  • ctime()

UNIXエポック時間を日付に変換する。

  • pp()

現在のプローブポイント名を返す。

  • thread_indent()

関数コールグラフを作成する際に、インデントの調整を補佐する関数。
thread_indent()の引数に渡した値の数だけ、内部にもつ"インデントカウンタ"を増減させる。
thread_indent()は、時間(マイクロ秒),プロセス名,プロセスID及びインデントカウンタ分のスペースで成る文字列を返す。
したがって、関数に入った時にthread_indent(1)を呼び出し、関数から出る時にthread_indent(-1)を呼ぶことで、
整った関数コールグラフを作る事が出来る。

以下のように、net/socket.cの出入り口でthread_indentを実行すると、

probe kernel.function("*@net/socket.c").call
{
  printf ("%s -> %s\n", thread_indent(1), probefunc())
}
probe kernel.function("*@net/socket.c").return
{
  printf ("%s <- %s\n", thread_indent(-1), probefunc())
}

以下のような結果が得られる。

     0 Xorg(816): -> sock_poll
     2 Xorg(816): <- sock_poll
     0 gnome-shell(1136): -> sock_poll
     5 gnome-shell(1136): <- sock_poll
     0 gnome-shell(1136): -> sock_aio_write
     4 gnome-shell(1136):  -> alloc_sock_iocb
     7 gnome-shell(1136):  <- alloc_sock_iocb
    16 gnome-shell(1136): <- sock_aio_write
     0 gnome-shell(1136): -> sock_poll
     3 gnome-shell(1136): <- sock_poll
     0 Xorg(816): -> sock_poll
     3 Xorg(816): <- sock_poll

各行の一番左の値は、インデントカウンタが0の時にthread_indent()が実行された時間からの差分である(単位はマイクロ秒)。

  • name

実行されたシステムコールの名前が設定される。

  • target()

"stap script -x プロセスID" もしくは "stap スクリプト名 -c command" と併用する。
このようにしてstapを実行した場合に、target()はそのプロセスIDを返す。
この関数を使えば特定のプロセスだけを対象にして、SystemTapスクリプトを書く事が出来る。

例:

probe syscall.* {
 if (pid() == target())
  printf("%s/n", name)
}

さらに詳しい情報が知りたい場合には、man stapfuncsを参照せよ。

3.3 Basic SystemTap Handler Constructs

SystemTapはCやawkから取り入れた文法を使う事が出来る。

3.3.1 変数

変数は各ハンドラ内で自由に定義できる。
グローバル変数は、プローブの外で、変数名の前にglobalと付けて定義する。
初期化していない変数の初期値は0である。
インクリメント演算子、デクリメント演算子も使用可能である。

例:

global count_jiffies, count_ms
probe timer.jiffies(100) { count_jiffies ++ }
probe timer.ms(100) { count_ms ++ }
probe timer.ms(12345)
{
 hz=(1000*count_jiffies) / count_ms
 printf ("jiffies:ms ratio %d:%d => CONFIG_HZ=%d\n",
  count_jiffies, count_ms, hz)
 exit ()
}

3.3.2 Target Variable

プローブがカーネルのコードの実際の位置に対応している場合には、
コード上に見えている変数から値を得る事が出来る。
どのような変数が参照出来るかは、-Lオプションで確認出来る。
例えばvfs_readの変数を確認する場合には以下のように実行する。

$ stap -L 'kernel.function("vfs_read")'
kernel.function("vfs_read@fs/read_write.c:364") $file:struct file* $buf:char* $count:size_t $pos:loff_t*

$に続くのが変数名で、:の後ろが変数の型である。
fs/read_write.cのvfs_read関数は、実際に以下のように定義されている。

ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)

グローバル変数や、別のファイルに定義されたファイルローカル変数などを参照する場合には、
"@var("varname@src/file.c")"のように参照する。
アロー演算子->を使ってメンバを参照することも出来る。

例:

stap -e 'probe kernel.function("vfs_read") {
     printf ("current files_stat max_files: %d\n",
             @var("files_stat@fs/file_table.c")->max_files);
     exit(); }'

カーネル空間のデータにアクセスするための関数も定義されている。

  • kernel_char(address)
  • kernel_short(address)
  • kernel_int(address)
  • kernel_long(address)
  • kernel_string(address)
  • kernel_string_n(address, n)

3.3.2.1 Pretty Printing Target Variables

SystemTapは、Target Variablesを決まった書式の文字列にして取得する方法を提供している。

  • $$vars

プローブ位置のスコープにある全ての変数を、
sprintf("parm1=%x ... parmN=%x var1=%x ... varN=%x", parm1, ..., parmN, var1, ..., varN)の書式の文字列で返す。
実行時に見つからない場合には"=?"となる。

  • $$locals

$$varsと同じ。ただし、変数はローカル変数のみ。

  • $$params

$$varsと同じ。ただし、変数は関数の引数のみ。

  • $$return

returnプローブの時にのみ使える。
戻り値をsprintf("return=%x", $return)の書式の文字列に展開する。
戻り値が無い場合には、空文字列になる。

変数が構造体へのポインタだった場合には、$$vars$のようにして、$を後ろに続けて置くと
構造体の内容を表示するようになる。

構造体がさらにポインタを持つ場合に、その構造体のポインタの指すデータ内容も表示する場合には、
$$vars$$のようにして$$を後ろに続けて置くことで、ネストした内容を表示する事が出来る。

3.3.2.2 型キャスト

void*型やアドレスを整数値として扱っている場合には、
キャストを使って型を変換して値を扱えるようにできる。
キャストは以下のように行う。

function task_state:long (task:long)
{
  return @cast(task, "task_struct", "kernel<linux/sched.h>")->state
}

@castの第一引数がアドレスを格納する変数。第二引数が型名。第三引数が、型が定義されているファイル名である。

3.3.2.3 Checking Target Variable Availability

変数がコード中に実際に存在するか確認出来る。これには@definedを使う。
@definedの引数に指定した変数が有効であれば、SystemTapが最適な値を返す。
以下が使用例で、変数flagsがプローブ位置で有効かどうか確認している。

probe vm.pagefault = kernel.function("__handle_mm_fault@mm/memory.c") ?,
                     kernel.function("handle_mm_fault@mm/memory.c") ?
{
 name = "pagefault"
 write_access = (@defined($flags) ? $flags & FAULT_FLAG_WRITE : $write_access)
 address = $address
}

3.3.3 条件文

SystemTapでは、条件分岐のif/elseとループ構文のwhile, forを使う事が出来る。
それぞれCの構文と同じように使う事が出来る。

if/else

if (condition)
 statement1
else
 statement2

while

while (condition)
 statement

for

for (initialization; conditional; increment) statement

また、条件演算子には>=, <=, !=, ==の4つを使う事が出来る。

3.3.4 コマンドライン引数

コマンドライン引数は、$数値もしくは@数値のようにしてスクリプト中に記述する。
$で指定した場合、値は整数値として扱われる。@で指定した場合には、文字列として展開される。
コマンドライン引数の第一引数を文字列として展開する際の例は以下の通り。

probe kernel.function(@1) { }
probe kernel.function(@1).return { }

第二引数以降を指定する際には、2, 3順番に指定していけばよい。

3.4 連想配列

SystemTap連想配列をサポートしている。
SystemTap連想配列は、通常では、いくつものprobeで使われるため、グローバル変数として定義される。

SystemTap連想配列の構文は、配列名[インデックス表現]である。
連想配列なのでインデックスには整数値の他に文字列も使える。
さらに、インデックスはカンマ区切りで最大9個までの値を指定出来る。
以下は配列の使用例。

global foo
global array
probe begin
{
	foo["tom"] = 23
	foo["dick"] = 24
	foo["harry"] = 25
	array[pid(), execname(), uid(), ppid(), 1] = "hello"
	exit()
}
重要
全ての連想配列は、一つのprobeでしか使わないとしてもグローバル変数として宣言されなければならない。

3.5 SystemTapでの配列操作

配列操作の使い方を示す。

3.5.1 連想配列への値の代入

3.4の例のように、"配列名[インデックス] = 値"で代入出来る。
以下の例のように、インデックスと値の箇所で関数を評価することも出来る。

foo[tid()] = gettimeofday_s()

3.5.2 値の読み込み

以下の例のように、配列にインデックスを指定すれば、値を読むことが出来る。
キーに対応した値が無い場合には0が返ってくる。

delta = gettimeofday_s() - foo[tid()]

3.5.3 連想値のインクリメント

"配列名[インデックス] ++"でインクリメントが出来る。--でデクリメントも出来る。

3.5.4 連想配列の各要素に対する演算

連想配列の各要素に対して一つずつ処理をする場合には、foreachを使う事が出来る。
以下の例のように、foreach(インデックス変数 in 配列名)としてforeach文を実行すると、
ループ毎にインデックス変数にインデックスの値が代入される。

global reads
probe vfs.read
{
  reads[execname()] ++
}
probe timer.s(3)
{
  foreach (count in reads)
     printf("%s : %d \n", count, reads[count])
}

連想配列なので、順序が維持されていない。インデックスを昇順で取り出したい場合には、
foreach(インデックス変数+ in 配列名)としてforeach文を実行すればよい。
降順の場合には、foreach(インデックス変数- in 配列名)とする。
取り出すインデックスの数を制限したい場合には、
foreach(インデックス変数 in 配列名 limit 整数値)としてforeach文を実行する。

以下の例では、降順で最大10個までインデックスを取り出している。

probe timer.s(3)
{
  foreach (count in reads- limit 10)
    printf("%s : %d \n", count, reads[count])
}

一つのキーに複数の値をカンマで指定した場合には、foreach([キー1, キー2, ...] in 配列名)で、
foreachを実行出来る。

global array
probe begin
{
	array[pid(), execname(), 1] = "hello"
	array[pid(), execname(), 2] = "world"
	array[pid(), execname(), 3] = "foo"

	foreach([id, name, index] in array) {
		printf("%d %s %d = %s\n", 
			id, name, index, array[id, name, index])
	}
	exit()
}

3.5.5 配列と配列要素の削除

"delete 配列名"で、配列の内容を全て消去出来る。
"delete 配列名[インデックス]"で、インデックスで指定された要素を削除出来る。

global array

probe begin
{
        array[0] = "hello"
        array[1] = "world"
        array[2] = "foo"
        exit()
}

probe end
{
        delete array[0]
        foreach(k in array)
                printf("%s\n", array[k])
        delete array
        foreach(k in array)
                printf("%s\n", array[k])
}

3.5.6 条件文で配列を使う

if文でも連想配列を評価して値を使う事が出来る。

連想配列がキーを持つかどうか判定する。

"[インデックス] in 配列名"で、連想配列がインデックスを持つかどうか判定出来る。

global reads
probe vfs.read
{
 reads[execname()] ++
}

probe timer.s(3)
{
  printf("=======\n")
  foreach (count in reads+)
    printf("%s : %d \n", count, reads[count])
  if(["stapio"] in reads) {
    printf("stapio read detected, exiting\n")
  exit()
 }
}

3.5.7 統計集合の計算

連想配列と関係ない気がするが、ここで紹介されていた。
演算子<<<を使って変数に代入すると、代入回数、合計数、最小値、最大値、平均値を計算してくれる。
値を取り出す場合には、@取り出したい値(変数)で取り出せる。

以下の例は0から4までの統計結果を出力している。
連想配列でなく、普通の変数でも使える。

global data
probe begin
{
	for(index = 0; index < 5; index++)
		data["hello"] <<< index
	exit()
}

probe end
{
	printf("count=%d sum=%d min=%d max=%d avg=%d\n",
		@count(data["hello"]),
		@sum(data["hello"]),
		@min(data["hello"]),
		@max(data["hello"]),
		@avg(data["hello"]))
}

3.6 Tapsets

SystemTapスクリプトの使うライブラリ群をTapsetsと呼ぶ。
Tapsetsは/usr/share/systemtap/tapsetにある.stp拡張子を持つファイルである。
Tapsetsは実行出来ない。
thread_indent()はindent.stpに定義されている関数である。