これまで説明してきたDスクリプトでは、モジュール名や関数名といった単位で採取対象を特定していました。
今回は、もっと複雑な採取対象の絞り込みを可能とする、前提条件の記述方法について説明したいと思います。
特定関数の呼び出し契機での絞り込み
本連載でこれまで例示してきたDスクリプトは、特定の関数から引数や戻り値を採取する一部の例外を除いては、基本的に全ての関数フローを採取するものでした。
しかし、必ずしも全ての関数フローの採取が必要なケースばかりではありません。むしろ全ての関数フローを採取した場合、以下のようなデメリットが生じます。
- 採取結果の中から調査対象を特定するのが難しくなる
- 採取結果の記録に必要なディスク領域が増加する
このようなデメリットを無くすためには、採取範囲の絞り込みが必要です。
たとえば、リスト1のプログラム(show_nesting
)を想定してみましょう。
これまで使用してきた関数フロー採取用のDスクリプトを使用した場合、リスト2のように全ての関数フローが採取されます。
ここで、このプログラムの実行における最も重要な処理が、関数f3()
呼び出しから先の処理である、と仮定しましょう。
この前提における関数フローの採取は、関数f3()
が呼ばれた時点から開始し、関数f3()
から復帰した時点で停止する、というのが理想的と言えるでしょう。
このような採取範囲の絞り込みを行うには、リスト3のようなDスクリプトを使用します。
それでは関数フローを採取してみましょう。
リスト2と比較してみればおわかりのように、関数f3()
呼び出し以後の関数フローのみが、採取対象となっています。
Dスクリプト文法詳細
先の実行例では、まずは動かしてみることを優先しましたので、ここでは文法的な面から掘り下げてみたいと思います。
述語(前提条件)の記述
第2回における説明の際に割愛したDスクリプトの文法要素を補うと、Dスクリプトの記述形式はリスト4のようになります。
<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のようになります。
self->traced
が採取の許可を、self->suppressed
が採取の抑止を制御しています。
引数・戻り値を参照する述語の記述
述語には、関数の引数(entry
プローブ使用時)や、戻り値(return
プローブ使用時)を記述することもできます。
たとえば、コマンドXXXX
の関数xxxxx()
が非0で復帰する際に、呼び出し時点での第1引数(arg0
)を表示するには、リスト6のDスクリプトを使用します。
述語"/arg1 == 0/
"に記述されたarg1
は、pid
プロバイダのreturn
プローブにおけるarg1
を意味しますので、関数 xxxx()
の戻り値を参照することになります。
述語記述における注意点
述語(前提条件)を用いたDスクリプトを記述する際には、いくつか注意すべき事があります。
記述順序に関する注意
dtrace
コマンドは、指定されたDスクリプトの内容を先頭から順に実施します。
たとえば、範囲絞り込みを行うDスクリプトを以下のように記述したと仮定します。オリジナル(リスト3)に対して、節 (2)/(3) の順序を入れ替えています。
このスクリプトの各節は、関数f3()
の開始の際に以下のように振る舞います。
- 関数
f3()
の開始なので、self->traced
を1に設定
- 関数
f3()
の開始なので、無視
self->traced
が1なので、実施=関数フロー表示
結果として、「関数f3()
の開始」は節(3)における関数フロー採取対象となります。
その一方で、関数f3()
の終了の際に、各節は以下のように振る舞います。
- 関数
f3()
の終了なので、無視
- 関数
f3()
の終了なので、self->traced
を0に設定
self->traced
が0なので、無視
開始と異なり、「関数f3()
の終了」は節(3)における関数フロー採取対象となりません。つまりリスト7のDスクリプトを使用すると、採取の対称性が崩れてしまうのです。
もうひとつの例として、前ページのリスト5のDスクリプトも見てみましょう。
関数f4()
に対する節(1)/(5)ではentry
/return
の順序で記述されているプローブが、関数f2()
に対する節(2)/(4)では逆順で書かれていることがわかります。
これは、節(2)/(4)の順序を入れ替えてentry
/return
の順で記述した場合、節(3)による表示が実施されるよりも先に、節(4)の entry
プローブによるself->suppressed = 1
が実施されるため、関数f2()
自身がトレース採取対象から除外されてしまうのを防ぐためです。
指定の関数を採取対象に含めるか否かに応じて、上記のような順序の調整が必要になります。
再帰呼び出しに関する注意
範囲を絞り込んだ採取の際に使用したDスクリプト(リスト3)を、もう一度見てみましょう。
先述した実行例では期待通りの結果を得られましたが、実はこの記述には問題があります。
もしも、絞り込み契機となる関数f3()
が、self->traced = 1
実施後に再度呼ばれる、いわゆる再帰呼び出しを行うものと仮定した場合、複数回のf3()
呼び出しがあっても、最初の f3()
終了でself->traced = 0
が実施されてしまうため、それ以後の関数フローは採取されません。
この問題を解決するためには、関数f3()
呼び出しの入れ子状況を正しく把握する必要があります。
そこで、リスト10のようなDスクリプトを使用します。
上記スクリプトでは、以前は単なるフラグ値として扱っていたself->traced
値を、関数f3()
呼び出しの入れ子の深さを表す値として扱っています。
これにより、関数f3()
の最初の呼び出しから復帰するまでは、self->traced
値が0になることがありませんので、期待通りの関数フローを採取することができます。
なお、前ページのリスト6に関しても、対象関数が再帰呼び出しを行うケースでは正しく動作しません。ただし、これを適切に実現するには、第5回で説明する配列機能が必要となりますので、そこで改めて説明したいと思います。
次回予告
次回は、関数フローから少々趣向を変えて、DTrace の統計情報採取機能について説明したいと思います。