Solaris 動的トレースガイド

第 3 章 変数

D のトレースプログラムでは、使用できる基本型の変数が 2 種類あります。 スカラー変数と連想配列です。第 1 章の例では、これらの変数の使用方法の概要を説明しました。この章では、D 変数の規則の詳細と、変数をさまざまなスコープに関連付ける方法について説明します。「集積体」と呼ばれる特殊な配列変数については、第 9 章集積体で説明します。

スカラー変数

スカラー変数では、整数やポインタなど、個々の固定サイズのデータオブジェクトを表現できます。スカラー変数では、1 つ以上のプリミティブ型や複合型で構成された固定サイズのオブジェクトも表現できます。D では、オブジェクトの配列と複合構造体の両方を作成できます。DTrace では、事前定義された最大長まで文字列を伸ばすことによって、これを固定サイズのスカラーとして表現することもできます。D プログラム内の文字列の長さの制御については、第 6 章文字列で詳しく説明します。

スカラー変数は、D プログラム内の未定義の識別子に初めて値を割り当てたときに、自動的に作成されます。たとえば、int 型のスカラー変数 x は、任意のプローブ節内で int 型の値を割り当てるだけで作成できます。

BEGIN
{
	x = 123;
}

このようにして作成されたスカラー変数は、「大域的な」変数です。 この変数の名前とデータ格納場所は、1 度定義されると、D プログラムのすべての節で認識されます。識別子 x を参照するたびに、この変数に関連付けられた単一の格納場所が参照されます。

D では、ANSI-C の場合とは違って、明示的変数宣言は必要ありません。大域変数を使用する前に、その名前と型を明示的に割り当てて、大域変数を宣言したい場合は、次の例のように、プログラム内のプローブ節の外側に宣言を挿入します。ほとんどの D プログラムは明示的変数宣言を必要としませんが、明示的変数宣言が便利な場合もあります。たとえば、変数の型を細かく制御したい場合や、プログラムの冒頭に一連の宣言とコメントを挿入して、プログラム内で使用する変数やその意味を記述したい場合などです。

int x; /* declare an integer x for later use */

BEGIN
{
	x = 123;
	...
}

D の変数宣言では、ANSI-C 宣言の場合とは違って、初期値の割り当てを行いません。初期値の割り当てには必ず、BEGIN プローブ節を使用します。すべての大域変数の記憶域には、最初にこの変数を参照する前に、DTrace によってゼロが格納されます。

D 言語定義では、D 変数のサイズと数は制限されていません。ただし、DTrace 実装とシステムの使用可能なメモリー容量による制限が存在します。こうした制限のうちプログラムのコンパイル時に適用可能なものは、D コンパイラによって適用されます。プログラムの制限関連のオプションの設定方法については、第 16 章オプションとチューニング可能パラメータで詳しく説明します。

連想配列

連想配列では、「キー」と呼ばれる名前を指定して取得できるデータ要素の集積を表現できます。D の連想配列のキーは、「」と呼ばれるスカラー式の値のリストで構成されます。配列の組は、配列の参照時に対応する配列値を取得するために呼び出される関数のパラメータリストのようなものです。D の連想配列には、それぞれ決まった型の、決まった数の組要素で構成される、固定の「キー署名」が 1 つずつ割り当てられています。D プログラムでは、配列ごとに異なるキー署名を定義できます。

連想配列は、通常の固定サイズの配列とは異なり、要素数があらかじめ制限されていません。単に整数をキーとして使用するのではなく、任意の組で要素にインデックスを付けることができます。また、要素は、事前に割り当てられた連続した記憶域に格納されるわけではありません。連想配列は、C、C++、JavaTM 言語プログラム内の、ハッシュテーブルなどの単純な辞書データ構造を使用する場合に便利です。連想配列を使用すると、D プログラム内で捕捉したイベントや状態の動的な履歴を作成し、より複雑な制御フローを実現できます。

