13 ユーザー・アプリケーションの静的定義トレース

DTraceには、pidプロバイダの機能を強化するために、カスタマイズされたプローブをユーザー・アプリケーション開発者がアプリケーション・コード内に定義する機能が提供されています。これらの静的プローブは、無効化されるとオーバーヘッドがほとんどゼロに近く、その他のすべてのDTraceプローブと同様に動的に有効化されます。静的プローブを使用すると、アプリケーションの実装知識を公開または必要とせずにDTraceのユーザーにアプリケーション・セマンティクスを説明できます。この章では、静的プローブをユーザー・アプリケーション内に定義する方法とそのようなプローブをユーザー・プロセス内で有効にする方法について説明します。

ノート:

DTraceは、32ビットおよび64ビット・バイナリの両方でユーザー・アプリケーションの静的定義トレースをサポートしています。

カーネル・モジュールでの静的プローブの使用方法の詳細は、「カーネル・モジュールの静的定義トレース」を参照してください。

 プローブ・ポイントの選択

DTraceを使用すると、開発者は、完全なアプリケーションも共有ライブラリも含めてアプリケーション・コードに静的プローブ・ポイントを埋め込むことができます。これらのプローブは、アプリケーションまたはライブラリが実行しているところであれば、開発および本番のいずれにおいても有効にできます。DTraceのユーザー・コミュニティにとって容易に理解できるセマンティックな意味を持つプローブを定義する必要があります。たとえば、リクエストを送信しているクライアントに対応するWebサーバーおよびそのリクエストに応答しているWebサーバーにはquery-receiveプローブおよびquery-respondプローブを定義できます。これらのサンプル・プローブは、ほとんどのDTraceユーザーによって簡単に理解でき、アプリケーションの低レベルの実装詳細ではなく最高レベルの抽象化オブジェクトに対応します。DTraceユーザーは、これらのプローブを使用してリクエストの時間配分を把握できます。使用しているquery-receiveプローブで引数としてURLリクエスト文字列が表示された場合、DTraceユーザーはこのプローブとioプロバイダを組み合せることでどのリクエストがほとんどのディスクI/Oを生成していたかを判別できます。

プローブの名前と場所を選択するときは、記述する抽象化オブジェクトの安定性も考慮する必要があります。たとえば、実装が変わっても、アプリケーションの将来のリリースでプローブは存続しますか。このプローブはすべてのシステム・アーキテクチャで意味があるか、または特定の命令セットに固有のものか?この章では、これらの判定によってどのように静的なトレース定義が導かれるかを説明します。

 アプリケーションに対するプローブの追加

ライブラリおよび実行可能ファイル用のDTraceプローブが、対応するアプリケーション・バイナリのELFセクションに定義されています。この項では、プローブの定義、プローブのアプリケーション・ソース・コードへの追加、およびDTraceプローブ定義をインクルードするアプリケーションのビルド・プロセスの増補のトピックについて詳しく説明します。

プロバイダおよびプローブの定義

DTraceプローブを.dソース・ファイルに定義します。このファイルはアプリケーションのコンパイルおよびリンク時に使用されます。最初に、ユーザー・アプリケーション・プロバイダの適切な名前を選択します。選択したプロバイダ名には、アプリケーション・コードを実行している各プロセスのプロセスIDが追加されます。たとえば、プロセスID 1203として実行されていたWebサーバーのプロバイダ名myservを選択した場合、このプロセスに対応するDTraceプロバイダ名はmyserv1203になります。.dソース・ファイルに、次の例に示すようなプロバイダ定義を追加します。

provider myserv
{
  ...
};

次に、各プローブおよび対応する引数を追加します。次の例は、「プローブ・ポイントの選択」で説明した2つのプローブを定義しています。最初のプローブには2つの引数があり、どちらの型もchar *です。2つ目のプローブに引数はありません。Dコンパイラでは、プローブ名の2つの連続する下線(__)がダッシュ(-)に変換されます。

provider myserv
{
  probe query__receive(char *, char *);
  probe query__respond();
};

アプリケーションの将来バージョンでの変更の可能性をプローブのコンシューマが理解できるように、プロバイダ定義に安定性属性を追加できます。DTraceの安定性属性の詳細は、「DTraceの安定性機能」を参照してください。

次の例は、安定性属性の定義方法を示しています。

#pragma D attributes Evolving/Evolving/Common provider myserv provider
#pragma D attributes Private/Private/Unknown provider myserv module
#pragma D attributes Private/Private/Unknown provider myserv function
#pragma D attributes Evolving/Evolving/Common provider myserv name
#pragma D attributes Evolving/Evolving/Common provider myserv args

provider myserv
{
  probe query__receive(char *, char *);
  probe query__respond();
};

アプリケーション・コードに対するプローブの追加

