More info...

2010-02-04

D言語基礎文法最速マスター(DTraceのほう)

そうそう、もう一つとっておきのネタがありました。ってことで、MySQL管理者最速マスターを書いたばかりだけど、さらに調子に乗ってお次はDTraceで使われているD言語について最速マスターネタを書いてみよう。こっちのD言語と紛らわしいが、英語にするとDTraceじゃない方は「D Programing Language」でDTraceの方は「D Language」なので、両者を区別出来る。このエントリでは「D言語」という記述が出てきたらDTraceの方を表すのであしからず。

DTraceの概念

DTraceは、システムのあらゆる場所を対象にして動作を追跡するための仕組みで、トラブルシューティングやプロファイリングに利用出来る。元々はSolaris 10向けに開発された機能だが、その後FreeBSDやMac OS Xへと移植された。DTraceはメモリ上にロードされた実行プログラムの内容を直接「動的に」書き換えて、プログラム内にプローブと呼ばれる計測点を設置する。プローブはカーネル空間でもユーザー空間でも設置できる。プログラムの実行がプローブにさしかかると、制御がカーネル内のdtraceモジュールに移り、そこで情報収集が行われる。どのような場所にプローブを設置して、どのような情報を採取するかという指令を出すのが、D言語というわけだ。プローブを設置する役割をもったモジュールをプロバイダといい、プロバイダごとに「どこにプローブを設置できるか」というルールが定められている。

プローブは動的にメモリ内へと注入される。従ってDTraceを利用していないとき、負荷はゼロである。

dtraceコマンド

DTraceのフロントエンドになるのが、dtraceコマンドだ。dtraceコマンドはインタープリタになっていて、D言語スクリプトを実行することが出来る。書式は次の通り。
shell> dtrace -l # 利用可能なプローブの一覧を表示する
shell> dtrace -s ファイル名 # スクリプトファイルをロードする
shell> dtrace -n 'D言語プログラム' # ワンライナー
dtraceコマンドを実行するにはrootユーザーになる必要がある。(Solarisの場合はrootじゃなくてもdtrace_proc、dtrace_user、dtrace_kernelなどの権限があれば良い。)sudoやpfexecなどを介してdtraceコマンドを呼び出すといいだろう。ユーザープログラムを追跡したい場合には、-pオプションでPIDを指定しよう。

D言語スクリプトファイルを実行形式にすることも出来る。その場合、ファイルの先頭を
#!/usr/sbin/dtrace -s
としよう。

詳しくはman dtraceで。

D言語の構造

D言語の基本的な書式はこうだ。

プローブ記述子
/ 述語 /
{
  アクション
}
この1セットを「プローブ節」と呼ぶ。必要なだけプローブ節をズラリと並べていくのがD言語のプログラミングスタイルだ。dtraceコマンドでD言語スクリプトを実行すると、指定した場所にプローブが設置される。が、この時点では何も起きない。トレース対象のプログラムがプローブにさしかかって始めてDTraceの出番がやってくるのである。ちなみに、トレース対象のプログラムがプローブへさしかかることを「プローブがファイアする」というが、まあ用語はどうでもいい。同じプローブ記述子のプローブ節をいくつも定義することも可能で、その場合は上から順番に実行される。

プローブ記述

プローブ記述子は「プロバイダ名:モジュール名:関数名:プローブ名」というタプルで指定する。プロバイダ名は必須だが、他の項目は省略できる。省略した場合はワイルドカード(*)と同じ扱いになり、該当したプローブが全て有効になる。

プローブの種類

まずはプローブを設置するプロバイダの種類(プロバイダ名とトレースのポイント)を紹介しよう。
  • fbt - function boundary tracing...関数の呼び出し(entry)とリターン(return)にプローブを設置する。
  • syscall...システムコール呼び出しとリターンをプローブで中継する。
  • sdt - statically defined tracing...ソースコードにおいて指定された任意の場所。(カーネル内) sdtには別名が色々つけられている。
  • pid ... ユーザープロセスにおける関数の呼び出しとリターン。
  • Fasttrap ... ソースコードにおいて指定された任意の場所。(ユーザープロセス内)
  • dtrace ... トレースの開始(BEGIN)と終了(END)、および例外発生時(ERROR)をトレース。
  • profiling ... 一定期間ごとに有効に鳴る。集計などに利用。

モジュール名、関数名、プローブ名は無数にあるので、どのような種類があるかはdtrace -lコマンドで確認しよう。以下はsyscallプロバイダで有効にできるプローブの一覧を表示するコマンド例。
shell> dtrace -l -P syscall # -Pはプロバイダ名を指定する。-n syscall::: でもOK

述語

「述語」はプローブがファイアしたときに、アクションを実行するかどうかを判定するための条件だ。ある条件に合致しただけアクションを実行するようにすれば、情報を絞り込んで抽出することが出来る。もちろん条件が「真」の時にアクションが実行されることになる。

アクション

ここで実際の情報収集をする。trace()とかtracemem()とかprintf()とかprinta()とかの関数を用いて情報を出力したり、変数に値を代入したりして後から参照したり述語の判定条件で利用したり出来る。