連想配列を定義するには、次のような形式の代入式を記述します。

name [ key ] = expression ;

name は任意の有効な D 識別子、key は 1 つ以上の式をコンマで区切った形式のリストです。たとえば、以下の式は、キー署名 [ int, string ] を持つ連想配列 a を定義し、[ 123, "hello" ] という組で指定された場所に整数値 456 を格納します。

a[123, "hello"] = 456;

配列内の各オブジェクトの型は、その配列内のすべての要素に共通の型で、固定されています。たとえば、a には最初に整数 456 が割り当てられているので、その後この配列に格納される値はすべて int 型になります。連想配列の要素を変更するときは、第 2 章で定義した代入演算子を、それぞれに定義されたオペランド規則に従って使用します。非互換な割り当てがあると、D コンパイラからエラーメッセージが返されます。連想配列のキーや値には、スカラー変数で使用できる任意の型を使用できます。連想配列をキーまたは値として、ほかの連想配列内に入れ子にすることはできません。

連想配列は、配列キー署名と互換性のある任意の組を使って参照できます。組の互換性の規則は、関数呼び出しと変数割り当ての互換性の規則に似ています。 同じ長さの組を使用する必要があります。また、実パラメータリスト内の型と、正規のキー署名内の対応する型との間には、互換性がなければなりません。たとえば、連想配列 x を次のように定義したとします。

x[123ull] = 0;

この場合、キー署名は unsigned long long 型、値は int 型になります。この配列は、式 x['a'] を使って参照することもできます。これは、「型変換」で説明した算術変換規則により、長さが 1 の int 型の文字定数 'a' で構成された組と、unsigned long long 型のキー署名に互換性があるからです。

D の連想配列を明示的に宣言してから使用する必要がある場合は、プログラムソースコードのプローブ節の外側に、配列名とキー署名の宣言を記述します。

int x[unsigned long long, char];

BEGIN
{
	x[123ull, 'a'] = 456;
}

連想配列の定義後は、互換性のあるキー署名を持つ組がすべて参照可能になります。まだ割り当てられていない組も参照できます。まだ割り当てられていない連想配列要素にアクセスすると、定義により、ゼロが格納されたオブジェクトが返されます。この定義によると、連想配列要素に配下の記憶域を割り当てるためには、この要素にゼロ以外の値を割り当てる必要があります。反対に、連想配列要素にゼロを割り当てると、DTrace により、配下の記憶域の割り当てが解除されます。この動作は重要な意味を持っています。なぜなら、連想配列要素に割り当てられる動的な変数空間には限りがあるからです。割り当てる空間が足りないと、割り当てに失敗し、動的な変数の中断を示すエラーメッセージが表示されます。使用していない連想配列要素には、常にゼロを割り当ててください。動的な変数の中断を予防するその他のテクニックについては、第 16 章オプションとチューニング可能パラメータを参照してください。

スレッド固有変数

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;
}

スレッド固有変数は、大域変数とは別の名前空間に格納されるので、変数名を再利用できます。プログラム内で名前を多重定義した場合、xself->x はそれぞれ別の変数になります。以下に、スレッド固有変数の使用例を示します。テキストエディタで以下のプログラムを入力し、rtime.d という名前のファイルに保存してください。


例 3–1 rtime.d: read(2) の実行開始からの経過時間を計算する

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 変数 pidtid は、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;
}

節固有変数

D プログラムの節ごとに再利用可能な記憶域を持つ D 変数も定義できます。節固有変数は、C、C++、Java 言語プログラムの自動変数とよく似ており、1 回の関数呼び出しが完了するまで有効です。節固有変数は、すべての D プログラム変数と同じく、最初の割り当て時に作成されます。これらの変数を参照し、割り当てるには、特殊な識別子 this に、演算子 -> を適用します。

BEGIN
{
	this->secs = timestamp / 1000000000;
	...
}

節固有変数を明示的に宣言してから使用したい場合は、キーワード this を使用します。