.dファイルにプローブを定義したら、ソース・コードを増補してプローブをトリガーする場所を示す必要があります。次のCアプリケーション・ソース・コード例を検討してください。

void main_look(void)
{
  ...
  query = wait_for_new_query();
  process_query(query);
  ...
}

アプリケーションにプローブを追加するには、プローブ定義に基づいてヘッダー・ファイルを生成するdtraceコマンドに-hオプションを使用します。たとえば、次のコマンドではヘッダー・ファイルmyserv.hが生成されます。このファイルにはmyserv.d内のプローブ定義に対応するマクロ定義が含まれます。

# dtrace -h -s myserv.d

これは実装が簡潔で理解しやすいコーディングであるため、この方法をお薦めします。この方法はCおよびC++の両方との互換性もあります。さらに、生成されたマクロはプロバイダ定義に定義した型に依存するため、コンパイラで型チェックを実行できます。

たとえば、プローブ・サイトを追加するには、dtrace -hによりmyserv.hに定義されているMYSERV_QUERY_RECEIVEマクロを使用できます。

#include "myserv.h"
...
void main_look(void)
{
  ...
  query = wait_for_new_query();
  MYSERV_QUERY_RECEIVE(query->clientname, query->msg);
  process_query(query);
  ...
}

前述の例では、マクロ名にプロバイダ名とプローブ名の両方がエンコードされています。

プローブが有効かどうかのテスト

DTraceプローブの計算オーバーヘッドは、通常、いくつかのno-op命令と同等です。ただし、プローブ引数を設定すると、特にコードがクラスまたはメソッドの名前を実行時に決定する必要がある動的言語の場合はコストが高くなる場合があります。

プローブ・マクロの他に、dtrace -hコマンドでもプロバイダ定義に指定した各プローブに対するis-enabled probeマクロが作成されます。必要なときにのみDTraceプローブに対する引数をプログラムで計算するようにするには、次のようにis-enabledプローブ・テストを使用してプローブが現在有効かどうかを確認できます。

if (MYSERV_QUERY_RECEIVE_ENABLED())
  MYSERV_QUERY_RECEIVE(query->clientname, query->msg);

プローブ引数を計算するための計算コストが高い場合、is-enabledプローブ・テストの実行により生じるわずかなオーバーヘッドは、プローブが有効でない場合のオフセットより多くなります。

プローブを含むアプリケーションの構築

DTraceプロバイダおよびプローブ定義をインクルードするようにアプリケーション・ビルド・プロセスを増補する必要があります。通常のビルド・プロセスでは、各ソース・ファイルを読み込んでコンパイルし、対応するオブジェクト・ファイルが作成されます。コンパイルされたオブジェクト・ファイルは、次の例に示すようにその後相互にリンクされて完成したアプリケーション・バイナリが作成されます。

src1.o: src1.c
    gcc -c src1.c

src2.o: src2.c
    gcc -c src2.c

myserv: src1.o src2.o
    gcc -o myserv src1.o src2.o

DTraceプローブ定義をアプリケーションにインクルードした場合、ビルド・プロセスに適切なMakefileルールを追加してdtraceコマンドを実行する必要があります。

dtraceコマンドは、先行するコンパイラ・コマンドにより作成されたオブジェクト・ファイルを後処理して、myserv.dおよびその他のオブジェクト・ファイルからオブジェクト・ファイルmyserv.oを生成します。-Gオプションは、プロバイダおよびプローブ定義とユーザー・アプリケーションをリンクするために使用されます。

gccへの-Wl,--export-dynamicリンク・オプションは、たとえばustack()を実行するなどして、実行時に削除された実行ファイルのシンボル検索をサポートするために必要です。

ソース・コードにプローブを挿入するとき、dtrace -hにより作成されたヘッダー・ファイルに定義されているマクロを使用した場合は、Makefileにそのコマンドを含める必要があります。

myserv.h: myserv.d
    dtrace -h -s myserv.d

src1.o: src1.c myserv.h
    gcc -c src1.c

src2.o: src2.c myserv.h
    gcc -c src2.c

myserv.o: myserv.d src1.o src2.o
    dtrace -G -s myserv.d src1.o src2.o

myserv: myserv.o
    gcc -Wl,--export-dynamic,--strip-all -o myserv myserv.o src1.o src2.o

Makefileのルールでは、プローブ定義へのヘッダー・ファイルの依存が考慮されます。

静的に定義されたプローブの使用

DTraceヘルパー・デバイス(/dev/dtrace/helper)を使用すると、USDTプローブを含んだユーザー空間アプリケーションでプローブ・プロバイダ情報をDTraceに送信できます。

トレース対象のプログラムがroot以外のユーザーにより実行されている場合、DTraceヘルパー・デバイスのモードを変更してユーザーがトレース情報を記録できるようにします。

# chmod 666 /dev/dtrace/helper

