C/C++プログラマのためのDTrace入門

第3回 マルチスレッド/子プロセス/共有ライブラリ

この記事を読むのに必要な時間:およそ 7 分

子プロセス

マルチスレッド化と同様に,子プロセスを生成してのマルチプロセス化も,高速化の手法として多くの局面で採用されます。

fork() 時のトレース採取

これまで説明に使用してきたDスクリプトでは,pidプロバイダに対してプロセスIDを明示的に指定してきました。

しかし,情報採取対象となっている親プロセスと,fork()システムコールで生成された子プロセスでは,プロセスIDは当然異なるものとなります。

つまり,親プロセスから情報採取するためのDスクリプトでは,子プロセスから引き続き関数フローの採取を行うことはできません。

新規に生成された子プロセスから情報採取を行う簡単な方法は,デバッガ等を使用して子プロセスの生成契機を補足し,生成された子プロセスのプロセスIDを取得することで,改めてdtraceの適用を行う,というものです。

たとえば,リスト6に示すような,fork()により子プロセスを生成するプログラムshow_mpを仮定します(エラー処理記述は割愛しています)⁠

リスト 6 子プロセスの生成show_mp

int
main(int argc,
     const char* argv[])
{
    pid_t pid;
    int loops = atoi(argv[1]);

    if(0 == (pid = fork())){ /* 子プロセス */
        running(loops);
    }
    else if(0 < pid){ /* 親プロセス */
        siginfo_t info;
        waitid(P_PID, pid, &info, WEXITED);
    }

    return 0;
}

それではデバッガを使って,子プロセスの生成契機を補足してみましょう。

以降の実行例は,Solaris の標準デバッガであるdbx を使用したものです。

図1 show_mpの実行

$ dbx show_mp
....
(dbx) dbxenv follow_fork_mode child ※ 子プロセスの追跡を指定
(dbx) run 10 ※ 実行開始
Running: show_mp 10
(process id 12161)
detaching from process 12161 ※ 親プロセスの切り離し
Attached to process 12162 ※ 子プロセスとの接続
stopped in __forkx at 0xd2af207b ※ fork() 完了時点で停止
0xd2af207b: __forkx+0x000b:     popl     %ecx
(dbx)

"Attached to process ....." 部分に,fork()により生成された子プロセスのプロセスID(上記実行例の場合は12162)が表示されますから,dbxcontコマンドで子プロセスの実行を再開する前に,当該プロセスに対してdtraceコマンドを適用して,情報採取を開始してください。

ただし,デバッガにより停止状態にあるプロセスに対して,dtraceコマンドの-p オプションによる採取対象プロセス ID 指定を行うと,以下のようなエラーが表示されます。

図2 -p指定付きdtraceコマンドの実行

$ dtrace -p 12162 ....
dtrace: failed to grab pid 12162: process is traced
$

デバッガの制御下にあるプロセスに対してdtrace コマンドを適用する場合は,-pオプションを使用しないようにしてください。

なお,dbxをコマンドラインから使用した場合,親プロセスか子プロセスの一方しか停止できません(※2)⁠そのため上記の手順では,親プロセスが次々と子プロセスを生成するような状況で,生成された全ての子プロセスに対するdtraceコマンド適用を行うことはできません。

このような場合には,以下の2つの処理を組み合わせるようなスクリプトを作成するなどして,生成された子プロセスに対して順次dtraceを適用する,といった実現方式が考えられます。

  • truss コマンド出力の加工などによるfork() 戻り値=子プロセスのプロセスIDの抽出
  • 与えられたプロセスIDに対するdtrace コマンドの適用

ただしこの方法の場合,trussコマンドによるfork()実行の検出から,子プロセスに対するdtrace適用までに時間差が生じますので,子プロセスからのトレース採取に漏れが生じる可能性があります。

※2)
SunStudio IDEからdbxを使用する場合,follow_fork_modeboth を指定することで,親プロセスと子プロセスの両方を停止できます

exec() 時のトレース採取

厳密には,exec()(※3)の実行は子プロセスに限定された話では無いのですが,fork() した子プロセス側で実施されるのが一般的ですので,⁠子プロセス」のトピックとして扱いたいと思います。

たとえば,リスト7に示すような,指定された引数で新たなプログラムをexec()するプログラムshow_execを仮定します。

リスト7 exec()の実行show_exec

int
main(int argc,
     char* argv[])
{
    execv(argv[1], argv + 1);

    /* execv() 成功時は,ここには到達しません */
    return 1;
}

exec() に関する注意点を説明するに当たり,ここで少々DTraceの情報採取の実現方式に関して説明をしようと思います。