this int x;   /* an integer clause-local variable */
this char c;  /* a character clause-local variable */

BEGIN
{
	this->x = 123;
	this->c = 'D';
}

節固有変数は、プローブ節の終了まで有効です。DTrace により、プローブ節に関連付けられたアクションの実行が完了すると、すべての節固有変数用の記憶域が回収され、次の節で再利用されます。したがって、節固有変数は、D 変数の中で唯一、ゼロに初期化されることがありません。プログラム内の単一のプローブに複数の節が含まれている場合、これらの節の実行が完了するまで、すべての節固有変数はそのままの状態を保持します。次の例を参照してください。


例 3–2 clause.d: 節固有変数

int me;			/* an integer global variable */
this int foo;		/* an integer clause-local variable */

tick-1sec
{
	/*
	 * Set foo to be 10 if and only if this is the first clause executed.
	 */
	this->foo = (me % 3 == 0) ? 10 : this->foo;
	printf("Clause 1 is number %d; foo is %d\n", me++ % 3, this->foo++);
}

tick-1sec
{
	/*
	 * Set foo to be 20 if and only if this is the first clause executed. 
	 */
	this->foo = (me % 3 == 0) ? 20 : this->foo;
	printf("Clause 2 is number %d; foo is %d\n", me++ % 3, this->foo++);
}

tick-1sec
{
	/*
	 * Set foo to be 30 if and only if this is the first clause executed.
	 */
	this->foo = (me % 3 == 0) ? 30 : this->foo;
	printf("Clause 3 is number %d; foo is %d\n", me++ % 3, this->foo++);
}

節は常に記述された順に実行され、節固有変数は同じプローブを有効にする複数の節を通して変更されないので、上記のプログラムを実行したときの出力は、いつも同じになります。


# dtrace -q -s clause.d
Clause 1 is number 0; foo is 10
Clause 2 is number 1; foo is 11
Clause 3 is number 2; foo is 12
Clause 1 is number 0; foo is 10
Clause 2 is number 1; foo is 11
Clause 3 is number 2; foo is 12
Clause 1 is number 0; foo is 10
Clause 2 is number 1; foo is 11
Clause 3 is number 2; foo is 12
Clause 1 is number 0; foo is 10
Clause 2 is number 1; foo is 11
Clause 3 is number 2; foo is 12
^C

節固有変数は、同じプローブを有効にする複数の節を通して変更されないので、あるプローブに対して実行される最初の節では、その値は未定義になります。節固有変数を使用するときは、必ず事前に適切な値を割り当ててください。そうしないと、プログラムの結果が予想どおりにならない可能性があります。

節固有変数は、任意のスカラー変数型を使って定義できますが、連想配列は、節固有のスコープでは定義できません。節固有変数のスコープは、対応する変数データだけに適用されます。この変数に定義された名前や型の識別情報には適用されません。節固有変数の定義後は、以降のすべての D プログラム節で、この名前と型署名を使用できます。記憶域は、節が変わると変更される可能性があります。

節固有変数を使って、計算の途中結果を累積できます。また、節固有変数をほかの変数の一時コピーとして使用することもできます。節固有変数には、連想配列よりも高速にアクセスできます。そのため、同じ D プログラム節内で特定の連想配列値を繰り返し参照する必要がある場合は、この値を節固有変数にコピーし、節固有変数を繰り返し参照するようにすると、効率がよくなります。

組み込み変数

以下の表に、D の組み込み変数を一覧します。これらの変数は、すべてスカラー大域変数です。現在、D では、スレッド固有変数、節固有変数、および組み込みの連想配列は定義されていません。

表 3–1 DTrace の組み込み変数

型と名前 

説明 

int64_t arg0、...、arg9

プローブに渡される最初の 10 個の入力引数は、生の 64 ビット整数として表現されます。プローブに渡された引数の数が 10 個未満の場合、残りの変数はゼロを返します。 

args[]

