Solaris 動的トレースガイド

構造体のポインタ

C や D では、しばしば、ポインタを使って構造体を参照します。ポインタを使って構造体のメンバーにアクセスするときは、演算子 -> を使用します。メンバー m を持つ構造体 struct s があり、この構造体をポイントするポインタ sp がある場合 (spstruct s * 型の変数)、メンバー m にアクセスする方法は 2 とおりあります。1 つは、演算子 * を使ってポインタ sp を間接参照する方法です。

struct s *sp;

(*sp).m

もう 1 つは、短縮形の演算子 -> を使用する方法です。sp が構造体のポインタである場合、次の 2 つの D コードの抜粋は、意味的に等しくなります。

(*sp).m				sp->m

DTrace では、構造体のポインタとして、curpsinfocurlwpsinfo をはじめとするいくつかの組み込み変数を利用できます。curpsinfo と curlwpsinfo は、それぞれ構造体 psinfolwpsinfo を参照し、現在のプローブを起動したスレッドに関連付けられている現在のプロセスおよび軽量プロセス (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 とその型および意味を用いた式を示します。

curpsinfo->pr_pid

pid_t

現在のプロセス ID 

curpsinfo->pr_fname

char []

実行可能ファイル名 

curpsinfo->pr_psargs

char []

最初のコマンド行引数 

構造体定義の全容については、あとで <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 から以下のような出力が得られます。


# dtrace -q -s ksyms.d
read 8192 bytes to user address 80639fc
read 8192 bytes to user address 80639fc
read 8192 bytes to user address 80639fc
read 8192 bytes to user address 80639fc
...
^C
#

一般的な 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_leniov_base にアクセスするため、this->iov を間接参照しています。iov_len と iov_base はそれぞれ、uiomove(9F) の長さ (バイト単位) と、最終的な基底アドレスを表しています。これらの値は、ドライバに対して発行されたシステムコール read(2) の入力パラメータと一致していなければなりません。シェルで dtrace -q -s ksyms.d コマンドを実行し、別のシェルで再度 strings -a /dev/ksyms コマンドを実行してください。次の例のような出力が得られます。


# dtrace -q -s ksyms.d
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
...
^C
#

実際に出力されるアドレスとプロセス ID は、この例のとおりではありませんが、read(2) の入力引数は、ksyms ドライバから uiomove(9F) に渡されるパラメータと一致しています。