DTrace は、ユーザープロセスの動作を把握するために役立つ非常に強力なツールです。デバッグ時やパフォーマンスの問題の解析時に、あるいはただ複雑なアプリケーションの動作を理解したい場合にも、DTrace は非常に有益です。この章では、ユーザープロセスアクティビティに関する DTrace 機能に注目し、例を挙げながらその使用法について説明します。
DTrace でのプロセスの扱い方は、従来のデバッガや監視ツールとは若干異なっています。従来のツールは、プロセスのスコープ内で実行されます。このため、プログラム変数のポインタの間接参照をユーザー側で行うことができます。これに対して、DTrace プローブは、Solaris カーネル内で実行されます。プロセス内で (プロセスの一部として) 実行されるわけではありません。プロセスデータにアクセスする場合、プローブは、サブルーチン copyin() または copyinstr() を使って、ユーザープロセスデータをカーネルのアドレス空間にコピーする必要があります。
たとえば、次のような write(2) システムコールがあるとします。
ssize_t write(int fd, const void *buf, size_t nbytes);
では、write(2) システムコールに渡される文字列の内容を出力するには、どうしたらよいでしょうか。以下は、正しくない D プログラムの例です。
syscall::write:entry { printf("%s", stringof(arg1)); /* incorrect use of arg1 */ }
このスクリプトを実行すると、次のようなエラーメッセージが出力されます。
dtrace: error on enabled probe ID 1 (ID 37: syscall::write:entry): \ invalid address (0x10038a000) in action #1 |
arg1 変数には、buf パラメータの値として、システムコールの実行プロセス内のメモリーを参照するアドレスが入ります。このアドレスの文字列を読み取るには、copyinstr() サブルーチンを使用し、その結果を printf() アクションで記録します。
syscall::write:entry { printf("%s", copyinstr(arg1)); /* correct use of arg1 */
このスクリプトの出力から、write(2) システムコールに渡されるすべての文字列を確認できます。ただし、以下のような異常な出力が得られる場合もあります。
0 37 write:entry mada��� |
copyinstr() サブルーチンは、入力引数 (NULL で終わっている ASCII 文字列のユーザーアドレス) に対して機能します。しかし、write(2) システムコールに渡されるバッファーが、ASCII 文字列ではなくバイナリデータを参照していることがあります。呼び出し側が意図した文字列だけを出力するには、copyin() サブルーチンを使用します。このサブルーチンの第 2 引数にサイズを指定します。
syscall::write:entry { printf("%s", stringof(copyin(arg1, arg2))); }
stringof 演算子がないと、DTrace は、copyin() を使って取得したユーザーデータを正しく文字列に変換できません。しかし、copyinstr() を使用する場合は、stringof は不要になります。これは、copyinstr が常に string 型を返すからです。
サブルーチン copyin() や copyinstr() は、過去にアクセスしたことのないユーザーアドレスを読み取ることができません。アドレスが含まれているページが、過去にアクセスされてフォルトインされたことがないと、たとえそのアドレスが有効であっても、エラーが起きる可能性があります。次の例で考えてみてください。
# dtrace -n syscall::open:entry'{ trace(copyinstr(arg0)); }' dtrace: description 'syscall::open:entry' matched 1 probe CPU ID FUNCTION:NAME dtrace: error on enabled probe ID 2 (ID 50: syscall::open:entry): invalid address (0x9af1b) in action #1 at DIF offset 52 |
この出力例では、アプリケーションが正常に機能していて、arg0 に指定されたアドレスも有効ですが、対応するプロセスがまだアクセスしたことがないページが参照されています。この問題を解決するには、カーネルまたはアプリケーションでデータが使用されるのを待ってからトレースを開始する必要があります。たとえば、システムコールが復帰して、copyinstr() が適用されるまで待ちます。次の例を参照してください。
# dtrace -n syscall::open:entry'{ self->file = arg0; }' \ -n syscall::open:return'{ trace(copyinstr(self->file)); self->file = 0; }' dtrace: description 'syscall::open:entry' matched 1 probe CPU ID FUNCTION:NAME 2 51 open:return /dev/null |
write(2) システムコールの呼び出しをすべてトレースすると、結果が際限なく出力されます。write() の呼び出しのたびに、dtrace(1M) コマンドが出力表示のために write() を呼び出します。このフィードバックループは、dtrace コマンドの干渉によって望ましくないデータが出力される一例です。このような不要なデータのトレースを防ぐには、次の述語を使用します。
syscall::write:entry /pid != $pid/ { printf("%s", stringof(copyin(arg1, arg2))); }
$pid マクロ変数は、プローブを有効にしたプロセスのプロセス ID に展開されます。pid 変数には、プローブが起動した CPU 上でスレッドを実行していたプロセスのプロセス ID が入ります。このため、述語 /pid != $pid/ を使用すれば、このスクリプト自体の実行に関連したイベントをトレースしないようにできます。
syscall プロバイダでは、すべてのシステムコールの開始 (entry) と終了 (return) をトレースできます。プロセスの動作を把握するときは、まずシステムコールに注目します。特に、そのプロセスがカーネル内で長時間実行されていたり、長時間ブロックされているように見える場合は、そうすることをお勧めします。プロセスがどこで時間を消費しているかを確認するには、prstat(1M) コマンドを使用します。
$ prstat -m -p 31337 PID USERNAME USR SYS TRP TFL DFL LCK SLP LAT VCX ICX SCL SIG PROCESS/NLWP 13499 user1 53 44 0.0 0.0 0.0 0.0 2.5 0.0 4K 24 9K 0 mystery/6 |
この例では、あるプロセスが多くのシステム時間を消費しています。この動作の原因の 1 つとして考えられるのは、このプロセスが大量のシステムコールを実行しているのではないかということです。コマンド行から、一番頻繁に呼び出されているシステムコールを調べる単純な D プログラムを実行してみましょう。
# dtrace -n syscall:::entry'/pid == 31337/{ @syscalls[probefunc] = count(); }' dtrace: description 'syscall:::entry' matched 215 probes ^C open 1 lwp_park 2 times 4 fcntl 5 close 6 sigaction 6 read 10 ioctl 14 sigprocmask 106 write 1092 |
出力結果から、一番頻繁に呼び出されているシステムコールがわかります。この例では、write(2) システムコールが該当します。syscall プロバイダを使って、すべての write() システムコールの呼び出し元について詳しく調べることができます。
# dtrace -n syscall::write:entry'/pid == 31337/{ @writes[arg2] = quantize(arg2); }' dtrace: description 'syscall::write:entry' matched 1 probe ^C value ------------- Distribution ------------- count 0 | 0 1 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 1037 2 |@ 3 4 | 0 8 | 0 16 | 0 32 |@ 3 64 | 0 128 | 0 256 | 0 512 | 0 1024 |@ 5 2048 | 0 |
このプロセスは、比較的少量のデータで、多数の write() システムコールを実行しています。このプロセスの場合、このアンバランスがパフォーマンスの問題の原因になっていると考えられます。これで、システムコールの動作について調査する一般的な方法がわかりました。
問題をより詳しく調査するときは、特定のプローブが起動したときのプロセススレッドのスタックをトレースするとよいでしょう。ustack() アクションは、ユーザースレッドのスタックをトレースします。たとえば、多数のファイルを開くプロセスがあるとします。このプロセスは、ときどき open(2) システムコールに失敗します。この場合、問題のある open() を実行するコードパスを探すには、ustack() アクションを使用します。
syscall::open:entry /pid == $1/ { self->path = copyinstr(arg0); } syscall::open:return /self->path != NULL && arg1 == -1/ { printf("open for '%s' failed", self->path); ustack(); }
このスクリプトでは、マクロ変数 $1 も使用されています。このマクロ変数には、dtrace(1M) コマンド行で指定された最初のオペランドの値が入ります。
# dtrace -s ./badopen.d 31337 dtrace: script './badopen.d' matched 2 probes CPU ID FUNCTION:NAME 0 40 open:return open for '/usr/lib/foo' failed libc.so.1`__open+0x4 libc.so.1`open+0x6c 420b0 tcsh`dosource+0xe0 tcsh`execute+0x978 tcsh`execute+0xba0 tcsh`process+0x50c tcsh`main+0x1d54 tcsh`_start+0xdc |
ustack() アクションによって、スタックのプログラムカウンタ (PC) の値が記録されます。すると、dtrace(1M) により、プロセスのシンボルテーブルが検索され、PC 値がシンボル名に解決されます。dtrace が PC 値をシンボル名に解決できない場合は、その値は 16 進整数として出力されます。
ustack() データの出力書式が設定される前にプロセスが終了 (強制終了も含む) すると、dtrace が、スタックトレース内の PC 値をシンボル名に変換できない場合があります。この場合、これらの値は 16 進整数で表示されます。この制限を回避するには、-dtrace コマンドの -c オプションや p オプションを使って、対象プロセスを指定します。これらのオプションやその他のオプションの詳細については、第 14 章dtrace(1M) ユーティリティーを参照してください。プロセス ID やコマンドがあらかじめわかっていない場合は、次のような D プログラムで制限を回避できます。
/* * This example uses the open(2) system call probe, but this technique * is applicable to any script using the ustack() action where the stack * being traced is in a process that may exit soon. */ syscall::open:entry { ustack(); stop_pids[pid] = 1; } syscall::rexit:entry /stop_pids[pid] != 0/ { printf("stopping pid %d", pid); stop(); stop_pids[pid] = 0; }
このスクリプトは、プロセス内のスレッドに ustack() アクションが適用されている場合、プロセスを、その終了直前に停止します。この方法を利用すれば、dtrace コマンドで PC 値をシンボル名に解決できます。stop_pids[pid] の値は、動的変数の消去が完了したら、ゼロになります。prun(1) コマンドを使って、停止したプロセスが実行再開するように設定してください。そうしないと、システム上の停止プロセスが蓄積されてしまいます。
uregs[] 配列を使用すると、個々のユーザーレジスタにアクセスできます。以下の表に、uregs[] 配列のインデックスを、Solaris でサポートされているシステムアーキテクチャ別に示します。
表 33–1 SPARC uregs[] 定数
定数 |
レジスタ |
---|---|
R_G0..R_G7 |
%g0..%g7 汎用レジスタ |
R_O0..R_O7 |
%o0..%o7 アウトレジスタ |
R_L0..R_L7 |
%l0..%l7 ローカルレジスタ |
R_I0..R_I7 |
%i0..%i7 インレジスタ |
R_CCR |
%ccr 条件コードレジスタ |
R_PC |
%pc プログラムカウンタ |
R_NPC |
%npc 次のプログラムカウンタ |
R_Y |
%y 乗算/除算レジスタ |
R_ASI |
%asi アドレス空間識別レジスタ |
R_FPRS |
%fprs 浮動小数点レジスタの状態 |
表 33–2 x86 uregs[] 定数
定数 |
レジスタ |
---|---|
R_CS |
%cs |
R_GS |
%gs |
R_ES |
%es |
R_DS |
%ds |
R_EDI |
%edi |
R_ESI |
%esi |
R_EBP |
%ebp |
R_EAX |
%eax |
R_ESP |
%esp |
R_EAX |
%eax |
R_EBX |
%ebx |
R_ECX |
%ecx |
R_EDX |
%edx |
R_TRAPNO |
%trapno |
R_ERR |
%err |
R_EIP |
%eip |
R_CS |
%cs |
R_ERR |
%err |
R_EFL |
%efl |
R_UESP |
%uesp |
R_SS |
%ss |
AMD64 プラットフォームの uregs 配列には、x86 プラットフォームの内容に加えて、以下の表の要素が含まれます。
表 33–3 amd64 uregs[] 定数
定数 |
レジスタ |
---|---|
R_RSP |
%rsp |
R_RFL |
%rfl |
R_RIP |
%rip |
R_RAX |
%rax |
R_RCX |
%rcx |
R_RDX |
%rdx |
R_RBX |
%rbx |
R_RBP |
%rbp |
R_RSI |
%rsi |
R_RDI |
%rdi |
R_R8 |
%r8 |
R_R9 |
%r9 |
R_R10 |
%r10 |
R_R11 |
%r11 |
R_R12 |
%r12 |
R_R13 |
%r13 |
R_R14 |
%r14 |
R_R15 |
%r15 |
すべてのプラットフォームで使用できる別名は、以下の表のとおりです。
表 33–4 共通の uregs[] 定数
定数 |
レジスタ |
---|---|
R_PC |
プログラムカウンタレジスタ |
R_SP |
スタックポインタレジスタ |
R_R0 |
最初の復帰コード |
R_R1 |
2 番目の復帰コード |
pid プロバイダでは、プロセス内の任意の命令をトレースできます。大半のプロバイダとは異なり、pid プローブは、D プログラム内のプローブ記述で、オンデマンドで作成されます。そのため、ユーザー自身で pid プローブを有効にしないかぎり、dtrace -l を実行しても何も出力されません。
pid プロバイダのもっとも単純な操作モードは、fbt プロバイダにとってのユーザー空間に似ています。以下は、ある関数の開始 (entry) と終了 (return) をすべてトレースするプログラム例です。$1 マクロ変数 (コマンド行の最初のオペランド) には、トレース対象のプロセスのプロセス ID が入ります。$2 マクロ変数 (コマンド行の 2 番目のオペランド) には、すべての関数呼び出しをトレースする関数の名前が入ります。
pid$1::$2:entry { self->trace = 1; } pid$1::$2:return /self->trace/ { self->trace = 0; } pid$1:::entry, pid$1:::return /self->trace/ { }
このスクリプトを userfunc.d という名前のファイルに保存し、chmod で実行可能ファイルに変更します。このスクリプトからは、次のような結果が出力されます。
# ./userfunc.d 15032 execute dtrace: script './userfunc.d' matched 11594 probes 0 -> execute 0 -> execute 0 -> Dfix 0 <- Dfix 0 -> s_strsave 0 -> malloc 0 <- malloc 0 <- s_strsave 0 -> set 0 -> malloc 0 <- malloc 0 <- set 0 -> set1 0 -> tglob 0 <- tglob 0 <- set1 0 -> setq 0 -> s_strcmp 0 <- s_strcmp ... |
pid プロバイダは、すでに実行中のプロセスに対してしか使用できません。$target マクロ変数 (第 15 章スクリプトの作成を参照) と、dtrace コマンドの -c オプションと-p オプションを使って、プロセスを作成し、取り込みます。さらに、DTrace を使って、これらのプロセスを計測します。たとえば、次の D スクリプトでは、特定の従属プロセスによって実行される libc の関数呼び出しの内訳がわかります。
pid$target:libc.so::entry { @[probefunc] = count(); }
このスクリプトを libc.d という名前のファイルに保存し、次のコマンドを実行すると、date(1) コマンドによって実行されるこの種の呼び出しの内訳がわかります。
# dtrace -s libc.d -c date dtrace: script 'libc.d' matched 2476 probes Fri Jul 30 14:08:54 PDT 2004 dtrace: pid 109196 has exited pthread_rwlock_unlock 1 _fflush_u 1 rwlock_lock 1 rw_write_held 1 strftime 1 _close 1 _read 1 __open 1 _open 1 strstr 1 load_zoneinfo 1 ... _ti_bind_guard 47 _ti_bind_clear 94 |
pid プロバイダでは、任意のユーザー関数内の任意の命令をトレースできます。pid プロバイダは、必要に応じて、関数内の各命令に対して 1 つずつプローブを作成します。各プローブの名前は、関数内の対応する命令のオフセット (16 進整数) になります。たとえば、PID 123 のプロセス内にあるモジュール bar.so の関数 foo で、オフセット 0x1c にある命令に関連したプローブを有効にしたい場合は、次のコマンドを使用します。
# dtrace -n pid123:bar.so:foo:1c |
関数 foo 内のプローブを、各命令用のプローブも含めてすべて有効にするには、次のコマンドを使用します。
# dtrace -n pid123:bar.so:foo: |
このコマンドは、ユーザーアプリケーションのデバッグと解析にたいへん役立ちます。発生頻度の低いエラーは、再現性が低いため、簡単にはデバッグできません。また、エラーの発生後、ようやく問題を突き止めたときには、すでにコードパスを再現できなくなっている場合もあります。以下の例のように、pid プロバイダと投機トレース (第 13 章投機トレースを参照) を組み合わせて、関数内のすべての命令を個別にトレースすると、この問題を解決できます。
pid$1::$2:entry { self->spec = speculation(); speculate(self->spec); printf("%x %x %x %x %x", arg0, arg1, arg2, arg3, arg4); } pid$1::$2: /self->spec/ { speculate(self->spec); } pid$1::$2:return /self->spec && arg1 == 0/ { discard(self->spec); self->spec = 0; } pid$1::$2:return /self->spec && arg1 != 0/ { commit(self->spec); self->spec = 0; }
errorpath.d を実行すると、次のような結果が出力されます。
# ./errorpath.d 100461 _chdir dtrace: script './errorpath.d' matched 19 probes CPU ID FUNCTION:NAME 0 25253 _chdir:entry 81e08 6d140 ffbfcb20 656c73 0 0 25253 _chdir:entry 0 25269 _chdir:0 0 25270 _chdir:4 0 25271 _chdir:8 0 25272 _chdir:c 0 25273 _chdir:10 0 25274 _chdir:14 0 25275 _chdir:18 0 25276 _chdir:1c 0 25277 _chdir:20 0 25278 _chdir:24 0 25279 _chdir:28 0 25280 _chdir:2c 0 25268 _chdir:return |