以下のような場合に、プローブポイントをコードに挿入します。
プログラムの変数値をトレースする。
エクスポートされたインタフェースからは得ることができない内部状態の情報を提供する。これは、デバッグやパフォーマンス分析を行う場合に役立ちます。
たとえば、プローブポイントを使用して、C++ の private クラスに隠されているパフォーマンス関連の情報を表示したり、ハッシュテーブル内の衝突率などの処理データを表示できます。ハッシュテーブルのコード中にプローブポイントを配置しておくと、衝突が検出されるたびにそのプローブポイントはトレースファイルに書き込みを行うことができます。
プローブを挿入するためのインタフェースは、TNF_PROBE_0 から TNF_PROBE_5 までの TNF_PROBE マクロによって定義されます。0 〜 5 の数字は、マクロによってトレースされる変数の数です。
これらのマクロを使用して、コードの任意の場所にプローブポイントを挿入することによって、変数の値を取得したり、プログラムの実行をトレースすることができます。libtnfprobe ライブラリでは標準のスカラー型 (int、long、float など) が定義されていますが、TNF_DECLARE_RECORD マクロと TNF_DEFINE_RECORD マクロを使用すると、さらに複雑な構造を定義することもできます。「プローブポイントのユーザー定義型」を参照してください。
単純な例として、TNF_PROBE_0 を以下に示します。このマクロには、引数の型は指定しません。
TNF_PROBE_0 (name, keys, detail);
各変数について説明します。
name - プローブの名前。この名前は、ANSI C の識別子の構文規則にすべて従います。name は、使用したときに宣言されるので、あらかじめ別に宣言する必要はありません。この宣言はブロックスコープ宣言なので、プログラムの名前空間には影響を与えません。
keys - プローブが属するグループのリスト。このリスト中には複数のキーワードが含まれており、各キーワードは空白文字で区切られています。このリストの中では、セミコロン (;)、等号 (=)、単一引用符 (') は使用できません。いずれかのグループが有効になると、そのプローブポイントも有効になります。keys を変数にすることはできません。keys はインライン文字列にしてください。
detail - 独自の属性と値を定義する手段を提供します。detail 文字列には、複数の組の属性と値が含まれており、各組はセミコロンで区切られています。ただし、この値は省略可能です。最初のワード (空白文字まで) は属性、残りの文字列 (セミコロンまで) は値とみなされます。区切り文字のセミコロンの前後には、スペースを入れることができます。detail 文中では単一引用符と等号は使用できません。
属性名の前にベンダー固有の記号と % 記号を付けて、名前の衝突を避けてください。以下の例では、 sunw%debug、comX%exception、comY%func_entry、comY%color という 4 つの属性が定義されています。prex は空白文字の値をトークン化するので、複数の語から成る値の場合はいずれかの語で照合できますが、文字列全体で照合することはできません。たとえば、以下のような例が考えられます。
sunw%debug entering function A; comX%exception no file; comY%func_entry; comY%color red blue
上記のコマンドで照合される値を表 1-8 に示します。
表 1-8 ユーザー定義属性の例
属性 |
値 |
prex が照合する値 |
---|---|---|
sunw%debug |
entering function A |
entering または function または A |
comX%exception |
no file |
no または file |
comY%func_entry |
|
/.*/ (正規表現) |
comY%color |
red blue |
red または blue |
libtnfprobe は、ベンダー記号が前に付いていない属性名をすべて予約します (つまり、名前に % 記号が含まれていない属性をすべて予約します)。「C プログラムのサンプル」の cookie.c では、以下の TNF_PROBE_0 を使用しています。
TNF_PROBE_0(start, "cookie main", "sunw%debug starting main");
プリプロセッサオプションの -DNPROBE (cc(1) のマニュアルページを参照) を使用してコンパイルする場合、またはプリプロセッサ制御文 #include <tnf/probe.h> の前に #define NPROBE を置いてコンパイルする場合には、プローブポイントと TNF の型拡張コードがプログラムにコンパイルされないようにしてください。
引数の名前に含まれている数字 1 〜 5 は、プローブポイントに指定する変数の数を表します。たとえば、TNF_PROBE_1 の構文は次のようになっています。
TNF_PROBE_1(name, keys, detail, arg_type_1, arg_name_1, arg_value_1);
また、TNF_PROBE_5 の構文は次のようになっています。
TNF_PROBE_5(name, keys, detail, arg_type_1, arg_name_1, arg_value_1 arg_type_2, arg_name_2, arg_value_2 arg_type_3, arg_name_3, arg_value_3 arg_type_4, arg_name_4, arg_value_4 arg_type_5, arg_name_5, arg_value_5);
以下、各引数について説明します。
arg_type_n − n 番目の引数の型です。n には 1 〜 5 の数字が入ります。定義済みの型を表 1-9 に示します。ユーザー独自の型の定義については、「プローブポイントのユーザー定義型」 を参照してください。
arg_name_n − n 番目の引数に与える名前です。 ANSI C の識別子に関する規則に従ってください。また、引数の名前は、引用符で囲まないでください。(「属性」で取り上げた slots 属性には、この名前の文字列の場合が含まれています。)
arg_value_n − トレースファイルに含まれている値に対して評価される式です。value_n に含まれている任意の変数に対して読み取り (アクセス) を行うことができます。マルチスレッドのプログラムで、読み取りを禁止する必要があるデータが value_n に含まれている場合には、マクロ TNF_PROBE_n の前後にロックを配置してください。
型 |
対応する C 言語の型と意味 |
---|---|
tnf_long |
int、long |
tnf_ulong |
unsigned int、unsigned long |
tnf_longlong |
long long (コンパイラに実装されている場合) |
tnf_ulonglong |
unsigned long long (コンパイラに実装されている場合) |
tnf_float |
float |
tnf_double |
double |
tnf_string |
char * |
tnf_opaque |
void * |
たとえば、「C プログラムのサンプル」の cookie.c では、以下のように TNF_PROBE_2 を使用しています。
TNF_PROBE_2(inloop, "cookie main loop","sunw%debug in the loop", tnf_long, loop_count, i, tnf_long, total_iterations, sum);
cookie.c のマクロ定義の一部について、表 1-10 で説明します。
表 1-10 cookie.c の TNF マクロ定義
TNF_PROBE_0 ( |
引数の型が指定されていないプローブ |
start, |
プローブの名前 |
"cookie main", |
プローブが cookie と main (keys 属性の値) に属しているグループのリスト |
"sunw%debug starting main"); |
ユーザー定義の属性 = sunw%debug、値 = starting main (デバッグプローブ関数で使用されます) |
TNF_PROBE_2 ( |
2 つの変数を持ったプローブ |
inloop, |
プローブの名前 |
"cookie main loop", |
keys - cookie、main、loop |
"sunw%debug in the loop", |
デバッグプローブ関数の値 |
tnf_long, |
最初の変数の型 |
loop_count, |
最初の変数の名前 (slots 属性の値) |
i, |
最初の変数 |
tnf_long, |
2 番目の変数の型 |
total_iterations, |
2 番目の変数の名前 (slots 属性の値) |
sum); |
2 番目の変数 |
""); |
例 1-4 では、関数の入口と出口にプローブポイントを配置して、関数内で費やされる時間を測定しています。関数の入口に配置されたプローブは、その関数への引数も記録します。
prex は、トレースが許可されているプローブポイントをプログラム実行時に検出すると、トレースファイルに記録を書き込みます。各プローブポイントは、検出された時刻を記録し、ファイル名、行番号、name、keys などのプローブポイントの詳細情報が含まれたタグ記録を参照します。これらのタグの記録は、トレースファイルに 1 度だけ書き込まれ、上書きされることはありません。
以下のコード例において、最初のプローブポイント work_args は、そのプローブポイントの 2 つの変数値 (state と message) も記録します。
#include <tnf/probe.h> int work(int state, char *message) { TNF_PROBE_2(work_start, "work_module work" "sunw%debug in function work", tnf_long, int_input, state, tnf_string, string_input, message); ... TNF_PROBE_0(work_end, "work_module work", ""); }
プログラムの構造体をトレースするには、TNF_DECLARE_RECORD マクロと TNF_DEFINE_RECORD_n マクロを使用して新しい型を定義します。これらのマクロは、プローブポイントに渡される型を拡張するためのコンパイル時インタフェースの一部です。
TNF_DECLARE_RECORD(c_type, tnf_type);
TNF_DEFINE_RECORD_1(c_type, tnf_type, tnf_member_type_1, tnf_member_name_1) TNF_DEFINE_RECORD_2(c_type, tnf_type, tnf_member_type_1, tnf_member_name_1, tnf_member_type_2, tnf_member_name_2) TNF_DEFINE_RECORD_3(c_type, tnf_type, tnf_member_type_1, tnf_member_name_1, tnf_member_type_2, tnf_member_name_2, tnf_member_type_3, tnf_member_name_3) TNF_DEFINE_RECORD_4(c_type, tnf_type, tnf_member_type_1, tnf_member_name_1, tnf_member_type_2, tnf_member_name_2, tnf_member_type_3, tnf_member_name_3, tnf_member_type_4, tnf_member_name_4) TNF_DEFINE_RECORD_5(c_type, tnf_type, tnf_member_type_1, tnf_member_name_1, tnf_member_type_2, tnf_member_name_2, tnf_member_type_3, tnf_member_name_3, tnf_member_type_4, tnf_member_name_4, tnf_member_type_5, tnf_member_name_5)
TNF_DECLARE_RECORD と TNF_DEFINE_RECORD は、新しく定義する型ごとに 1 つだけ作成してください。TNF_DECLARE_RECORD は、TNF_DEFINE_RECORD よりも前に置く必要があります。定義されている tnf_type を複数のソースファイルで使用する必要がある場合は、その複数のファイルで共有するヘッダーファイル中に TNF_DECLARE_RECORD を宣言することができます。TNF_DEFINE_RECORD は、いずれか 1 つのソースファイルだけに作成する必要があります。
TNF_DEFINE_RECORD マクロインタフェースは、1 つの関数といくつかのデータ構造体を定義します。したがってこのインタフェースは、関数の内部ではなく、ソースファイル (.c ファイルまたは .cc ファイル) のファイルスコープ (有効範囲) 内で使用してください。
TNF_DEFINE_RECORD 文の後にセミコロンを置かないでください。セミコロンを置くと、コンパイル時に警告が出力されます。
以下、各変数について説明します。
c_type − 新しい tnf_type を作成するためのテンプレートです。C 構造体のすべての要素を、定義する TNF 型で使用する必要はありません。c_type は、C の構造体型にしてください。
tnf_type − 新しく作成した型に与えられる名前です。このインタフェースは、tnf_type を接頭辞とした名前空間を使用します。したがって、ライブラリが xxx_type という新しい型を定義した場合には、そのライブラリは、xxx_type を接頭辞に使用して別のシンボルを定義することはできません。
型の名前空間の管理方法は、ライブラリ内のその他の名前空間の管理方法と同じです。つまり、ライブラリ内の型以外のシンボルが使用している固有の接頭辞を新しい TNF 型の名前の接頭辞としてください。これによって、新しい TNF 型が定義されている複数のライブラリをリンクしても、名前空間が衝突することはなくなります。
たとえば、libpalloc.so というライブラリが名前の接頭辞として pal を使用してシンボルを定義している場合には、この libpalloc.so は、新しく定義する TNF 型の名前にも pal を接頭辞として使用する必要があります。
tnf_member_type_n − C 構造体の n 番目のメンバーの TNF 型です。
tnf_member_name_n − C 構造体の n 番目のメンバーの名前です。
新しい TNF 型の定義方法と、プローブ内でのその TNF 型の使い方を例 1-5 に示します。
この例 1-5 は、すべてのシンボルの接頭辞に pal を使用している架空のライブラリ libpalloc.so の一部であると仮定しています。
#include <tnf/probe.h> typedef struct pal_header { long size; char * descriptor; struct pal_header *next; } pal_header_t; TNF_DECLARE_RECORD(pal_header_t, pal_tnf_header); TNF_DEFINE_RECORD_2(pal_header_t, pal_tnf_header, tnf_long, size, tnf_string, descriptor) /* * 注 : 接頭辞 pal_tnf_header が付いている名前空間は、 * このクライアントではもう使用できない。 */ void pal_free(pal_header_t *header_p) { int state; TNF_PROBE_2(pal_free_start, "palloc pal_free", "sunw%debug entering pal_free", tnf_long, state_var, state, pal_tnf_header, header_var, header_p); . . . }
next フィールドを使用して自分自身を指し示す構造体 (リンクされたリスト) 中などで、tnf_type を再帰的または相互再帰的に定義することができます。
このような構造体が TNF_PROBE に渡されると、リンクされたリストの全体がトレースファイルに記録されます (next フィールドが NULL になるまで続きます)。ただし、そのリストが循環リストの場合には、処理が無限ループに入ります。この無限ループを中断するには、tnf_type から next フィールドを削除するか、または next メンバーの型を tnf_opaque として定義します。
頻繁に使用される mutex ロックのような、頻繁に通過するコード内のセクションには、プローブポイントを配置しないでください。
SPARCStation10 の場合、各プローブが使用するワーキングセットメモリーは約 30 ワード (データが 10 ワード、テキストが 20 ワード)、無効な各プローブで費やされる時間は約 200 ns (ナノ秒) です。アプリケーションのパフォーマンスは、配置するプローブの数を調整することによって改善できます。
プローブポイントを配置したライブラリを出荷する場合は、ベンチマークを実行して、パフォーマンスが著しく低下していないことを確認してください。パフォーマンスを改善するにはプローブの数を減らすか、プローブの配置を変更してください。
dbx、truss、および prex は、/proc を使用してターゲットプロセスを制御します。/proc では、1 つのクライアントだけが安全にターゲットプロセスを制御できます。このため、dbx や prex のようなユーティリティを同じターゲットプログラム上で同時に実行することはできません。dbx または truss が実行されているターゲットプログラム上で prex を実行しようとすると、prex は「cannot attach to target (ターゲットに接続できません)」というメッセージを表示します。
ただし、以下の手順によって prex と dbx を交互に実行できます。
prex を起動します。
プローブポイントの状態を設定します。
quit suspend コマンドを実行します。
dbx を起動します。
中断されているプログラムに接続します。
ターゲットは、prex と dbx 以外のコードは実行しません。
ターゲットに SIGSTOP シグナルを送ってそのターゲットを一時停止させてから、"quit resume" を prex に入力することもできます。これを行なった場合には、停止されたプロセス上で dbx を呼び出してから、SIGCONT シグナルを送る必要があります (そうしない場合、dbx がハングします)。
dlopen(3X) によって取り込まれた共有オブジェクト内のプローブは、prex のコマンド履歴に従って自動的に設定されます。dlclose(3X) によって共有オブジェクトが削除されると、prex はターゲットプログラム内のプローブの情報をリフレッシュします。これは、dlopen と dlclose に必要な作業がほかにもあるということを意味しています。したがって、dlopen と dlclose の所要時間は多少長くなります。
この機能を必要とせず、また dlopen と dlclose の処理に影響を与えたくない場合は、ターゲットから prex を分離してください。
prex は、ターゲットプログラムに直接送られるシグナルを妨げません。ただし、prex は Control-c (SIGINT) や Control-z (SIGSTOP) などの端末から生成されたシグナルをすべて受信しますが、それをターゲットプログラムには転送しません。
ターゲットプログラムにシグナルを送るには、シェルから kill(1) コマンドを実行してください。
トレースファイルにイベントを書き込む際には、システムコール障害などの障害がいくつか発生する可能性があります。これらの障害が発生すると、ターゲットプロセス内で障害コードが設定されます。ターゲットプロセスは正常に実行を継続しますが、トレース記録は書き込まれません。
Control-c を prex に入力して prex プロンプトを表示させると、prex はターゲット内の障害コードを検査して、トレース障害が発生したかどうかを通知します。
プログラムが fork() を実行した場合には、子プロセスが検出したプローブはすべて同じトレースファイルに記録されます。イベントにはプロセス ID 付きで記録されるので、そのイベントがどのプロセスから発生したのかを判定できます。
マルチスレッドのプログラムで、あるスレッドが fork を行なったときに他のスレッドが実行されていると、競合状態が発生することがあります。トレースファイルが破損しないようにするには、fork を実行するときに他のスレッドが停止しているようにするか、または fork1(2) を使用してください。
ターゲットプログラム自身 (ターゲットプログラムがフォークした子プロセスではない) が exec(2) を実行した場合には、prex がターゲットから分離して終了します。ユーザーは、以下のコマンドを使用して、prex を再接続することができます。
$ prex -p pid