内に含まその他のドキュメントサポート リソース | PDF 文書ファイルをダウンロードする (2302 KB)
第 7 章 構造体と共用体関連性のある変数同士は、「構造体」および「共用体」と呼ばれる複合データオブジェクトにグループ化することができます。D でこれらのオブジェクトを作成するには、新しい型定義を作成します。作成した新しい型は、連想配列の値を含むあらゆる D 変数で使用できます。この章では、こうした複合型を作成し操作するための構文とセマンティクスを紹介し、これらに使用する D 演算子について説明します。構造体と共用体の構文については、DTrace プロバイダ fbt と pid の使い方を示すプログラム例で説明します。 構造体いくつかの型のグループから成る新しい型を作成するときは、D のキーワード struct (「structure (構造体)」の略) を使用します。この新しい構造体型を D の変数や配列の型として使用することにより、関連性のある複数の変数を単一の名前で定義できます。D の構造体は、C や C++ の対応する構造体と同じです。D の構造体は、Java プログラミングにおけるクラスに似ていますが、クラスとは違ってメソッドを持たず、データメンバーだけを備えています。 以下では、D を使って、シェルでシステムコール read(2) または write(2) が実行されるたびに、経過時間、呼び出し回数、引数として渡される最大バイト数などのデータを記録する、複雑なシステムコールトレースプログラムを作成してみましょう。次の例のように、3 つの連想配列を使ってプロパティを記録する D 節を記述できます。 syscall::read:entry, syscall::write:entry
/pid == 12345/
{
ts[probefunc] = timestamp;
calls[probefunc]++;
maxbytes[probefunc] = arg2 > maxbytes[probefunc] ?
arg2 : maxbytes[probefunc];
}
ただし、この節では、DTrace は 3 つの連想配列を作成し、それぞれに probefunc に対応する同一の組の値の各コピーを格納するため、効率面で問題があります。構造体を使用すると、読みやすく管理もしやすい、よりコンパクトなプログラムになります。まず、プログラムソースファイルの冒頭で新しい構造体型を宣言します。 struct callinfo {
uint64_t ts; /* timestamp of last syscall entry */
uint64_t elapsed; /* total elapsed time in nanoseconds */
uint64_t calls; /* number of calls made */
size_t maxbytes; /* maximum byte count argument */
};
キーワード struct の後ろに、この新しい型を参照する識別子 (オプション) を付けて、struct callinfo としています。続く中括弧 ({ }) 内には構造体のメンバーが記述され、宣言全体はセミコロン (;) で終わります。 構造体のメンバーの定義には、D 変数宣言と同じ構文を使用します。最初にメンバーの型を入力し、次に識別子名、最後にセミコロン (;) を入力します。 構造体の宣言には、新しい型を定義する働きしかありません。この宣言自体が、変数を作成したり、DTrace 内の記憶域を割り当てたりすることはありません。宣言後は、D プログラム内で struct callinfo を型として使用できます。構造体テンプレートを使用して、struct callinfo 型の変数 1 つにつき、4 つの変数のコピーが格納されます。メンバーは、メンバーリストの順序に従ってメモリー内に配置されます。データオブジェクトの配置に必要な場合は、メンバー間にパディングスペースが挿入されます。 個々のメンバーの値にアクセスするには、演算子「.」とメンバーの識別子名を使って、次のような式を作成します。 variable-name.member-name 以下に、新しい構造体型を使った改良版のプログラム例を示します。エディタで以下の D プログラムを入力し、rwinfo.d という名前のファイルに保存してください。 例 7–1 rwinfo.d: read(2) と write(2) の統計情報の収集struct callinfo {
uint64_t ts; /* timestamp of last syscall entry */
uint64_t elapsed; /* total elapsed time in nanoseconds */
uint64_t calls; /* number of calls made */
size_t maxbytes; /* maximum byte count argument */
};
struct callinfo i[string]; /* declare i as an associative array */
syscall::read:entry, syscall::write:entry
/pid == $1/
{
i[probefunc].ts = timestamp;
i[probefunc].calls++;
i[probefunc].maxbytes = arg2 > i[probefunc].maxbytes ?
arg2 : i[probefunc].maxbytes;
}
syscall::read:return, syscall::write:return
/i[probefunc].ts != 0 && pid == $1/
{
i[probefunc].elapsed += timestamp - i[probefunc].ts;
}
END
{
printf(" calls max bytes elapsed nsecs\n");
printf("------ ----- --------- -------------\n");
printf(" read %5d %9d %d\n",
i["read"].calls, i["read"].maxbytes, i["read"].elapsed);
printf(" write %5d %9d %d\n",
i["write"].calls, i["write"].maxbytes, i["write"].elapsed);
}
プログラムを入力したら、シェルプロセスを 1 つ指定して、dtrace -q -s rwinfo.d を実行します。次に、シェルコマンドをいくつか入力します。シェルコマンドを入力し終わったら、dtrace 端末ウィンドウ内で Control-C キーを押します。すると、END プローブが起動し、結果が出力されます。
構造体のポインタC や D では、しばしば、ポインタを使って構造体を参照します。ポインタを使って構造体のメンバーにアクセスするときは、演算子 -> を使用します。メンバー m を持つ構造体 struct s があり、この構造体をポイントするポインタ sp がある場合 (sp は struct s * 型の変数)、メンバー m にアクセスする方法は 2 とおりあります。1 つは、演算子 * を使ってポインタ sp を間接参照する方法です。 struct s *sp; (*sp).m もう 1 つは、短縮形の演算子 -> を使用する方法です。sp が構造体のポインタである場合、次の 2 つの D コードの抜粋は、意味的に等しくなります。 (*sp).m sp->m DTrace では、構造体のポインタとして、curpsinfo、curlwpsinfo をはじめとするいくつかの組み込み変数を利用できます。curpsinfo と curlwpsinfo は、それぞれ構造体 psinfo と lwpsinfo を参照し、現在のプローブを起動したスレッドに関連付けられている現在のプロセスおよび軽量プロセス (LWP) の状態情報のスナップショットを提供します。Solaris LWP は、ユーザースレッドのカーネル表現です。このユーザースレッドの上に、Solaris スレッドと POSIX スレッドのインタフェースが構築されています。便宜上、DTrace は、この情報を /proc ファイルシステムの /proc/pid/psinfo ファイルや /proc/pid/lwps/lwpid/lwpsinfo ファイルと同じ形式でエクスポートします。/proc 構造体は、システムヘッダーファイル <sys/procfs.h> に定義されており、ps(1)、pgrep(1)、truss(1) などの監視/デバッグツールで使用されます。詳細は、proc(4) のマニュアルページを参照してください。以下に、例として、curpsinfo とその型および意味を用いた式を示します。
構造体定義の全容については、あとで <sys/procfs.h> ヘッダーファイルと proc(4) の該当箇所を参照して、確認してください。次の例では、pr_psargs メンバーを使ってコマンド行引数の照合を行うことにより、特定のプロセスを識別します。 構造体は、C プログラムで複雑なデータ構造を作成するときによく使用されます。同様に、D でも、構造体を記述し、参照することにより、Solaris オペレーティングシステムカーネルとそのシステムインタフェースの内部処理を効果的に監視できます。次の例では、ksyms(7D) ドライバと read(2) 要求の関係に注目して、先ほど紹介した構造体 curpsinfo といくつかのカーネル構造体について見ていきます。ドライバは、uio(9S) と iovec(9S) の 2 つの一般的な構造体を使って、文字デバイスファイル /dev/ksyms の読み取り要求に応答します。 構造体 uio にアクセスするときは、struct uio のように名前を指定するか、uio_t のように型の別名を指定します。この構造体は、カーネルとユーザープロセス間でのデータのコピーを伴う入出力要求を記述する際によく使用されるもので、詳細は uio(9S) のマニュアルページに記載されています。システムコール readv(2) または writev(2) を使って複数のチャンクが要求されると、そのたびに、入出力要求をそれぞれ部分的に記述した 1 つ以上の iovec(9S) 構造体から成る配列が、uio に格納されます。struct uio を操作するカーネルデバイスドライバインタフェース (DDI) ルーチンの中には、uiomove(9F) が含まれます。このルーチンは、カーネルドライバがユーザープロセスの read(2) 要求に応えてデータをユーザープロセスにコピーするために使用する、関数群の 1 つです。 ksyms ドライバは、文字デバイスファイル /dev/ksyms を管理しています。この文字デバイスファイルは、カーネルのシンボルテーブルに関する情報が収められた ELF ファイルのように見えますが、実際にはそうではありません。ドライバが、現在カーネルにロードされているモジュールセットを使用しているので、そのように見えるだけです。ドライバは、uiomove(9F) ルーチンを使って read(2) 要求に応答します。次の例では、/dev/ksyms から引数を指定して read(2) を呼び出す処理と、ドライバから uiomove(9F) を呼び出して read(2) に指定された位置のユーザーアドレス空間に結果をコピーする処理とが、同じであることを示します。 /dev/ksyms を強制的に読み取るには、strings(1) ユーティリティーに -a オプションを指定して実行します。シェルで strings -a /dev/ksyms を実行し、出力結果を確認してみましょう。エディタにスクリプト例の最初の節を入力し、ksyms.d という名前のファイルに保存してください。 syscall::read:entry
/curpsinfo->pr_psargs == "strings -a /dev/ksyms"/
{
printf("read %u bytes to user address %x\n", arg2, arg1);
}
この最初の節では、curpsinfo->pr_psargs という式を使って、strings(1) コマンドのコマンド行引数にアクセスし、これを照合しています。このため、スクリプトは、引数をトレースする前に正しい read(2) 要求を選択できます。char の配列になっている左側の引数と、文字列である右側の引数は、演算子 == で結ばれています。これにより、D コンパイラは、左側の引数を文字列に拡張し、文字列比較を行う必要があると判断します。シェルで dtrace -q -s ksyms.d コマンドを実行し、別のシェルで strings -a /dev/ksyms コマンドを実行してください。strings(1) を実行すると、DTrace から以下のような出力が得られます。
一般的な D のプログラミングテクニックを使ってこの例を拡張し、最初の read(2) 要求からカーネル内の下位スレッドへ下りていくようなプログラムも作成できます。syscall::read:entry でカーネルに入ると、次のスクリプトにより、このスレッドが対象のスレッドであることを示すスレッド固有のフラグ変数が設定されます。このフラグは、syscall::read:return で消去されます。設定済みのフラグは、その他のプローブで、uiomove(9F) などのカーネル関数を計測する述語として使用できます。DTrace の関数境界トレース ( fbt) プロバイダは、カーネル内に定義された関数 (DDI 内の関数を含む) の開始 (entry) プローブと終了 (return) プローブを発行します。次のソースコードは、fbt プロバイダを使って uiomove(9F) を計測するコードです。このコードを入力して、ksyms.d ファイルに保存してください。 例 7–2 ksyms.d: read(2) と uiomove(9F) の関係のトレース/*
* When our strings(1) invocation starts a read(2), set a watched flag on
* the current thread. When the read(2) finishes, clear the watched flag.
*/
syscall::read:entry
/curpsinfo->pr_psargs == "strings -a /dev/ksyms"/
{
printf("read %u bytes to user address %x\n", arg2, arg1);
self->watched = 1;
}
syscall::read:return
/self->watched/
{
self->watched = 0;
}
/*
* Instrument uiomove(9F). The prototype for this function is as follows:
* int uiomove(caddr_t addr, size_t nbytes, enum uio_rw rwflag, uio_t *uio);
*/
fbt::uiomove:entry
/self->watched/
{
this->iov = args[3]->uio_iov;
printf("uiomove %u bytes to %p in pid %d\n",
this->iov->iov_len, this->iov->iov_base, pid);
}
この例の最後の節では、スレッド固有変数 self->watched を使って、対象のカーネルスレッドが DDI ルーチン uiomove(9F) に入るタイミングを特定しています。そこから、組み込みの args 配列を使って、uiomove() の 4 番目の引数 (args[3]) にアクセスしています。この引数は、要求を表す struct uio のポインタになっています。D コンパイラは、args 配列の各メンバーに、計測されるカーネルルーチンの C 関数プロトタイプに当たる型を自動的に割り当てます。uio_iov メンバーには、要求の struct iovec のポインタが格納されます。このポインタのコピーは、節で使用するため、節固有変数 this->iov に保存されます。最後の文では、iovec のメンバーである iov_len と iov_base にアクセスするため、this->iov を間接参照しています。iov_len と iov_base はそれぞれ、uiomove(9F) の長さ (バイト単位) と、最終的な基底アドレスを表しています。これらの値は、ドライバに対して発行されたシステムコール read(2) の入力パラメータと一致していなければなりません。シェルで dtrace -q -s ksyms.d コマンドを実行し、別のシェルで再度 strings -a /dev/ksyms コマンドを実行してください。次の例のような出力が得られます。
実際に出力されるアドレスとプロセス ID は、この例のとおりではありませんが、read(2) の入力引数は、ksyms ドライバから uiomove(9F) に渡されるパラメータと一致しています。 共用体共用体も、ANSI-C と D の両方でサポートされる複合型です。共用体と構造体の間には、密接な関係があります。共用体では、異なった型を持つ複数のメンバーが定義されますが、メンバーオブジェクトはすべて同じ記憶域を占有します。つまり共用体は、時と場合に応じて有効なメンバーが変わる、バリアント型のオブジェクトであり、ある時点で有効なメンバーは、共用体の割り当て方によって決まります。通常、共用体のどのメンバーが有効になるかは、その他の変数や状態によって決定されます。共用体のサイズは、その最大のメンバーのサイズに一致しており、共用体のメモリー配置は、そのメンバーに必要な最大配置に一致しています。 Solaris の kstat フレームワークで定義されている構造体には、C と D の共用体について説明する以下の例で使用されている共用体が含まれます。kstat フレームワークは、メモリーの使用率や入出力スループットなどのカーネル統計情報を表す、名前付きのカウンタのセットをエクスポートするために使用されます。このフレームワークは、mpstat(1M) や iostat(1M) のようなユーティリティーの実装にも使用されます。このフレームワークは、struct kstat_named を使って、名前付きカウンタとその値を表します。このフレームワークの定義は以下のとおりです。 struct kstat_named {
char name[KSTAT_STRLEN]; /* name of counter */
uchar_t data_type; /* data type */
union {
char c[16];
int32_t i32;
uint32_t ui32;
long l;
ulong_t ul;
...
} value; /* value of counter */
};
上記の宣言は、わかりやすくするために省略されています。構造体宣言の全容は、<sys/kstat.h> ヘッダーファイルで確認できます。また、kstat_named(9S) にも説明が記載されています。上記の宣言は、ANSI-C と D のどちらでも有効です。この宣言では、カウンタの型によって異なった型のメンバーを持つ共用体の値をメンバーとして含む、構造体が定義されています。共用体自体が別の型 struct kstat_named の内部に宣言されているので、共用体の正式名は省略されています。この宣言書式は、「無名共用体」として知られています。メンバー value は、先ほどの宣言で記述された共用体型ですが、この共用体型はほかの場所では使用されないので、名前を持っていません。構造体メンバー data_type には、struct kstat_named 型のオブジェクトごとに、どの共用体メンバーが有効になるかを示す値が割り当てられています。data_type の値には、C プリプロセッサトークンのセットが定義されています。たとえば、トークン KSTAT_DATA_CHAR はゼロであり、現在値がメンバー value.c に格納されていることを示しています。 例 7–3 は、ユーザープロセスをトレースすることにより、kstat_named.value という共用体にアクセスする様子を示しています。kstat_data_lookup(3KSTAT) 関数を使って、kstat カウンタの標本がユーザープロセスから収集され、この関数は struct kstat_named へのポインタを返します。mpstat(1M) ユーティリティーは、実行されるたびにこの関数を繰り返し呼び出し、最新のカウンタ値を収集します。シェルで mpstat 1 を実行し、出力結果を確認してください。しばらくしたら、Control-C キーを押して、mpstat を終了させます。収集したカウンタを確認するため、libkstat 内で mpstat コマンドが kstat_data_lookup(3KSTAT) 関数を呼び出すたびに起動するプローブを有効にします。このためには、新しい DTrace プロバイダ pid を使用します。pid プロバイダを使用すると、関数のエントリポイントなど、C のシンボル位置にあるユーザープロセス内で、動的にプローブを作成できます。次のようなプローブ記述を作成して、pid に、ユーザー関数の開始 (entry) 位置と終了 (return) 位置でプローブを作成させることもできます。 pidprocess-ID:object-name:function-name:entry pidprocess-ID:object-name:function-name:return たとえば、プロセス ID 内に、kstat_data_lookup(3KSTAT) の開始時に起動するプローブを作成したい場合、次のようなプローブ記述を作成します。 pid12345:libkstat:kstat_data_lookup:entry pid プロバイダは、プローブ記述に対応するプログラム位置で、指定したユーザープロセスに、動的計測機能を挿入します。このプローブ実装により、計測されるプログラム位置に到達したユーザースレッドは、オペレーティングシステムカーネルに割り込んで DTrace に入り、対応するプローブを起動するようになります。したがって、計測機能の場所がユーザープロセスに関連付けられていても、指定した DTrace の述語とアクションは、オペレーティングシステムカーネルのコンテキストで実行されます。pid プロバイダの詳細は、第 30 章pid プロバイダに記載されています。 プログラムのコンパイル時に評価されて dtrace の追加コマンド行引数になる「マクロ変数」という識別子をプログラムに挿入すると、D プログラムを別のプロセスに適用するたびに D プログラムソースを編集する必要がなくなります。マクロ変数を指定するときは、ドル記号 ($) と、識別子として機能する数字を入力します。コマンド dtrace -s script foo bar baz を実行した場合、D コンパイラにより自動的に、マクロ変数 $1、$2、$3 がそれぞれトークン foo、bar、baz として定義されます。マクロ変数は、D プログラム式やプローブ記述で使用します。たとえば、次のプローブ記述では、dtrace の追加引数として指定されているプロセス ID を計測できます。 pid$1:libkstat:kstat_data_lookup:entry
{
self->ksname = arg1;
}
pid$1:libkstat:kstat_data_lookup:return
/self->ksname != NULL && arg1 != NULL/
{
this->ksp = (kstat_named_t *)copyin(arg1, sizeof (kstat_named_t));
printf("%s has ui64 value %u\n", copyinstr(self->ksname),
this->ksp->value.ui64);
}
pid$1:libkstat:kstat_data_lookup:return
/self->ksname != NULL && arg1 == NULL/
{
self->ksname = NULL;
}
マクロ変数と再利用可能なスクリプトの詳細は、第 15 章スクリプトの作成で説明します。プロセス ID を使ってユーザープロセスを計測する方法がわかったところで、共用体の標本の収集を再開しましょう。次のソースコード例をエディタに入力し、kstat.d という名前で保存してください。 例 7–3 kstat.d: kstat_data_lookup(3KSTAT) への呼び出しのトレースpid$1:libkstat:kstat_data_lookup:entry
{
self->ksname = arg1;
}
pid$1:libkstat:kstat_data_lookup:return
/self->ksname != NULL && arg1 != NULL/
{
this->ksp = (kstat_named_t *) copyin(arg1, sizeof (kstat_named_t));
printf("%s has ui64 value %u\n",
copyinstr(self->ksname), this->ksp->value.ui64);
}
pid$1:libkstat:kstat_data_lookup:return
/self->ksname != NULL && arg1 == NULL/
{
self->ksname = NULL;
}
シェルで mpstat 1 コマンドを実行して、統計情報の標本を収集し、毎秒報告するモードで mpstat(1M) の実行を開始します。mpstat の実行開始後、別のシェルでコマンド dtrace -q -s kstat.d `pgrep mpstat` を実行します。アクセス対象の統計情報に対応した出力が得られます。dtrace を強制終了し、シェルプロンプトに戻るには、Control-C キーを押します。
各端末ウィンドウ内の出力を取得し、統計情報の値をそれぞれ 1 つ前の値から次々に減算していけば、dtrace の出力と mpstat の出力の相関関係が明らかになります。このプログラム例は、ルックアップ関数に入るときにカウンタ名のポインタを記録し、kstat_data_lookup(3KSTAT) が終了するときにほとんどのトレース作業を実行します。D の組み込み関数 copyinstr() と copyin() は、戻り値 arg1 が NULL 以外のとき、ユーザープロセスから DTrace に関数の実行結果をコピーします。kstat データのコピーが完了すると、共用体の ui64 カウンタ値が報告されます。この単純な例では、mpstat が value.ui64 メンバーを使用するカウンタの標本を収集すると想定しています。練習として、複数の述語を使用し、data_type メンバーに対応する共用体メンバーを出力するよう、kstat.d を書き直してみてください。また、連続するデータ値の差分を計算して mpstat に似た結果を実際に出力するような kstat.d も作成してみてください。 メンバーのサイズとオフセット演算子 sizeof を使用すると、構造体や共用体を含むあらゆる D 型 (D 式) のサイズ (バイト単位) を特定できます。演算子 sizeof は、式または丸括弧で囲まれた形式の型名に適用できます。次の 2 つの例を参照してください。 sizeof expression sizeof (type-name) たとえば、式 sizeof (uint64_t) の戻り値は 8 です。式 sizeof (callinfo.ts) も、先ほどのプログラム例のソースコードに挿入した場合、8 を返します。演算子 sizeof の正式な戻り型は、size_t です。これは、バイト数を表現する際に使用される型の別名であり、定義により、現在のデータモデル内のポインタと同じサイズの符号なし整数になります。式に演算子 sizeof を適用した場合、この式は D コンパイラによって評価されますが、結果のオブジェクトサイズはコンパイル時に計算され、式のコードは生成されません。sizeof は、整数定数が必要な箇所で使用できます。 同様の演算子 offsetof を使って、構造体型または共用体型のオブジェクトに関連付けられた記憶域の開始位置から、構造体メンバーまたは共用体メンバーのオフセットをバイト単位で特定することもできます。演算子 offsetof は、次の形式の式で使用します。 offsetof (type-name, member-name) type-name は構造体型の名前、共用体型の名前、またはこれらの型の別名で、member-name はその構造体または共用体のメンバーの名前を示す識別子です。offsetof は、sizeof と同様に、size_t を返し、D プログラム内の整数定数を使用できる箇所で使用できます。 ビットフィールドD では、任意のビット数の整数の構造体メンバーまたは共用体メンバーも定義できます。これを「ビットフィールド」と呼びます。ビットフィールドを宣言するときは、次の例のように、符号付きまたは符号なしの整数基本型、メンバー名、およびフィールドに割り当てるビット数を示す接尾辞を指定します。 struct s {
int a : 1;
int b : 3;
int c : 12;
};
ビットフィールド幅は、メンバー名の後ろのコロンに続く整数定数で表されます。ビットフィールド幅は必ず正の数で、対応する整数基本型の幅を超えない範囲のビット数とします。D では、64 ビットを超えるビットフィールドは宣言できません。D のビットフィールドは、対応する ANSI-C の機能と互換性があり、ANSI-C の機能にアクセスできます。通常、ビットフィールドは、メモリー記憶域が不足している場合や、構造体レイアウトをハードウェアレジスタレイアウトと一致させる必要がある場合に使用します。 ビットフィールドは、整数やマスクセットのレイアウトを自動化し、メンバー値を抽出するコンパイラ構成です。マスクをユーザー定義し、演算子 & を使用しても、同じ結果が得られます。C と D のコンパイラは、できるだけ効率よくビットを詰めようとしますが、処理の順序や方法は特に指定されていません。そのため、使用するコンパイラやアーキテクチャが異なる場合は、ビットフィールドを指定しても、同一のビットレイアウトが得られるとはかぎりません。安定したビットレイアウトが必要な場合は、ビットマスクを作成し、演算子 & を使って値を抽出してください。 ビットフィールドメンバーにアクセスするときは、構造体や共用体のメンバーにアクセスするときと同じく、その名前と演算子「.」または「->」を指定します。ビットフィールドは、どの式でも使用できるようにするため、自動的に 2 番目に大きい整数型に拡張されます。ビットフィールド記憶域はバイト境界上には配置できません。また、そのサイズは、バイト数の概数であってはなりません。したがって、ビットフィールドメンバーには、演算子 sizeof や offsetof を適用することはできません。D コンパイラでは、演算子 & を使ってビットフィールドメンバーのアドレスを指定することも禁じられています。 |
|||||||||||||