DTrace では、個々のオペレーティングシステムスレッドに固有の変数記憶域を宣言できます。こうしたスレッド固有変数は、以前に説明した大域変数とは対照的な機能を備えています。スレッド固有変数は、プローブを有効にし、有効にしたプローブを起動するすべてのスレッドにタグやその他のデータでマークを付けたい場合に便利です。D では、このようなプログラムを簡単に作成できます。というのも、スレッド固有変数は、D コード内で共通の名前を使用して、個々のスレッドに関連付けられた別々のデータ記憶域を参照できるからです。スレッド固有変数を参照するには、特殊な識別子 self に、演算子 -> を適用します。
syscall::read:entry { self->read = 1; }
この D コードの抜粋では、read(2) システムコールの呼び出し時にプローブが有効になり、このプローブを起動する各スレッドに、read という名前のスレッド固有変数が関連付けられます。スレッド固有変数は、大域変数と同じく、最初の割り当て時に自動的に作成されます。また、最初の代入文の右式で使用されている型 (この例では int) が適用されます。
D プログラム内で変数 self->read が参照されるたびに、対応する DTrace プローブが起動したときに実行されていたオペレーティングシステムスレッドに関連付けられたデータオブジェクトが参照されます。スレッド固有変数は、システム内のスレッドの識別情報を示す組によって暗黙的なインデックスが付けられた、連想配列と見なすことができます。スレッドの識別情報は、システムが実行している間は変わりません。 あるスレッドを終了したあと、同じオペレーティングシステムのデータ構造で新しいスレッドを生成した場合、DTrace スレッド固有記憶域の識別情報は再利用されません。
スレッド固有変数の定義後は、この変数を使って、システム内の任意のスレッドを参照できます。これは、この変数が特定のスレッドに割り当てられていない場合も同様です。スレッド固有変数のスレッドのコピーがまだ割り当てられていない場合は、コピーのデータ記憶域にゼロが格納されて定義されます。連想配列要素の場合と同じく、スレッド固有変数に配下の記憶域を割り当てるには、この変数にゼロ以外の値を割り当てる必要があります。また、連想配列要素の場合と同じく、スレッド固有変数にゼロを割り当てると、DTrace により、配下の記憶域の割り当てが解除されます。使用していないスレッド固有変数には、常にゼロを割り当ててください。スレッド固有変数に割り当てる動的な変数空間を微調整するその他のテクニックについては、第 16 章オプションとチューニング可能パラメータを参照してください。
D プログラムでは、連想配列をはじめとする任意の型のスレッド固有変数を定義できます。以下に、スレッド固有変数の定義例を示します。
self->x = 123; /* integer value */ self->s = "hello"; /* string value */ self->a[123, 'a'] = 456; /* associative array */
スレッド固有変数は、ほかの D 変数の場合と同じく、明示的変数宣言なしで使用できます。あえて宣言を作成する場合は、キーワード self を前に付けて、プログラム節の外側に配置します。
self int x; /* declare int x as a thread-local variable */ syscall::read:entry { self->x = 123; }
スレッド固有変数は、大域変数とは別の名前空間に格納されるので、変数名を再利用できます。プログラム内で名前を多重定義した場合、x と self->x はそれぞれ別の変数になります。以下に、スレッド固有変数の使用例を示します。テキストエディタで以下のプログラムを入力し、rtime.d という名前のファイルに保存してください。
syscall::read:entry { self->t = timestamp; } syscall::read:return /self->t != 0/ { printf("%d/%d spent %d nsecs in read(2)\n", pid, tid, timestamp - self->t); /* * We're done with this thread-local variable; assign zero to it to * allow the DTrace runtime to reclaim the underlying storage. */ self->t = 0; }
では、シェルに戻ってプログラムを実行しましょう。しばらくすると、結果が出力されます。何も出力されない場合は、いくつかのコマンドを実行してみてください。
# dtrace -q -s rtime.d 100480/1 spent 11898 nsecs in read(2) 100441/1 spent 6742 nsecs in read(2) 100480/1 spent 4619 nsecs in read(2) 100452/1 spent 19560 nsecs in read(2) 100452/1 spent 3648 nsecs in read(2) 100441/1 spent 6645 nsecs in read(2) 100452/1 spent 5168 nsecs in read(2) 100452/1 spent 20329 nsecs in read(2) 100452/1 spent 3596 nsecs in read(2) ... ^C # |
rtime.d は、スレッド固有変数 t を使って、スレッドが read(2) に入ったときのタイムスタンプを捕捉します。続いて、戻り節で、現在のタイムスタンプと self->t の差分が計算され、read(2) の実行が開始されてからの経過時間が出力されます。組み込み D 変数 pid と tid は、read(2) の実行スレッドのプロセス ID とスレッド ID を報告します。この情報を得たあと、不要になった self->t には 0 が割り当てられます。このため、DTrace は、t に関連付けられた配下の記憶域を現在のスレッドで再利用できます。
通常、read(2) は、サーバープロセスとデーモンによってバックグラウンドで実行されているので、ユーザーが操作を停止している間も出力行が続々と表示されていきます。execname 変数を使って read(2) の実行プロセス名を出力し、よりわかりやすい情報を得たい場合は、rtime.d の 2 番目の節を次のように変更します。
printf("%s/%d spent %d nsecs in read(2)\n", execname, tid, timestamp - self->t);
調べたいプロセスが見つかったら、述語を追加して、read(2) の動作を詳しく調べることができます。
syscall::read:entry /execname == "Xsun"/ { self->t = timestamp; }