Solaris 動的トレースガイド

第 1 章 はじめに

この章では、Solaris オペレーティングシステムの動的トレース機能について説明します。DTrace は、システムの動作を把握したいと考えているユーザーにとって理想的なツールです。Solaris に組み込まれた包括的な動的トレース機能、DTrace を利用して、管理者や開発者は、実行中の本稼働システム上で、ユーザープログラムやオペレーティングシステムの動作を検査できます。DTrace には、システムの動作を調べる機能、複数のソフトウェア層でパフォーマンスの問題を検出する機能、システムの異常な動作の原因を突き止める機能などがあります。これから見ていくように、DTrace では、システムを動的に計測し、DTrace D プログラミング言語で記述して任意の問題に対して簡潔な答えを迅速に導き出すような、独自のカスタムプログラムを作成することもできます。この章の最初の部分では、DTrace の概要と、単純な D プログラムの作成方法について説明します。残りの部分では、D プログラミングの規則の全容と、システムを詳しく分析する手順およびヒントを紹介します。Web 上の DTrace コミュニティー (http://www.sun.com/bigadmin/content/dtrace/ ) では、DTrace の利用経験やスクリプトをその他のユーザーと共有できます。このマニュアルで紹介するすべてのスクリプト例は、Solaris システムの /usr/demo/dtrace ディレクトリに収められています。

入門ガイド

DTrace では、オペレーティングシステムカーネルとユーザープロセスに動的に変更を加え、「プローブ」と呼ばれる特定の場所に指定の追加データが記録されるようにすることで、ソフトウェアシステムの情報を得ることができます。プローブとは、DTrace が一連の「アクション」(スタックトレースの記録、タイムスタンプの記録、関数の引数の記録など) の実行要求を結合する場所またはアクティビティを指します。プローブは、Solaris システム内の要所要所に配置された、プログラミング可能な「センサー」として機能します。何が起こっているか確認したい場合は、DTrace を使って、該当する情報が適切なセンサーに記録されるようにプログラミングします。この結果、DTrace は、プローブが「起動」するたびにプローブからデータを収集し、ユーザーに報告します。プローブにアクションを指定しないと、DTrace はプローブの起動を記録するだけです。

DTrace のプローブには、2 つの名前があります。 1 つは一意の整数値の ID、もう 1 つは人間が読める形式の文字列名です。以下では、手始めに、新しいトレース要求を発行するたびに 1 回だけ起動するプローブ BEGIN を使って、ごく単純な要求を作成します。文字列名を指定してプローブを有効にするには、dtrace(1M) ユーティリティーの -n オプションを使用します。次のコマンドを入力します。


# dtrace -n BEGIN

しばらくすると、1 個のプローブが有効になったというメッセージに続いて、BEGIN プローブの起動を知らせる行が出力されます。この出力のあと、dtrace は、ほかのプローブの起動を待機して一時停止した状態になります。ここでは、ほかに有効なプローブがなく、BEGIN は 1 回しか起動しないため、Control-C キーを押して dtrace を終了し、シェルプロンプトに戻ってください。


# dtrace -n BEGIN
dtrace: description 'BEGIN' matched 1 probe
CPU     ID		      FUNCTION:NAME
  0      1                  :BEGIN
^C
#

この出力から BEGIN という名前のプローブが 1 回起動したことがわかり、その名前と整数値の ID (ここでは 1) が表示されています。デフォルトでは、このプローブが起動した CPU の整数名も出力されます。この例の CPU の欄を見ると、dtrace コマンドがプローブの起動時に CPU 0 で実行されていたことがわかります。

DTrace 要求の作成時には、任意の数のプローブおよびアクションを使用できます。以下では、上記のコマンド例に END プローブを追加して、2 つのプローブを使った単純な要求を作成してみましょう。END プローブは、トレースの完了時に 1 回だけ起動します。次のコマンドを入力し、BEGIN プローブの出力行が表示されたら、先ほどと同様に Control-C キーを押します。


# dtrace -n BEGIN -n END
dtrace: description 'BEGIN' matched 1 probe
dtrace: description 'END' matched 1 probe
CPU     ID		      FUNCTION:NAME
  0      1                  :BEGIN
^C
  0      2                    :END
#

Control-C キーを押すと、dtrace が終了し、END プローブが引き起こされます。dtrace は、END プローブの起動を報告してから終了します。

プローブを指定し、有効にする方法がわかったところで、今度は、おなじみの「Hello, World」プログラムの DTrace 版を作成してみましょう。DTrace 実行文は、コマンド行に入力するだけでなく、D プログラミング言語を使ってテキストファイルに記述することもできます。ここでは、テキストエディタで hello.d という名前の新しいファイルを作成し、以下の D プログラムを記述します。


例 1–1 hello.d: D プログラミング言語で記述した “Hello, World” プログラム

BEGIN
{
	trace("hello, world");
	exit(0);
}

プログラムを保存し、dtrace -s オプションを使って実行します。次のコマンドを入力します。


# dtrace -s hello.d
dtrace: script 'hello.d' matched 1 probe
CPU     ID		      FUNCTION:NAME
  0	    1                  :BEGIN   hello, world
#

先ほどの例と同様の出力に続いて、“hello, world” というテキストが出力されます。先ほどの例とは異なり、ここでは、しばらく待つ必要も Control-C キーを押す必要もありません。これは、hello.d ファイル内の BEGIN プローブに、「アクション」が指定されたからです。では、D プログラムの構造をもう少し掘り下げて、何が起こっているのか確認していきましょう。

D プログラムは、有効化の対象となるプローブ (複数可) について記述する複数の「」と、プローブの起動時に実行するアクションの集合 (オプション) で構成されます。アクションは、プローブ名に続く中括弧 ({ }) の中に、連続する文として列挙されます。各文の末尾には、セミコロン (;) が付きます。最初の文では、関数 trace() を使って、BEGIN プローブの起動時に指定の引数 (ここでは文字列「hello, world」を記録し、出力するように、DTrace に指示しています。2 番目の文では、関数 exit() を使って、トレースを中断し、dtrace コマンドを終了するように指示しています。DTrace は、trace()exit() のように、D プログラム内で呼び出せる便利な関数を提供しています。関数を呼び出すには、関数名に続いて、丸括弧内に引数を指定します。すべての D 関数については、第 10 章アクションとサブルーチンを参照してください。

C プログラミング言語の知識をお持ちのユーザーは、ここまでの例や名前から、DTrace の D プログラム言語が C に似ていることに気が付いていることでしょう。実は、D 言語は、C 言語のサブセットにトレース用の特殊な関数と変数を組み合わせた言語なのです。D 言語の機能の詳細については、第 2 章以降で学習します。以前に C プログラミングをしたことがあるなら、D 言語でトレースプログラムを作成するために、その知識をほぼそのまま流用できます。もちろん、C プログラミングの経験がなくても、D プログラミングは決して難しいものではありません。D プログラミングの全構文は、この章で学ぶことができます。言語の規則について学ぶ前に、まず DTrace の機能についてもう少し詳しく見ていきましょう。そのあとで上記より複雑な D プログラムの作成方法を学んでいきます。

プロバイダとプローブ

上記の例では、BEGINEND という 2 つの単純なプローブの使用方法について学習しました。これらのプローブは、どこから提供されたものなのでしょうか。DTrace のプローブは、「プロバイダ」と呼ばれるカーネルモジュールのセットから提供されています。プロバイダはそれぞれ、プローブを作成して特定の計測機能を実行します。各プロバイダは、DTrace の使用時に、DTrace フレームワークに提供可能なプローブを発行するチャンスを与えられます。その後、ユーザーは、トレースアクションを有効にし、発行された任意のプローブに結合します。システム上で使用可能な全プローブを一覧するには、次のコマンドを入力します。


# dtrace -l
  ID   PROVIDER            MODULE          FUNCTION NAME
   1     dtrace                                     BEGIN
   2     dtrace                                     END
   3     dtrace                                     ERROR
   4   lockstat           genunix       mutex_enter adaptive-acquire
   5   lockstat           genunix       mutex_enter adaptive-block
   6   lockstat           genunix       mutex_enter adaptive-spin
   7   lockstat           genunix       mutex_exit  adaptive-release

   ... many lines of output omitted ...

#

すべての内容が出力されるまで、多少時間がかります。プローブの総数を表示するには、次のコマンドを入力します。


# dtrace -l | wc -l
        30122

プローブの数は、使用するオペレーティングプラットフォームと、インストールされているソフトウェアの種類によって異なります。このため、このコマンドで出力される値は、マシンによって異なります。利用可能なプローブは膨大な数にのぼります。これらを利用して、システムの動作を隅々まで詳しく確認できます。厳密には、上のコマンドで出力された数に加えて、その他のプローブも利用できます。あとで見ていきますが、一部のプロバイダは、ユーザーのトレース要求に応じて直接、新しいプローブを作成できます。したがって、利用可能な DTrace プローブは、事実上無限にあることになります。

端末ウィンドウ内の dtrace -l の出力をもう 1 度見てください。前述したように、各プローブには、整数値の ID と人間が読める形式の名前があります。人間が読める形式の名前は 4 つの部分で構成され、dtrace の出力中で個別の列に表示されます。プローブ名を構成する 4 つの部分は、次のとおりです。

プロバイダ 

このプローブを発行した DTrace プロバイダの名前。プロバイダ名は、通常、プローブを有効にする計測機能を実行する DTrace カーネルモジュールの名前と一致しています。 

モジュール 

プローブが置かれているモジュールの名前 (このプローブが特定のプログラムの場所である場合)。この名前は、カーネルモジュール名またはユーザーライブラリ名です。 

機能 

プローブが置かれているプログラム関数の名前 (このプローブが特定のプログラムの場所である場合)。 

名前 

BEGINEND のように、プローブの機能を連想しやすい名前。プローブ名の最後の部分です。

人間が読める形式のプローブ名を完全な形で記述するときは、4 つの部分をコロンで区切ります。

provider:module: function:name

一覧に表示されるプローブの中には、先ほど使用した BEGINEND のように、モジュールと関数を持たないものもあります。これらのプローブは、特定の計測機能付きプログラムの関数や場所を表すものではないので、モジュールのフィールドと関数のフィールドが空欄になっています。これらのプローブは、「トレース要求の終了」など、より抽象度の高い概念を表しています。名前の構成要素としてモジュール名と関数名を持つプローブを「アンカーされたプローブ」、これらの名前を持たないプローブを「アンカーされていないプローブ」と呼びます。

要求時にプローブ名の一部を省略して指定した場合、DTrace は通常、指定された名前と一致する値を持つすべてのプローブを検出します。たとえば、BEGIN と指定した場合、DTrace は、プロバイダフィールド、モジュールフィールド、関数フィールドの値とは関係なく、BEGIN という名前フィールドを持つプローブをすべて検出します。上記の例では、たまたま一致する文字列を持つプローブが 1 個しかなかったため、同じ結果になりました。BEGIN プローブの正式な名前は dtrace:::BEGIN です。この名前が示すように、このプローブは DTrace フレームワーク自体から提供され、関数にアンカーされていません。したがって、hello.d プログラムは、次のように書き換えることができます。結果は、上記の例と同じです。

dtrace:::BEGIN
{
	trace("hello, world");
	exit(0);
}

これで、プローブの提供元と、プローブの指定方法がわかりました。以下では、プローブを有効にし、DTrace に何らかの処理を指示したときの様子について少し詳しく見ていきます。その後、D プログラミング言語の解説に戻ります。

コンパイルと計測機能

Solaris で従来のプログラムを作成するときは、プログラムを記述したあと、コンパイラを使って、ソースコードを実行可能なオブジェクトコードに変換します。dtrace コマンドの使用時には、先ほど hello.d プログラムの記述に使用した D 言語用のコンパイラが呼び出されます。コンパイルの完了後、プログラムは DTrace で実行するためにオペレーティングシステムカーネルに送信されます。ここで、プログラムに指定されたプローブが有効になり、対応するプロバイダにより、これらのプローブをアクティブにするために必要な計測機能が実行されます。

DTrace の計測機能はすべて、完全に動的です。 使用されるプローブだけが、個別に有効になります。アクティブでないプローブに、計測機能用コードはありません。このため、DTrace を使用していないときに、システムのパフォーマンスが低下することはありません。計測が完了し、dtrace コマンドが終了すると、それまで使用していたすべてのプローブが自動的に無効になり、その計測機能が削除されます。こうして、システムは完全に元の状態に戻ります。DTrace がアクティブになっていないときのシステムの状態は、DTrace ソフトウェアがインストールされていないときのシステムの状態と実質的に同じです。

各プローブの計測機能は、稼働中のオペレーティングシステムまたはユーザーが選択したユーザープロセスに対して動的に実行されます。システムは休止または停止することなく、計測機能コードは有効になったプローブにのみ追加されます。このため、DTrace を使用したプローブの影響は、ユーザーが DTrace に指示した内容だけに制限されます。 関係のないデータがトレースされたり、システム内で一元的な「トレーススイッチ」がオンになったりすることはありません。このように、すべての DTrace 計測機能は、できるだけ能率よく機能するように設計されています。DTrace のこれらの機能を利用すれば、システムを停止することなく、リアルタイムで問題を解決できます。

DTrace フレームワークは、任意の数の仮想クライアントをサポートします。DTrace 計測や DTrace コマンドは、システムのメモリー容量が許すかぎり、好きな数だけ同時に実行できます。なお、DTrace コマンドは、配下にある共通の計測機能を使用しつつ、それぞれ独立して処理を行います。このため、1 つのシステム上で同時に DTrace を利用できるユーザーの数は、事実上無制限になります。 開発者、管理者、サービス担当者は、共同で作業できるだけでなく、同一システム上のさまざまな問題に、互いに干渉することなく個別に対処できます。

DTrace D プログラムは、安全な中間形式にコンパイルされたあと、プローブの起動時に実行されます。この点で、C や C++ より、JavaTM プログラミング言語で書かれたプログラムによく似ています。この中間形式の安全性は、DTrace カーネルソフトウェアがこのプログラムを最初に検査するときに検証されます。また、DTrace 実行環境は、D プログラムの実行中に発生する実行時エラー (ゼロ除算、無効なメモリーの間接参照など) を処理し、その結果をユーザーに報告します。このため、信頼性の低いプログラムを作成して、Solaris カーネルやシステムで実行中のプロセスに DTrace が害を及ぼす危険を回避できます。これらの安全性機能により、本稼働環境でシステムのクラッシュや破損を心配することなく DTrace を使用できます。プログラミングミスを犯しても、DTrace がエラーを報告し、計測機能を無効にするため、ユーザーはエラー箇所を修正し、再試行するだけで済みます。DTrace のエラー報告機能とデバッグ機能については、あとで説明します。

以下の図に、プロバイダ、プローブ、DTrace カーネルソフトウェア、dtrace コマンドをはじめとする、DTrace アーキテクチャの各種構成要素を示します。

図 1–1 DTrace のアーキテクチャと構成要素の概要

DTrace アーキテクチャ: カーネル機能およびプロバイダ、カーネルからライブラリへのドライバインタフェース、複数のコマンドをサポートするライブラリ。

DTrace の機能について理解できたところで、D プログラミング言語の解説に戻り、いくつかのプログラムを作成してみましょう。

変数と算術式

次のプログラム例では、DTrace の profile プロバイダを使って、単純な時間ベースのカウンタを実装します。profile プロバイダは、D プログラム内の記述に基づいて新しいプローブを作成できます。たとえば、profile:::tick-nsec (n は整数) という名前のプローブを作成した場合、profile プロバイダは、n 秒ごとに起動するプローブを 1 つ作成します。次のソースコードを記述し、counter.d という名前のファイルに保存します。

/*
 * Count off and report the number of seconds elapsed
 */
dtrace:::BEGIN
{
	i = 0;
}

profile:::tick-1sec
{
	i = i + 1;
	trace(i);
}

dtrace:::END
{
	trace(i);
}

実行すると、Control-C キーを押すまでの経過秒数がカウントされ、最後に合計が出力されます。


# dtrace -s counter.d
dtrace: script 'counter.d' matched 3 probes
CPU     ID                    FUNCTION:NAME
  0  25499                       :tick-1sec         1
  0  25499                       :tick-1sec         2
  0  25499                       :tick-1sec         3
  0  25499                       :tick-1sec         4
  0  25499                       :tick-1sec         5
  0  25499                       :tick-1sec         6
^C
  0      2                             :END         6
#

プログラムの最初の 3 行は、プログラムの内容を説明するコメント行です。C、C++、および Java プログラミング言語の場合と同様に、D コンパイラは /**/ で囲まれた文字列をすべて無視します。コメントは、D プログラム内のどこでも使用できます。プローブ節の中でも外でも使用できます。

BEGIN プローブ節では、次の文により、i という名前の新しい変数が定義され、整数値 0 が割り当てられています。

i = 0;

C、C++、および Java プログラミング言語の場合とは異なり、D の変数は、プログラム文の中で使用するだけで作成されます。明示的変数宣言は不要です。変数の型は、この変数をプログラム内ではじめて使用したとき、そこに代入された値の型になります。各変数は 1 つの型を持ち、プログラムの完了時まで変わりません。したがって、2 回目以降の使用時にも、最初に代入された値と同じ型の値を参照させる必要があります。たとえば、counter.d の変数 i に最初に代入される値は、整数定数 0 です。そのため、この変数の型は int になります。D は、C と同じ基本的な整数データ型を提供します。たとえば、次のデータ型があります。

char

文字型または単一バイトの整数型 

int

デフォルト整数型 

short

短整数型 

long

長整数型 

long long

拡張された長整数型 

これらのデータ型のサイズは、オペレーティングシステムカーネルのデータモデル (第 2 章型、演算子、および式を参照) によって異なります。また D では、オペレーティングシステムごとに定義されている各種データ型と同様に、さまざまな固定長の符号付き/符号なし整数型に、覚えやすい組み込み名が付けられています。

counter.d の中央部分には、カウンタ i の値を増分するプローブ節があります。

profile:::tick-1sec
{
	i = i + 1;
	trace(i);
}

この節には、使用可能なプロセッサ上で毎秒 1 回起動する新しいプローブを作成するように profile プロバイダに指示する、profile:::tick-1sec という名前のプローブが指定されています。この節に含まれる 2 つの文のうち、最初の文は以前の値に 1 を加算して i に代入し、2 番目の文は新しい i の値をトレースします。D では、C の一般的な算術演算子をすべて使用できます。使用可能な算術円算子のリストは、第 2 章型、演算子、および式に記載されています。また C の場合と同様に、++ 演算子は、対応する変数を 1 増分したい場合に使用します。trace() 関数は、引数として任意の D 式を取ります。この考えに従って counter.d をさらに簡潔に書き直すと、次のようになります。

profile:::tick-1sec
{
	trace(++i);
}

変数 i の型を明示的に制御したい場合は、型名を丸括弧で囲んで割り当てます。こうすれば、整数 0 がその型に「キャスト」されます。たとえば、D で char の最大サイズを決定したい場合は、BEGIN 節を次のように変更します。

dtrace:::BEGIN
{
	i = (char)0;
}

counter.d の実行を開始してしばらくすると、トレース値が大きくなり、その後再びゼロに戻ります。値がゼロに戻るのを待てない場合は、profile プローブ名を profile:::tick-100msec に変更してみてください。こうすれば、カウンタの値は 100 ミリ秒に 1 回 (毎秒 10 回) のペースで増分するようになります。

述語

D とその他のプログラミング言語 (C、C++、Java など) のもっとも顕著な違いは、D では、if 文やループに代表される制御フロー構文が使用されない点にあります。D プログラム節は、決まった量のデータ (オプション) をトレースする単一の文のリストとして記述されます。D では、プログラム節の前に置かれる「述語」という論理式を使って、条件付きでデータをトレースしたり、制御フローを変更したりできます。述語式は、プローブの起動時、対応する節に関連付けられた文が実行される前に評価されます。述語の評価の結果が真 (ゼロ以外の値) なら、文のリストが実行されます。述語の評価の結果が偽 (ゼロ) なら、文は実行されず、プローブの起動は無視されます。

次のソースコードを入力し、countdown.d という名前のファイルに保存してください。

dtrace:::BEGIN
{
	i = 10;
}

profile:::tick-1sec
/i > 0/
{
	trace(i--);
}

profile:::tick-1sec
/i == 0/
{
	trace("blastoff!");
	exit(0);
}

この D プログラムは、述語を使用することにより、10 秒間のカウントダウンを行うタイマーを実装しています。countdown.d を実行すると、10 からカウントダウンが開始され、メッセージが表示されたあと、プログラムは終了します。

# dtrace -s countdown.d
dtrace: script 'countdown.d' matched 3 probes
CPU     ID                    FUNCTION:NAME
	0  25499                       :tick-1sec        10
	0  25499                       :tick-1sec         9
	0  25499                       :tick-1sec         8
	0  25499                       :tick-1sec         7
	0  25499                       :tick-1sec         6
	0  25499                       :tick-1sec         5
	0  25499                       :tick-1sec         4
	0  25499                       :tick-1sec         3
	0  25499                       :tick-1sec         2
	0  25499                       :tick-1sec         1
	0  25499                       :tick-1sec   blastoff!
# 

このプログラムは、まず BEGIN プローブを使って、整数 i をカウントダウンの開始値 10 に初期化します。次に、前記の例と同じく tick-1sec プローブを使って、毎秒 1 回起動するタイマーを実装します。countdown.d では、tick-1sec プローブが述語とアクションリストの異なる 2 つの節に記述されている点に注目してください。述語は、プローブ名と節の文が入っている中括弧 ({ }) との間に置かれた論理式です。前後をスラッシュ (/ /) で囲まれた形式になっています。

最初の述語は、i がゼロより大きいかどうか (タイマーがまだ実行中かどうか) をテストします。

profile:::tick-1sec
/i > 0/
{
	trace(i--);
}

関係演算子 > は、「-より大きい」を意味し、偽の場合は整数値 0、真の場合は 1 を返します。D では、C の関係演算子がすべてサポートされます。使用可能な関係演算子のリストは、第 2 章型、演算子、および式に記載されています。i がまだゼロになっていない場合、スクリプトは i をトレースし、-- 演算子を使って、値を 1 減らします。

2 番目の述語は、== 演算子を使って、i が 0 (カウントダウンが完了した) の場合に真を返します。

profile:::tick-1sec
/i == 0/
{
	trace("blastoff!");
	exit(0);
}

最初の hello.d の例の場合と同様に、 countdown.d は、二重引用符で囲まれた文字列 (「文字列定数」) を使って、カウントダウン完了時のメッセージを出力します。次に、exit() 関数を使って dtrace を終了し、シェルプロンプトに戻ります。

countdown.d の構造をよく見ると、プローブの記述は同じであるが述語とアクションが異なる 2 つの節を作成することによって、次のような論理フローが作成されていることがわかります。

i = 10
毎秒 1 回
	i がゼロより大きい場合
		trace(i--);
	i がゼロの場合
		trace("blastoff!");
		exit(0);

述語を使って複雑なプログラムを作成するときは、まずこの方法でアルゴリズムを視覚化した上で、それぞれの条件構文を別々の節および述語に変換してみることをお勧めします。

では次に、述語を新しいプロバイダ syscall と組み合わせて、実際に D トレースプログラムを作成してみましょう。syscall プロバイダには、任意の Solaris システムコールの開始時または終了時にプローブを有効にする機能があります。次の例では、DTrace を使って、シェルがシステムコール read(2) または write(2) を実行する様子を観察します。まず、端末ウィンドウを 2 つ開きます。1 つは DTrace 用、もう 1 つは監視するシェルプロセス用です。後者のウィンドウに次のコマンドを入力して、このシェルのプロセス ID を取得します。


# echo $$
12345

1 つ目の端末ウィンドウに戻り、次の D プログラムを入力して、rw.d という名前のファイルに保存します。プログラムを入力する際、12345 は、シェルのプロセス ID (echo コマンドの実行結果) で置き換えてください。

syscall::read:entry,
syscall::write:entry
/pid == 12345/
{

}

rw.d のプローブ節の本体には、何も入力しません。これは、このプログラムではプローブの起動通知をトレースするだけで、ほかには何もトレースしないからです。rw.d の内容を入力できたら、dtrace を使って計測を開始します。その後、2 つ目のウィンドウでいくつかのコマンドを実行します。なお、コマンドを 1 つ入力したら、そのたびに Return キーを押す必要があります。コマンドの実行に合わせて、1 つ目のウィンドウに、dtrace からのプローブ起動通知が表示されます。以下の出力例を参照してください。


# dtrace -s rw.d
dtrace: script 'rw.d' matched 2 probes
CPU     ID                    FUNCTION:NAME
	0     34                      write:entry 
	0     32                       read:entry 
	0     34                      write:entry 
	0     32                       read:entry 
	0     34                      write:entry 
	0     32                       read:entry 
	0     34                      write:entry 
	0     32                       read:entry 
...

この出力から、シェルがシステムコール read(2)write(2) を実行することにより、端末ウィンドウから文字を読み取り、結果をエコーバックしていることがわかります。この例では、これまでに説明した概念のほかに、いくつかの新しい概念が含まれています。まず、read(2)write(2) を同じ方法で計測するため、複数のプローブ記述をコンマで区切った形式の単一のプローブ節が使用されています。

syscall::read:entry,
syscall::write:entry

読みやすさを考慮して、これらのプローブ記述は 2 行に分けて入力されています。この配置は必須ではありませんが、より読みやすいスクリプトを作成するのに役立ちます。次に、該当のシェルプロセスが実行するシステムコールだけを検出する述語が定義されています。

/pid == 12345/

この述語では、事前定義済みの DTrace 変数 pid (対応するプローブを起動したスレッドのプロセス ID が入る) が使用されています。DTrace には、プロセス ID のような使用頻度の高い情報を導き出す変数定義が多数組み込まれています。以下に、基本的な D プログラムの作成に役立つ DTrace 変数をいくつか紹介しておきます。

変数名 

データ型 

意味 

errno

int

システムコールの現在の errno

execname

string

現在のプロセスの実行可能ファイルの名前 

pid

pid_t

現在のプロセスのプロセス ID 

tid

id_t

現在のスレッドのスレッド ID 

probeprov

string

現在のプローブ記述のプロバイダフィールド 

probemod

string

現在のプローブ記述のモジュールフィールド 

probefunc

string

現在のプローブ記述の関数フィールド 

probename

string

現在のプローブ記述の名前フィールド 

作成した計測機能プログラムを使用し、プロセス ID と計測するシステムコールプローブを変更して、システム上で実行中のさまざまなプロセスを計測してみてください。その後、もう少し簡単な変更を加えることで、rw.dtruss(1) のような非常に単純なシステムコールトレースツールに変更できます。何も記述されていないプローブ記述フィールドは、「あらゆるプローブ」を表すワイルドカードとして機能します。上記のプログラムを次のような新しいソースコードに変更することによって、シェルで実行されるあらゆるシステムコールをトレースできるようになります。

syscall:::entry
/pid == 12345/
{

}

シェルに cdlsdate などのコマンドを入力して、DTrace プログラムからの報告内容を確認してください。

出力書式

システムコールをトレースすることにより、ほとんどのユーザープロセスの動作を効果的に監視できます。管理者または開発者として Solaris の truss(1) ユーティリティーを使用したことがあるユーザーなら、このユーティリティーが問題の解決に役立つ手軽で便利なツールであることをすでにご存知でしょう。truss をまだ使ったことがないユーザーも、シェルに次のコマンドを入力して、今すぐその効果を試してください。


$ truss date

date(1) によって実行されるすべてのシステムコールのトレース結果が書式付きで表示され、最後の行に date(1) そのものの出力が表示されます。次の例は、上記の rw.d プログラムに truss(1) とよく似た出力書式を設定し、出力を読みやすくしたものです。以下のプログラムを入力し、trussrw.d という名前のファイルに保存してください。


例 1–2 trussrw.d: truss(1) の出力書式を設定したシステムコールトレースプログラム

syscall::read:entry,
syscall::write:entry
/pid == $1/
{
	printf("%s(%d, 0x%x, %4d)", probefunc, arg0, arg1, arg2);
}

syscall::read:return,
syscall::write:return
/pid == $1/
{
	printf("\t\t = %d\n", arg1);
}

この例では、すべての述語の定数 12345 の部分が、ラベル $1 で置き換えられています。このラベルを使用することで、監視対象のプロセスをスクリプトの引数として指定できます。 $1 には、スクリプトのコンパイル時に、最初の引数の値が代入されます。trussrw.d を実行するには、dtrace-q オプションと -s オプションを指定したあと、最後の引数としてシェルのプロセス ID を指定します。-q オプションを指定すると、dtrace が非出力モードになり、これまでの例で出力されていた見出し行と CPU 列および ID 列が出力されなくなります。つまり、出力されるのは、明示的にトレースしたデータだけになります。指定のシェルウィンドウで次のコマンドを入力し (その際、12345 の部分はシェルプロセスのプロセス ID で置き換える)、指定したシェルで、Return キーを数回押します。


# dtrace -q -s trussrw.d 12345
	                 = 1
write(2, 0x8089e48,    1)                = 1
read(63, 0x8090a38, 1024)                = 0
read(63, 0x8090a38, 1024)                = 0
write(2, 0x8089e48,   52)                = 52
read(0, 0x8089878,    1)                 = 1
write(2, 0x8089e48,    1)                = 1
read(63, 0x8090a38, 1024)                = 0
read(63, 0x8090a38, 1024)                = 0
write(2, 0x8089e48,   52)                = 52
read(0, 0x8089878,    1)                 = 1
write(2, 0x8089e48,    1)                = 1
read(63, 0x8090a38, 1024)                = 0
read(63, 0x8090a38, 1024)                = 0
write(2, 0x8089e48,   52)                = 52
read(0, 0x8089878,    1)^C
#

ここからは、D プログラムとその出力についてさらに詳しく見ていきます。まず、上記のプログラムとよく似た節により、シェルで呼び出されている read(2)write(2) が計測されます。ただし、この例では、データのトレースと結果の書式付き出力に、新しい関数 printf() が使用されています。

syscall::read:entry,
syscall::write:entry
/pid == $1/
{
	printf("%s(%d, 0x%x, %4d)", probefunc, arg0, arg1, arg2);
}

printf() 関数は、以前に使用した trace() 関数のようにデータをトレースする機能と、データとその他のテキストを指定された書式で出力する機能を兼ね備えています。DTrace は、printf() 関数からの指示を受けて、2 番目以降の各引数に関連付けられたデータをトレースし、最初の printf() 引数 (「書式設定文字列」) で指定された規則に従って結果を書式設定します。

書式設定文字列は、% で始まる任意の数の書式変換を含む標準文字列で、対応する引数の書式を指定します。この例の書式設定文字列の最初の変換は 2 番目の printf() 引数、2 番目の変換は 3 番目の引数 (以降同様) に対応しています。変換と変換の間のすべてのテキストは、変更されずそのまま出力されます。% 変換文字に続く文字は、対応する引数の書式を表します。以下に、trussrw.d で使用されている 3 つの書式変換の意味を示します。

%d

対応する値を 10 進整数として出力 

%s

対応する値を文字列として出力 

%x

対応する値を 16 進整数として出力 

DTrace の printf() の機能は、C のprintf(3C) ライブラリルーチンやシェルの printf(1) ユーティリティーと同じです。以前に printf() を使ったことがない場合は、第 12 章出力書式にその書式とオプションの詳細が記載されているので確認してください。この章は、すでに別の言語の printf() を使用したことがあるユーザーも、注意深く読む必要があります。D の printf() は組み込み関数であり、DTrace 専用の新しい書式変換を利用できます。

D コンパイラには、1 つ 1 つの printf() 書式設定文字列をその引数リストと照合することにより、プログラミングミスを防ぐ機能があります。上記の例の節にある probefunc を整数 123 に変更してみてください。変更後のプログラムを実行すると、文字列書式変換 %s では整数引数を扱うことができないことを示すエラーメッセージが表示されます。


# dtrace -q -s trussrw.d
dtrace: failed to compile script trussrw.d: line 4: printf( )
	   argument #2 is incompatible with conversion #1 prototype:
	        conversion: %s
	         prototype: char [] or string (or use stringof)
	          argument: int
#

システムコール read または write の名前とその引数を出力するには、次のような printf() 文を使用します。

printf("%s(%d, 0x%x, %4d)", probefunc, arg0, arg1, arg2);

この文は、現在のプローブ関数の名前とシステムコールの最初の 3 つの整数引数 (DTrace 変数 arg0arg1、および arg2) をトレースします。プローブ引数の詳細は、第 3 章変数を参照してください。read(2)write(2) の最初の引数はファイル記述子を表しており、10 進数として出力されます。2 番目の引数はバッファーアドレスを表しており、16 進数値として書式設定されています。最後の引数はバッファーサイズを表しており、10 進数値として書式設定されています。3 番目の引数で使用されている書式指定子 %4d は、%d 書式変換を使用し、最小フィールド幅を 4 文字に制限して値を出力するように指定しています。4 文字未満の整数であれば、printf() によって適切な数の空白文字が挿入され、出力の配置が調整されます。

システムコールの結果を出力し、各出力行を完成させるには、次の節を使用します。

syscall::read:return,
syscall::write:return
/pid == $1/
{
	printf("\t\t = %d\n", arg1);
}

また、syscall プロバイダは、システムコールごとに、entry プローブと return プローブの両方を発行します。syscall プロバイダの return プローブの DTrace 変数 arg1 には、システムコールの戻り値が入ります。この戻り値は、10 進整数として書式設定されます。書式設定文字列内のバックスラッシュで始まる文字シーケンスは、タブ (\t) と改行 (\n) を表しています。これらの「エスケープシーケンス」は、入力するのが難しい文字を出力または記録する場合に使用します。D では、C、C++、および Java プログラミング言語と共通のエスケープシーケンスがサポートされています。すべてのエスケープシーケンスについては、第 2 章型、演算子、および式を参照してください。

配列

D では、整数型の変数のほか、文字列型や複合型を表す型として、「構造体」と「共用体」を定義できます。D では、C で使用できるすべての型を使用できます。C プログラミングの経験がないユーザーは、第 2 章型、演算子、および式で、さまざまなデータ型について確認してください。D では、「連想配列」と呼ばれる特殊な変数もサポートされています。連想配列は、キーの集合を値の集合に関連付けるという点では通常の配列と同じですが、キーが固定範囲の整数値に制限されないという特徴を備えています。

D の連想配列には、1 個以上の任意の型の値からなるリストでインデックスを付けることができます。個々のキー値が集まって「」を形成し、この組が、配列へのインデックス付けと、個々のキーに対応する値の参照または変更に使用されます。連想配列で使用される組はすべて、同じ型署名を使用している必要があります。これは、すべての組キーの長さ、キー型、およびキーの並び順が一致していなければならないということです。連想配列の各要素に関連付けられる値も、配列全体の固定型になります。たとえば、次の D 文は、組署名が [ string, int ]、値の型が int の新しい連想配列 a を定義し、この配列に整数値 456 を格納します。

a["hello", 123] = 456;

配列の定義後は、その他の D 変数と同じ方法で、配列要素にアクセスできます。たとえば、次の D 文は、値を 456 から 457 に増分することにより、以前に a に格納された配列要素を変更します。

a["hello", 123]++;

配列要素の値がまだ割り当てられていない場合、値はゼロに設定されます。では、実際に D プログラム内で連想配列を使用してみましょう。以下のプログラムを入力し、rwtime.d という名前のファイルに保存してください。


例 1–3 rwtime.d: read(2) および write(2) 呼び出しの時間

syscall::read:entry,
syscall::write:entry
/pid == $1/
{
	ts[probefunc] = timestamp;
}

syscall::read:return,
syscall::write:return
/pid == $1 && ts[probefunc] != 0/
{
	printf("%d nsecs", timestamp - ts[probefunc]);
}

rwtime.d の実行時には、trussrw.d のときと同じように、シェルプロセスの ID を指定します。シェルコマンドをいくつか入力すると、各システムコールの経過時間が表示されます。次のコマンドを入力し、別のシェルで Return キーを何回か押します。


# dtrace -s rwtime.d `pgrep -n ksh`
dtrace: script 'rwtime.d' matched 4 probes
CPU     ID                    FUNCTION:NAME
  0     33                      read:return 22644 nsecs
  0     33                      read:return 3382 nsecs
  0     35                     write:return 25952 nsecs
  0     33                      read:return 916875239 nsecs
  0     35                     write:return 27320 nsecs
  0     33                      read:return 9022 nsecs
  0     33                      read:return 3776 nsecs
  0     35                     write:return 17164 nsecs
...
^C
#

各システムコールの経過時間をトレースするには、read(2)write(2) の entry (開始時) および return (終了時) を両方とも計測し、各ポイントで時間を確認する必要があります。次に、システムコールの終了時に、最初のタイムスタンプと 2 番目のタイムスタンプの差分を計算します。システムコールごとに別々の変数を使用することもできますが、そうすると新たにシステムコールを追加してプログラムを拡張するのが難しくなります。そのため、プローブ関数名でインデックスが付けられた連想配列を使用します。以下は、最初のプローブ節です。

syscall::read:entry,
syscall::write:entry
/pid == $1/
{
	ts[probefunc] = timestamp;
}

この節では、ts という名前の配列が定義され、適切なメンバーに DTrace 変数 timestamp の値が割り当てられます。この変数は、Solaris ライブラリルーチン gethrtime(3C) とよく似た機能を持っており、常に増加するナノ秒カウンタの値を返します。entry のタイムスタンプが保存されたあと、対応する return プローブにより、再度 timestamp が調べられ、現在の時間と保存された値の差分が報告されます。

syscall::read:return,
syscall::write:return
/pid == $1 && ts[probefunc] != 0/
{
	printf("%d nsecs", timestamp - ts[probefunc]);
}

return プローブの述語は、DTrace がトレースしているプロセスが適切であることと、対応する entry プローブがすでに起動していて、ts[probefunc] にゼロ以外の値が割り当てられていることを要求します。この方法で、DTrace がはじめて起動したときの無効な出力を排除できます。dtrace の実行時に、すでにシェルが read(2) システムコール内で入力を待っている場合、最初の read(2)read:entry を飛ばして、read:return プローブが起動します。このとき、まだ割り当てられていない ts[probefunc] の値はゼロになります。

外部のシンボルと型

DTrace 計測機能は、Solaris オペレーティングシステムカーネルの内部で実行されます。このため、特殊な DTrace 変数やプローブ引数だけでなく、カーネルデータの構造、シンボル、および型にもアクセスできます。DTrace の上級ユーザー、管理者、サービス担当者、ドライバ開発者は、この機能を利用して、オペレーティングシステムカーネルやデバイスドライバの低レベルの動作を検査できます。Solaris オペレーティングシステムの内部構造については、このマニュアルの冒頭で紹介した関連書籍を参照してください。

D では、オペレーティングシステムには定義されているが、D プログラム内では定義されていないシンボルにアクセスする際、特別なスコープ演算子として逆引用符 (`) を使用します。たとえば、Solaris カーネルには、メモリーアロケータのデバッグ機能を有効にする、チューニング可能なシステム変数 kmem_flags の C 宣言が含まれています。kmem_flags の詳細は、『Solaris カーネルのチューンアップ・リファレンスマニュアル』を参照してください。このチューニング可能な変数は、次のように、カーネルソースコード内に C で宣言されています。

int kmem_flags;

この変数の値を D プログラム内でトレースする場合は、次のような D 文を記述します。

trace(`kmem_flags);

DTrace は、カーネルシンボルに、対応するオペレーティングシステム C コードで使用されている型を関連付けます。このため、ネイティブのオペレーティングシステムデータ構造に、ソースから簡単にアクセスできます。カーネルシンボル名は、D 変数および関数識別子とは別の名前空間に格納されています。したがって、カーネルシンボル名と D 変数名が競合することはありません。

この章では、DTrace の概要を把握し、より規模が大きく複雑な D プログラムを作成するために必要な DTrace の基礎知識を身に付けました。以降の章では、D の規則の全容を明らかにし、実際に DTrace を使って、複雑なパフォーマンス測定やシステム機能分析を単純化する方法を学んでいきます。さらに、DTrace を使ってユーザーアプリケーションの動作とシステムの動作を関連付け、ソフトウェアスタック全体を分析する方法についても学びます。

DTrace の学習は、まだ始まったばかりです。