まずはデバッガを使用して,show_execプログラムのmain関数冒頭を逆アセンブルしてみましょう。

図3 main冒頭の逆アセンブル結果~その1

$ dbx show_exec
....
(dbx) dis main
0x08050af0: main       : pushl %ebp
0x08050af1: main+0x0001: movl  %esp,%ebp
0x08050af3: main+0x0003: subl  $0x00000004,%esp
0x08050af6: main+0x0006: movl  0x0000000c(%ebp),%edx
0x08050af9: main+0x0009: movl  0x0000000c(%ebp),%eax
0x08050afc: main+0x000c: addl  $0x00000004,%eax
....

上記の結果を踏まえて,以下の操作を行います。

  1. main 冒頭にブレークポイントを設定
  2. プログラムを実行 ~ main で停止
  3. main のブレークポイントを無効化

dbxでの実行例を以下に示します。

図4 show_execの実行~その1

(dbx) stop in main ※ ブレークポイント設定
(2) stop in main
(dbx) run /usr/bin/ls -l /tmp ※ 実行開始
Running: show_exec /usr/bin/ls -l /tmp
(process id 12493)
stopped in main at 0x08050af0 ※ ブレークポイントで停止
0x08050af0: main       :        pushl    %ebp
(dbx) status
*(2) stop in main
(dbx) handler -disable 2 ※ ブレークポイントの無効化
(dbx) status
*[2] stop in main -disable

この段階でデバッグ中のプロセス(上記の例ではプロセスID 12493)に対してdtraceコマンドを適用し,関数フロー採取を開始します。

dtraceコマンドによる採取が開始されたなら,もう一度先ほどの関数冒頭部分を逆アセンブルしてみましょう。

図5 main冒頭の逆アセンブル結果~その2

(dbx) dis main
0x08050af0: main       : int   $0x3
0x08050af1: main+0x0001: movl  %esp,%ebp
0x08050af3: main+0x0003: subl  $0x00000004,%esp
0x08050af6: main+0x0006: movl  0x0000000c(%ebp),%edx
0x08050af9: main+0x0009: movl  0x0000000c(%ebp),%eax
0x08050afc: main+0x000c: addl  $0x00000004,%eax
....

dtrace コマンドの適用前までは"pushl %ebp"であった関数冒頭部分に,"int $0x3" 命令が埋め込まれています。

上記の結果からわかるように,pidプロバイダを使用したDスクリプトを実行する際には,情報採取対象プロセスのプログラム領域の書き換えが発生しています。

一方で,exec()が実施された際にも,新たに実行されるプログラムの内容によって,プログラム領域が書き換えられます。

ということは,DTraceによる関数フロー採取のためのプログラム書き換えは,exec()の実施によって全て上書きされてしまいますから,exec()後のプロセスからは情報採取ができないことを意味します。

つまり,exec()後のプロセスから情報採取をしようとするなら,改めてDTraceによる対象プロセスのプログラム書き換えを実施する必要がある,というわけです。

dbxを使用している場合であれば,exec() が完了して新たなプログラムの実行が開始された段階で,プログラムの実行が一旦中断されます(図4の続きから継続する場合は,dbxcontコマンドを使用してください)⁠

図6 show_execの実行~その2

(dbx) run /usr/bin/ls -l /tmp ※ 実行開始
Running: show_exec /usr/bin/ls -l /tmp
(process id 12493)
dbx: process 12493 about to exec("/usr/bin/ls")
dbx: program "/usr/bin/ls" just exec'ed
dbx: to go back to the original program use "debug $oprog"
Reading ls
stopped in main at 0x080520ac ※ exec() 完了時点で停止
0x080520ac: main       :        pushl    %ebp
(dbx)

この時点で改めて dtraceコマンドを対象プロセス(上記の例ではプロセスID 12493)に適用することで,exec()後のプロセスからも情報採取を行うことができます。

なお,dbxの適用対象プロセスがexec()を実施した場合,dbxはそれ以後のデバッグ対象をexec()されたプログラムに変更します(上記の例では "/usr/bin/ls")⁠

exec()前の段階から再度実行し直したい場合は,デバッグ対象プログラムの変更を "debug $oprog" によってdbxに通知してください。

※3)
別プログラムを実行する exec() には,リスト7で使用しているexecv()などを含め,幾つかのバリエーションがありますが,ここでは全てをまとめて"exec()"と記述しています。

著者プロフィール

藤原克則(ふじわらかつのり)

Mercurial三昧の日々が嵩じて, いつの間にやら『入門Mercurial Linux/Windows対応』を上梓。凝り性なのが災いして,年がら年中アップアップな一介の実装屋。最近は仕事の縁が元で,OpenSolarisに入れ込む毎日。