C/C++プログラマのためのDTrace入門

第4回前提条件の記述

これまで説明してきたDスクリプトでは、モジュール名や関数名といった単位で採取対象を特定していました。

今回は、もっと複雑な採取対象の絞り込みを可能とする、前提条件の記述方法について説明したいと思います。

特定関数の呼び出し契機での絞り込み

本連載でこれまで例示してきたDスクリプトは、特定の関数から引数や戻り値を採取する一部の例外を除いては、基本的に全ての関数フローを採取するものでした。

しかし、必ずしも全ての関数フローの採取が必要なケースばかりではありません。むしろ全ての関数フローを採取した場合、以下のようなデメリットが生じます。

  • 採取結果の中から調査対象を特定するのが難しくなる
  • 採取結果の記録に必要なディスク領域が増加する

このようなデメリットを無くすためには、採取範囲の絞り込みが必要です。

たとえば、リスト1のプログラムshow_nestingを想定してみましょう。

リスト1 関数の入れ子show_nesting
void f1(){ }

void f2(){ f1(); }

void f3(){ f2(); }

void f4(){ f3(); }

int
main(int argc,
     const char* argv[])
{
    f4();
    return 0;
}

これまで使用してきた関数フロー採取用のDスクリプトを使用した場合、リスト2のように全ての関数フローが採取されます。

リスト2 全関数フローの採取結果
CPU FUNCTION
  0  -> _start
  0    -> __fsr
  0    <- __fsr
  0    -> main
  0      -> f4
  0        -> f3
  0          -> f2
  0            -> f1
  0            <- f1
  0          <- f2
  0        <- f3
  0      <- f4
  0   <- main

ここで、このプログラムの実行における最も重要な処理が、関数f3()呼び出しから先の処理である、と仮定しましょう。

この前提における関数フローの採取は、関数f3()が呼ばれた時点から開始し、関数f3()から復帰した時点で停止する、というのが理想的と言えるでしょう。

このような採取範囲の絞り込みを行うには、リスト3のようなDスクリプトを使用します。

リスト3 採取範囲の絞り込みwatch_focused.d
pid$target:show_nesting:f3:entry
{
    self->traced = 1;
}

pid$target:show_nesting::entry,
pid$target:show_nesting::return
/self->traced/
{
}

pid$target:show_nesting:f3:return
{
    self->traced = 0;
}

それでは関数フローを採取してみましょう。

図1 caption
$ dtrace -F \
         -s watch_focused.d \
         -c ./show_nesting
dtrace: script 'watch_focused.d' matched 15 probes
CPU FUNCTION
  0  -> f3
  0    -> f2
  0      -> f1
  0      <- f1
  0    <- f2
  0  <- f3
dtrace: pid 12820 has exited
$

リスト2と比較してみればおわかりのように、関数f3()呼び出し以後の関数フローのみが、採取対象となっています。

Dスクリプト文法詳細

先の実行例では、まずは動かしてみることを優先しましたので、ここでは文法的な面から掘り下げてみたいと思います。

述語(前提条件)の記述

第2回における説明の際に割愛したDスクリプトの文法要素を補うと、Dスクリプトの記述形式はリスト4のようになります。

リスト4 Dスクリプト文法
<Probe-Description> [, ....]
[/<Predicate>/]
{
    [<Action> ....]
}

<Predicate>は一般に「述語」と呼ばれ、⁠前提条件」のことを意味します。

<Predicate> 部分には、条件判定のための式が記述されます。式の値が非ゼロであれば、C/C++と同様に条件成立とみなされます。

<Predicate>部分を記述する場合、スラッシュ("/")で囲む必要があります。<Predicate>が省略された場合は、常に<Action>が実施されます。

Dスクリプトでは、この一連のまとまりのことを(clause)と呼びます。

selfを使用したデータ保持

先ほどのDスクリプトには、"self->traced"という記述がありましたが、これは初めて目にするものです。

この記述は、スレッド固有変数と呼ばれる記憶領域の作成/参照を行うためのものです。