または、システムにaclパッケージがインストールされている場合は、次の例に示すように、ACLルールを使用して、特定のユーザーへのアクセスを制限できます。

# setfacl -m u:guest:rw /dev/dtrace/helper
# ls -l /dev/dtrace
total 0
crw-rw----  1 root root 10, 16 Sep 26 10:38 dtrace
crw-rw----+ 1 root root 10, 17 Sep 26 10:38 helper
drwxr-xr-x  2 root root     80 Sep 26 10:38 provider
# getfacl /dev/dtrace/helper
getfacl: Removing leading '/' from absolute path names
# file: dev/dtrace/helper
# owner: root
# group: root
user::rw-
user:guest:rw-
group::rw-
mask::rw-
other::---

ノート:

ユーザーがプログラムを実行する前に、デバイスのモードを変更する必要があります。

ユーザー・アプリケーション内のプローブのフルネームは、通常のprovider PID : module : function : nameの形式をとります。
provider

プロバイダ定義ファイルで定義されたプロバイダの名前。

PID

実行中の実行可能ファイルのプロセスID。

module

実行可能ファイルの名前。

function

プローブが位置する関数の名前。

name

プロバイダ定義ファイルに定義されたプローブの名前で、連続する2つの下線(__)はすべてダッシュ(-)により置換されます。

たとえば、PIDが1173のmyservプロセスの場合、query-receiveプローブのフルネームはmyserv1173:myserv:main_look:query-receiveとなります。

次の簡単な例は、トレースされたプロセスをdtraceから呼び出す方法を示しています。

# dtrace -c ./myserv -qs /dev/stdin <<EOF
                     $target:::query-receive
    {
      printf("%s:%s:%s:%s %s %s\n", probeprov, probemod, probefunc, probename,
                                    stringof(args[0]), stringof(args[1]));
    }
  $target:::query-respond
    {
      printf("%s:%s:%s:%s\n", probeprov, probemod, probefunc, probename);
    }
EOF

myserv1173:myserv:main_look:query-receive foo1 msg1
myserv1173:myserv:process_query:query-respond
myserv1173:myserv:main_look:query-receive bar2 msg1
myserv1173:myserv:process_query:query-respond
...

ノート:

query-receiveプローブの場合、stringof()を使用してargs[0]およびargs[1]をキャストしてstringを入力します。それ以外の場合は、次のようなDTraceコンパイル・エラーが表示されます。

dtrace: failed to compile script /dev/stdin: line 7:
printf( ) argument #5 is incompatible with conversion #4 prototype:
	conversion: %s
	 prototype: char [] or string (or use stringof)
	  argument: char *

プローブ定義ファイルでプローブ引数がchar *ではなく型stringとして定義されている場合は、次のようなコンパイル警告が表示されます。

In file included from src1.c:5:
myserv.h:39: warning: parameter names (without types) in function declaration

この場合、プローブ引数を型stringにキャストする必要はなくなります。

次のスクリプトは、簡単なユーザー空間プログラムのインストゥルメント、コンパイルおよびトレースの完全なプロセスを示しています。testscriptという名前のファイルに保存します。

#!/bin/bash

# Define the probes
cat > prov.d <<EOF
provider myprog
{
  probe dbquery__entry(char *);
  probe dbquery__result(int);
};
EOF

# Create the C program
cat > test.c <<EOF
#include <stdio.h>
#include "prov.h"

int
main(void)
{
        char *query = "select value from table where name = 'foo'";
        /* If the dbquery-entry probe is enabled, trigger it */
        if (MYPROG_DBQUERY_ENTRY_ENABLED())
                MYPROG_DBQUERY_ENTRY(query);
        /* Pretend to run query and obtain result */
        sleep(1);
        int result = 42;
        /* If the dbquery-result probe is enabled, trigger it */
        if (MYPROG_DBQUERY_RESULT_ENABLED())
                MYPROG_DBQUERY_RESULT(result);
        return (0);
}
EOF



test.o: test.c prov.h
  gcc -c test.c

prov.o: prov.d test.o
  dtrace -G -s prov.d test.o

test: prov.o
  gcc -o test prov.o test.o
EOF

# Make the executable
make test

# Trace the program
dtrace -c ./test -qs /dev/stdin <<EOF
myprog\$target:::dbquery-entry
{
        self->ts = timestamp;
        printf("Query = %s\n", stringof(args[0]));
}

myprog\$target:::dbquery-result
{
        printf("Query time = %d microseconds; Result = %d\n",
            (timestamp - self->ts) / 1000, args[0]);
}
EOF

このスクリプトを実行した出力では、コンパイル・ステップとプログラムのトレース結果が示されます。

# chmod +x testscript
# ./testscript
dtrace -h -s prov.d
gcc -c test.c
dtrace -G -s prov.d test.o
gcc -o test prov.o test.o
Query = select value from table where name = 'foo'
Query time = 1000481 microseconds; Result = 42