ちなみに、述語とアクションは省略可能だ。ただしアクションを省略する場合には、述語も省略する必要がある。省略時のデフォルトの動作はプローブがファイアしたことを示すメッセージが出るだけだ。dtraceコマンドに-Fオプションをつけると、呼び出し/リターン型のプローブにおいて、出力結果が「呼び出し時にインデントを増やしてリターン時にインデントを減らす」となって見易くなる。

変数の種類

D言語には5種類の変数が存在する。DTraceを使う場合には、対象のプログラムやカーネルモジュールがマルチスレッドになっていることを意識する必要があるのだが、変数の種類もこのDTraceの特性を反映したものになっている。

  • スカラー変数 ... いわゆる普通の変数。スコープはグローバル。(書式: x = 123)
  • スレッド固有変数 ... 読んで字のごとく、そのプローブをファイアしたスレッドに対してのみ有効な変数(書式: self->x = 123)
  • 節固有変数 ... プローブ固有の変数。プローブのアクションを全部実行すると消滅する。ローカル変数みたいな感じ?(書式: this->x = 123)
  • 組み込み変数 ... D言語によって予め定義された変数。(例: arg0、timestamp、uid、execname)
  • 外部変数 ... カーネル内のメモリ領域に直接アクセスする。危険。(例: `segkpsize)

組み込み変数

  • arg0、...、arg9 ... プローブに対する引数の最初の10個(数値型で取得)
  • caller ... プローブがファイアする直前のプログラムカウンタ
  • cwd ... カレントワーキングディレクトリ
  • errno ... 直前のシステムコールによって返されたエラーの値
  • uid ... 実行中のプロセスの実効ユーザーID
  • gid ... 実行中のプロセスの実効グループID
  • pid ... 実行中のプロセスID
  • timestamp ... タイムスタンプ
  • execname ... 実行プログラム名
  • probefunc ... 関数名
  • probename ... プローブ名

変数の型

D言語の変数は宣言は必要ないけど型が存在する。(ダックタイピングではないということだ。)
  • char ... 1バイト整数
  • short ... 2バイト整数
  • int ... 4バイト整数
  • long long ... 8バイト整数
  • float ... 4バイト浮動小数点
  • double ... 8バイト浮動小数点
  • long double ... 16バイト浮動小数点
  • string ... 文字列
多次元配列とか連想配列とかもあるけど、イテレーションが出来ないという制約があるのであんまり使わない。構造体とかポインタも使えるけど無視。

制御文

ifとかwhileとか、そんなものは無い!究極のstraightforwardな言語、ここにあり!漢は黙って真っ直ぐ進め!!ってなわけだが、条件分岐っぽいのをやりたい場合は「述語」でやるといい。条件分岐はないとは言え、マルチスレッドを意識しないといけないので、D言語スクリプトの挙動は割とややこしいんだけどね。

情報の出力

  • trace(式) ... 括弧内の式を評価した結果を標準出力へ出力する。
  • tracemem(アドレス, サイズ) ... カーネル空間内のメモリをダンプする。ユーザー空間のメモリは、いったんcopyin()とかcopyinstr()とかでdtraceモジュール内に読み込む必要あり。
  • printf(フォーマット, ...) ... C言語のprintf()と同じ感じ。
  • printa(集積体) ... 集積体を表示する。(詳細後述)
  • ustack()、jstack()、stack() ... それぞれユーザープロセスのスタック、Javaのスタック、カーネル内のスタックトレースを表示する。

このへんで具体例

ここまでの解説で出来るワンライナー。

どんなプログラムが実行されているかが逐一表示されるプログラム。
shell> dtrace -n 'syscall::exec*:entry { trace(copyinstr(arg0)) }'

どんなファイルがopenされているのかが逐一表示されるプログラム。
shell> dtrace -n 'syscall::open*:entry { trace(execname); trace(copyinstr(arg0))}'

システムコールがエラーになったらプロセスのスタックトレースを表示するプログラム。
dtrace -n 'syscall:::return/errno!=0/{trace(execname);trace(errno);ustack()}'

集積体

集積体は統計用の特殊な変数だ。プロファイリングをするときはコイツがないと話にならない。書式は変数名の前にアットマークをつけて、連想配列っぽく[]内でキーを指定し、なおかつ集計関数を代入する。printa()で集計結果を指定できる。

以下、集積体の例。

どのプロセスがどのファイルを何回openしたかをカウントする。
shell> dtrace -n 'syscall::open*:entry {@a[execname, copyinstr(arg0)]=count()} END{printa(@a)}'

プロセスごとのR/Wを集計
shell> dtrace -n 'syscall::read*:entry {@r[execname, pid]=sum(arg0)} syscall::write*:entry {@w[execname, pid]=sum(arg0)} END{printa(@r);printa(@w)}'

これまでの例はsyscallプロバイダしか使ってないが、もっと細かいトレースをしたいときは他のプロバイダを利用することになるだろう。

DTrace Toolkit

汎用的に利用出来るDTraceプログラムをセットにしたDTrace Toolkitというのが配布されている。ずいぶん昔に書いたエントリだが、賢いdtraceの使い方を参照のこと。

まとめっぽいもの

そんなわけで、DTraceは超便利なのでSolaris/Mac/FreeBSDユーザーはぜひDTraceを使って見てくだしあ。

0 件のコメント:

コメントを投稿