Dスクリプトにおける変数をそれぞれの通用範囲の点から見た場合、以下に示す3種類に分類されます。

- 大域変数(global varible)
"VarName" 形式
- 節固有変数(clause local variable)
"this->VarName" 形式
- スレッド固有変数(thread local variable)
"self->VarName" 形式

「大域変数」はここで初めて出てきましたが、C/C++プログラムで言うところの大域変数と同じと思って構いません。

大域変数および節固有変数は、同一Dスクリプト内の任意の時点で参照可能です(※1⁠⁠。

但し、大域変数は暗黙の初期値として0が設定されるため、値が未設定の状態でも参照可能ですが、節固有変数を値が未設定の状態で参照した場合は、Dスクリプトの実行時エラーとなります。

スレッド固有変数に設定した値は、同じスレッド上から参照した場合のみ、設定した値を得ることができます。別のスレッドから参照した場合、事前に当該スレッドで値の参照がされていればその値が、そうでなければ 0(NULL)値が得られます。

大域変数や節固有変数は、複数スレッド間で共有されてしまいますので、対象プロセスがマルチスレッド稼動している場合、これらを一時退避等の用途で使用すると、想定外の挙動となる可能性(※2)がありますので注意してください。

複数条件の組み合わせ

<Predicate> 部分はC/C++ 等での条件記述と同様に、"&&"や"||"を用いて複数の式を列挙することが可能です。

複数条件記述が使えることで、たとえば以下の様な採取範囲の絞り込みを行うことができます。

  • 関数f4()呼び出し以後のフローを採取
  • ただし、関数f2呼び出し以後のフローは不要

このような絞り込みを行うDスクリプトは、リスト5のようになります。

リスト5 抑止付きの絞り込み
pid$target:show_nesting:f4:entry
{
    self->traced = 1;
}

pid$target:show_nesting:f2:return
/self->traced/
{
    self->suppressed = 0;
}

pid$target:show_nesting::entry,
pid$target:show_nesting::return
/self->traced && !self->suppressed/
{
}

pid$target:show_nesting:f2:entry
/self->traced/
{
    self->suppressed = 1;
}

pid$target:show_nesting:f4:return
{
    self->traced = 0;
}

self->tracedが採取の許可を、self->suppressedが採取の抑止を制御しています。

引数・戻り値を参照する述語の記述

述語には、関数の引数entryプローブ使用時)や、戻り値returnプローブ使用時)を記述することもできます。

たとえば、コマンドXXXXの関数xxxxx()が非0で復帰する際に、呼び出し時点での第1引数arg0を表示するには、リスト6のDスクリプトを使用します。

リスト6 戻り値を用いた述語記述
pid$target:XXXX:xxxx:entry
{
    self->arg0 = arg0;
}

pid$target:XXXX:xxxx:return
/arg1 != 0/
{
    printf("arg0=%p", self->arg0);
}

述語"/arg1 == 0/"に記述されたarg1は、pidプロバイダのreturnプローブにおけるarg1を意味しますので、関数 xxxx() の戻り値を参照することになります。

述語記述における注意点

述語(前提条件)を用いたDスクリプトを記述する際には、いくつか注意すべき事があります。

記述順序に関する注意

dtraceコマンドは、指定されたDスクリプトの内容を先頭から順に実施します。

たとえば、範囲絞り込みを行うDスクリプトを以下のように記述したと仮定します。オリジナル(リスト3)に対して、節 (2)/(3) の順序を入れ替えています。

リスト7 記述順序の入れ替え
/* 節 (1) */
pid$target:show_nesting:f3:entry
{
    self->traced = 1;
}

/* 節 (2) */
pid$target:show_nesting:f3:return
{
    self->traced = 0;
}

/* 節 (3) */
pid$target:show_nesting::entry,
pid$target:show_nesting::return
/self->traced/
{
}