現在のプローブに渡される型付き引数です (存在する場合)。args[] 配列へのアクセスには整数インデックスが使用されますが、各要素には、指定のプローブ引数に対応する型が定義されます。たとえば、read(2) システムコールプローブで args[] を参照する場合、args[0] の型は intargs[1] の型は void *args[2] の型は size_t になります。

uintptr_t caller

現在のプローブを入力する直前の、現在のスレッドのプログラムカウンタの場所を示します。 

chipid_t chip

現在の物理チップの CPU チップ識別子です。詳細は、第 26 章sched プロバイダを参照してください。

processorid_t cpu

現在の CPU の CPU 識別子です。詳細は、第 26 章sched プロバイダを参照してください。

cpuinfo_t *curcpu

現在の CPU の CPU 情報です。詳細は、第 26 章sched プロバイダを参照してください。

lwpsinfo_t *curlwpsinfo

現在のスレッドに関連付けられている軽量プロセス (LWP) の LWP 状態です。この構造の詳細は、proc(4) のマニュアルページに記載されています。

psinfo_t *curpsinfo

現在のスレッドに関連付けられているプロセスのプロセス状態です。この構造の詳細は、proc(4) のマニュアルページに記載されています。

kthread_t *curthread

現在のスレッドのオペレーティングシステムカーネルの内部データ構造 kthread_t のアドレスです。kthread_t<sys/thread.h> に定義されています。この変数とその他のオペレーティングシステムデータ構造の詳細は、『SOLARIS インターナル — カーネル構造のすべて』を参照してください。

string cwd

現在のスレッドに関連付けられているプロセスの現在の作業ディレクトリ名です。 

uint_t epid

現在のプローブの有効なプローブ ID (EPID) です。この整数は、特定の述語と一連のアクションによって有効化された特定のプローブを一意に識別します。 

int errno

このスレッドによって直前に実行されたシステムコールから返されるエラー値です。 

string execname

現在のプロセスを実行するため、exec(2) に渡された名前です。

gid_t gid

現在のプロセスの実グループ ID です。 

uint_t id

現在のプローブのプローブ ID です。この ID は、DTrace によって発行された、システム内のプローブを一意に識別する識別子です。この ID を確認するには、dtrace -l を実行します。

uint_t ipl

現在の CPU でのプローブ起動時の割り込み優先レベル (IPL) を表します。Solaris オペレーティングシステムカーネルでの割り込みレベルと割り込み処理の詳細は、『SOLARIS インターナル — カーネル構造のすべて』を参照してください。

lgrp_id_t lgrp

現在の CPU をメンバーに持つ応答時間グループの応答時間グループ ID です。詳細は、第 26 章sched プロバイダを参照してください。

pid_t pid

現在のプロセスのプロセス ID です。 

pid_t ppid

現在のプロセスの親プロセスのプロセス ID です。 

string probefunc

現在のプローブの記述に含まれる関数名の部分です。 

string probemod

現在のプローブの記述に含まれるモジュール名の部分です。 

string probename

現在のプローブの記述に含まれる名前の部分です。 

string probeprov

現在のプローブの記述に含まれるプロバイダ名の部分です。 

psetid_t pset

現在の CPU が含まれているプロセッサセットのプロセッサセット ID です。詳細は、第 26 章sched プロバイダを参照してください。

string root

現在のスレッドに関連付けられているプロセスのルートディレクトリ名です。 

uint_t stackdepth

現在のスレッドのプローブ起動時のスタックフレームの深さを表します。 

id_t tid

現在のスレッドのスレッド ID です。スレッドがユーザープロセスに関連付けられている場合、この値は、pthread_self(3C) の呼び出し結果と同じになります。

uint64_t timestamp

ナノ秒タイムスタンプカウンタの現在の値です。このカウンタの値は、過去の任意の時点から増分しています。そのため、このカウンタは、相対計算専用です。 

uid_t uid

現在のプロセスの実ユーザー ID です。 

