AWESOME LINUX WORLD

Linux 成分多めの技術系ブログ

/proc/[pid]/stack でプロセスの待ち状態を確認する

プロセスの待ち状態は ps コマンドで確認できるが、これだけだと詳細は分からない。 詳細を知るには /proc/[pid]/stack が有効。今回はこれについてのメモ。

■ /proc/[pid]/stack とは

[pid] に紐づくプロセスのカーネルスタック内の関数呼び出しのシンボリックトレースを出力する。プロセスが待ち状態の時、具体的に何を待っているのか (何の関数を実行中か) を確認する際に便利。

(※ このファイルは、カーネルビルドオプション「CONFIG_STACKTRACE」が有効 (y) の場合のみに使用可能)

表示例

$ sudo cat /proc/$$/stack
[<0>] do_wait+0x1b3/0x220
[<0>] kernel_wait4+0x96/0x120
[<0>] do_syscall_64+0x5b/0xf0
[<0>] entry_SYSCALL_64_after_hwframe+0x44/0xa9

■ 待ち状態のプロセスを確認する

例として、イベントを待つプロセスを起動する。

ここでは、クライアントからの接続 (イベント) を待つサーバを nc コマンド*1で立てる。-l 12345 で nc プロセスが、12345 番ポートで Listen するように実行する。

$ nc -l 12345

次に、起動したプロセスの状態を確認する。

$ ps aux | grep "nc -l"
USER     PID     %CPU %MEM  SZ    RSS  TTY     STAT  START   TIME COMMAND
...
user     432678  0.0  0.0   8260  2132 pts/1    S+   19:59   0:00 nc -l 12345
...

STAT 列が S+ なので、割り込み可能なスリープであることが分かるが、実際に何を待ち合わせているのかは判断できない。そこで、/proc/[pid]/stack を使う。

$ sudo cat /proc/$(pidof nc)/stack
[<0>] do_select+0x573/0x7a0
[<0>] core_sys_select+0x168/0x310
[<0>] kern_select+0xcd/0x150
[<0>] __x64_sys_select+0x21/0x30
[<0>] do_syscall_64+0x5b/0xf0
[<0>] entry_SYSCALL_64_after_hwframe+0x44/0xa9

select システムコール (sys_select) により、最終的に do_select() で待ち合わせていることが分かる。sys_select() は、多重化された I/O との同期を図る関数である。

ソースコードを確認する

より詳細には、do_select() のソースコードを読む必要がある。厳密には使用環境ごとのディストリビューションのバージョンに沿ったソースコードを確認する必要があるが、ここでは、最新のアップストリームカーネル (v5.9-rc3 時点) で確認する。

まずは、プロセスの状態を遷移させる関数を探す。読んでいくと、★ の部分が目につく。 TASK_INTERRUPTIBLE とは、割り込み可能なスリープ状態のことなので、 この先で状態を遷移させていると推測できる。

static int do_select(int n, fd_set_bits *fds, struct timespec64 *end_time)
{
    ktime_t expire, *to = NULL;
    struct poll_wqueues table;
    ...
    u64 slack = 0;
    ...
    for (;;) {
        ...

        if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE, ★
                       to, slack))
            timed_out = 1;
    }
    ...
}

予想どおり、★1 でプロセスの状態を TASK_INTERRUPTIBLE へ遷移させている。 そして、★3 で実行可能状態 (TASK_RUNNING) に遷移させている。 つまり、★1 は実行されているが、★3 までは実行されていないということになる。

よって、★2 を読み進めていくことで、具体的に何を待ち合わせているのか、 ★2 へと進む条件は何であるのか、といったことの手掛かりを掴める可能性がある。

static int poll_schedule_timeout(struct poll_wqueues *pwq, int state,
              ktime_t *expires, unsigned long slack)
{
    int rc = -EINTR;

    set_current_state(state); ★1
    if (!pwq->triggered)
        rc = schedule_hrtimeout_range(expires, slack, HRTIMER_MODE_ABS); ★2
    __set_current_state(TASK_RUNNING); ★3

    ...
}

以上、このように読み進めていく *2

*1:詳細は ncat(1) を参照のこと

*2:schedule_hrtimeout_range() から先も記載したいところだが、解説するとなると深堀する必要があり、今回のお題に載せるにはやや冗長であると感じたため、割愛する