このスクリプトの各節は、関数f3()の開始の際に以下のように振る舞います。

  1. 関数f3()の開始なので、self->tracedを1に設定
  2. 関数 f3() の開始なので、無視
  3. self->traced が1なので、実施=関数フロー表示

結果として、⁠関数f3()の開始」は節(3)における関数フロー採取対象となります。

その一方で、関数f3()の終了の際に、各節は以下のように振る舞います。

  1. 関数f3()の終了なので、無視
  2. 関数f3()の終了なので、self->tracedを0に設定
  3. self->traced が0なので、無視

開始と異なり、⁠関数f3()の終了」は節(3)における関数フロー採取対象となりません。つまりリスト7のDスクリプトを使用すると、採取の対称性が崩れてしまうのです。

もうひとつの例として、前ページのリスト5のDスクリプトも見てみましょう。

リスト8 抑止付きの絞り込み(リスト6の再掲)
/* 節 (1) */
pid$target:show_nesting:f4:entry
{
    self->traced = 1;
}

/* 節 (2) */
pid$target:show_nesting:f2:return
/self->traced/
{
    self->suppressed = 0;
}

/* 節 (3) */
pid$target:show_nesting::entry,
pid$target:show_nesting::return
/self->traced && !self->suppressed/
{
}

/* 節 (4) */
pid$target:show_nesting:f2:entry
/self->traced/
{
    self->suppressed = 1;
}

/* 節 (5) */
pid$target:show_nesting:f4:return
{
    self->traced = 0;
}

関数f4()に対する節(1)/(5)ではentry/returnの順序で記述されているプローブが、関数f2()に対する節(2)/(4)では逆順で書かれていることがわかります。

これは、節(2)/(4)の順序を入れ替えてentry/returnの順で記述した場合、節(3)による表示が実施されるよりも先に、節(4)の entryプローブによるself->suppressed = 1が実施されるため、関数f2()自身がトレース採取対象から除外されてしまうのを防ぐためです。

指定の関数を採取対象に含めるか否かに応じて、上記のような順序の調整が必要になります。

再帰呼び出しに関する注意

範囲を絞り込んだ採取の際に使用したDスクリプト(リスト3)を、もう一度見てみましょう。

リスト9 採取範囲の絞り込み(再掲)
pid$target:show_nesting:f3:entry
{
    self->traced = 1;
}

pid$target:show_nesting::entry,
pid$target:show_nesting::return
/self->traced/
{
}

pid$target:show_nesting:f3:return
{
    self->traced = 0;
}

先述した実行例では期待通りの結果を得られましたが、実はこの記述には問題があります。

もしも、絞り込み契機となる関数f3()が、self->traced = 1実施後に再度呼ばれる、いわゆる再帰呼び出しを行うものと仮定した場合、複数回のf3()呼び出しがあっても、最初の f3()終了でself->traced = 0が実施されてしまうため、それ以後の関数フローは採取されません。

この問題を解決するためには、関数f3()呼び出しの入れ子状況を正しく把握する必要があります。

そこで、リスト10のようなDスクリプトを使用します。

リスト10 再帰に対応した絞り込み
pid$target:show_nesting:f3:entry
{
    self->traced += 1;
}

pid$target:show_nesting::entry,
pid$target:show_nesting::return
/self->traced/
{
}

pid$target:show_nesting:f3:return
{
    self->traced -= 1;
}

上記スクリプトでは、以前は単なるフラグ値として扱っていたself->traced値を、関数f3()呼び出しの入れ子の深さを表す値として扱っています。

これにより、関数f3()の最初の呼び出しから復帰するまでは、self->traced値が0になることがありませんので、期待通りの関数フローを採取することができます。

なお、前ページのリスト6に関しても、対象関数が再帰呼び出しを行うケースでは正しく動作しません。ただし、これを適切に実現するには、第5回で説明する配列機能が必要となりますので、そこで改めて説明したいと思います。

次回予告

次回は、関数フローから少々趣向を変えて、DTrace の統計情報採取機能について説明したいと思います。

おすすめ記事

記事・ニュース一覧