uint64_t uregs[]

現在のスレッドの、プローブ起動時のユーザーモード登録値 (保存済み) です。uregs[] 配列の使用方法については、第 33 章ユーザープロセスのトレースを参照してください。

uint64_t vtimestamp

ナノ秒タイムスタンプカウンタの現在の値です。CPU 上で現在のスレッドの実行が開始されてから現在までの経過時間から、DTrace の述語およびアクションで使用された時間を減じた値になります。このカウンタの値は、過去の任意の時点から増分しています。そのため、このカウンタは、相対時間計算専用です。 

uint64_t walltimestamp

1970 年 1 月 1 日の協定世界時 00:00 から現在までの経過時間をナノ秒単位で示します。 

trace() をはじめとする D 言語の組み込み関数については、第 10 章アクションとサブルーチンで説明します。

外部変数

D では、オペレーティングシステムには定義されているが D プログラムには定義されていない変数にアクセスする際、特殊なスコープ演算子として、逆引用符 (`) を使用します。たとえば、Solaris カーネルには、メモリーアロケータのデバッグ機能を有効にする、チューニング可能なシステム変数 kmem_flags の C 宣言が含まれています。kmem_flags の詳細は、『Solaris カーネルのチューンアップ・リファレンスマニュアル』を参照してください。このチューニング可能な変数は、次のように、カーネルソースコード内に C 変数として宣言されています。

int kmem_flags;

D プログラム内でこの変数の値にアクセスするには、次のような D 表記法を使用します。

`kmem_flags

DTrace では、各カーネルシンボルと、対応するオペレーティングシステムの C コード内のシンボルの型が関連付けられます。このため、ソースからネイティブのオペレーティングシステムデータ構造に簡単にアクセスできます。オペレーティングシステムの外部変数を使用するには、対応するオペレーティングシステムのソースコードにアクセスする必要があります。

D プログラムから外部変数へのアクセスは、別のプログラム (オペレーティングシステムカーネルやそのデバイスドライバなど) の内部実装の詳細へのアクセスを意味します。このように、実装の詳細にアクセスする場合、信頼性の高い安定したインタフェースは提供されません。こうした詳細に依存する D プログラムは、ソフトウェアの該当部分をアップグレードすると、動作しなくなる可能性があります。このため、外部変数は、通常、カーネルおよびデバイスドライバの開発者やサービス担当者が、DTrace を使ってパフォーマンスや機能上の問題のデバッグを行うときに使用されます。D プログラムの安定性の詳細は、第 39 章安定性を参照してください。

カーネルシンボル名は、D の変数や関数の識別子とは別の名前空間に格納されます。したがって、カーネルシンボル名と D 変数名の競合が発生することはありません。変数の直前に逆引用符を付けると、D コンパイラは、一致する変数定義を見つけるため、ロードされたモジュールのリストの順番に従って、既知のカーネルシンボルを検索します。Solaris カーネルは、動的にロードされたモジュールごとに独立したシンボル用名前空間を提供します。このため、有効なオペレーティングシステムカーネル内で同じ変数名を繰り返し使用できます。名前の競合を防ぐには、シンボル名の逆引用符より先に、アクセスされる必要のある変数を持つカーネルモジュール名を指定します。たとえば、ロード可能なカーネルモジュールは、通常、_fini(9E) 関数を提供します。したがって、foo という名前のカーネルモジュールが提供する _fini 関数のアドレスを参照するには、次のように記述します。

foo`_fini

外部変数には、オペランドの型の通常規則に従って、値を変更するものは除く、任意の D 演算子を適用できます。DTrace を起動すると、D コンパイラにより、アクティブなカーネルモジュールに対応する変数名のセットがロードされます。このため、これらの変数の宣言は不要です。外部変数には、値を変更するような演算子 (=+= など) を適用してはいけません。DTrace では、安全性の面から、監視対象のソフトウェアの状態を改変することは禁じられています。