ハングアップしたプロセスのスタックを確認する方法

#1 はじめに
プロセスがハングアップした場合、その原因調査が必要になるときがあります。
「ハングアップ」とは、例えば、何らかの理由でI/Oが完了せず、I/O完了を待ち続けている状態のことです。
このようなとき、I/O完了待ちプロセスのスタックを調べると、原因究明につながることがあります。
ここでは、httpdプロセスのユーザモード、カーネルモードのスタックの確認方法を説明します。


#2 環境
VMware Workstation 14 Playerでゲストマシンを作成しました。
ゲストマシンのOS版数は以下のとおりです。

```
[root@centos74 ~]# cat /etc/redhat-release
CentOS Linux release 7.4.1708 (Core)

[root@centos74 ~]# uname -r
3.10.0-693.el7.x86_64
```

#3 事前準備
ここでは、httpdを使って、カーネルモード、ユーザモードのスタックを確認してみます。

##3.1 httpdパッケージのインストール
```
[root@centos74 ~]# yum -y install httpd
[root@centos74 ~]# systemctl start httpd
[root@centos74 ~]# ps -C httpd
   PID TTY          TIME CMD
  1095 ?        00:00:00 httpd
  1096 ?        00:00:00 httpd
  1097 ?        00:00:00 httpd
  1098 ?        00:00:00 httpd
  1099 ?        00:00:00 httpd
  1100 ?        00:00:00 httpd
```

##3.2 httpdのdebuginfoパッケージのインストール
httpdのユーザモードの関数名等を表示するため、httpdのdebuginfoパッケージをインストールします。

```
httpdパッケージの版数を確認する。
[root@centos74 ~]# rpm -qa|grep httpd
httpd-2.4.6-67.el7.centos.6.x86_64
httpd-tools-2.4.6-67.el7.centos.6.x86_64

httpdパッケージと同じ版数のdebuginfoパッケージをインストールする。
[root@centos74 ~]# yum --enablerepo="base-debuginfo" -y install httpd-debuginfo-2.4.6-67.el7.centos.6.x86_64

パッケージを確認する。httpd-debuginfoがインストールされたことがわかる。
[root@centos74 ~]# rpm -qa|grep httpd
httpd-2.4.6-67.el7.centos.6.x86_64
httpd-debuginfo-2.4.6-67.el7.centos.6.x86_64
httpd-tools-2.4.6-67.el7.centos.6.x86_64
```

##3.3 aprのdebuginfoパッケージのインストール
gstackコマンド(後述)を使うと、スタックトーレスに"libapr"という文字列が表示されました。
httpdがaprの関数を呼び出しているため、aprのdebuginfoパッケージをインストールしました。

```
aprパッケージの版数を確認する。
[root@centos74 ~]# rpm -qa|grep apr
apr-util-1.5.2-6.el7.x86_64
apr-1.4.8-3.el7_4.1.x86_64

aprパッケージと同じ版数のdebuginfoパッケージをインストールする。
[root@centos74 ~]# yum --enablerepo="base-debuginfo" -y install apr-debuginfo-1.4.8-3.el7.x86_64

パッケージを確認する。apr-debuginfoがインストールされたことがわかる。
[root@centos74 ~]# rpm -qa|grep apr
apr-util-1.5.2-6.el7.x86_64
apr-1.4.8-3.el7_4.1.x86_64
apr-debuginfo-1.4.8-3.el7.x86_64
```

##3.4 glibcのdebuginfoパッケージのインストール
ユーザモードのプロセスは、glibcの関数を経由してシステムコールを間接的に呼び出しています。
したがって、glibcのdebuginfoパッケージをインストールしました。

```
glibcパッケージの版数を確認する。
[root@centos74 ~]# rpm -qa|grep glibc
glibc-common-2.17-196.el7.x86_64
glibc-headers-2.17-196.el7.x86_64
glibc-debuginfo-2.17-196.el7.x86_64
glibc-2.17-196.el7.x86_64

glibcパッケージと同じ版数のdebuginfoパッケージをインストールする。
[root@centos74 ~]# yum --enablerepo="base-debuginfo" -y install glibc-debuginfo-2.17-196.el7.x86_64

パッケージを確認する。glibc-debuginfoがインストールされたことがわかる。
[root@centos74 ~]# rpm -qa|grep glibc
glibc-common-2.17-196.el7.x86_64
glibc-headers-2.17-196.el7.x86_64
glibc-debuginfo-2.17-196.el7.x86_64
glibc-2.17-196.el7.x86_64
glibc-devel-2.17-196.el7.x86_64
glibc-debuginfo-common-2.17-196.el7.x86_64
```

#4 カーネルモードのスタック確認方法
##4.1 psコマンドによる確認方法
psコマンドを使って、httpdプロセスのPID,状態等を確認します。
このとき、psコマンドの出力フォーマットとしてwchanを指定します。
wchanは、プロセスがI/O完了を待つために呼び出したカーネル関数を表示します。

pid=1095のhttpdは、poll_schedule_timeoutを呼び出してI/O完了待ちであることがわかります。
他のhttpdは、inet_csk_acceptを呼び出してI/O完了待ちであることがわかります。
なお、psコマンドの使い方は、[ここ(psコマンドの使い方)](https://qiita.com/hana_shin/items/a1c5400458f8ca04a809)にあります。

```
[root@centos74 ~]# ps -C httpd -o comm,pid,ppid,s,wchan
COMMAND            PID   PPID S WCHAN
httpd             1095      1 S poll_schedule_timeout
httpd             1096   1095 S inet_csk_accept
httpd             1097   1095 S inet_csk_accept
httpd             1098   1095 S inet_csk_accept
httpd             1099   1095 S inet_csk_accept
httpd             1100   1095 S inet_csk_accept
```

##4.2 /procファイルシステムによる確認方法
psコマンドだと、I/O完了待ちをしている関数しかわかりません。
I/O完了待ちに至る関数呼び出しは、`/proc/<pid>/stack`を確認する必要があります。

ここでは、httpd(pid=1096)プロセスのカーネルモードのスタックを表示してみます。
最終的に、inet_csk_acceptを呼び出していることがわかります。
そして、次に実行する命令のアドレスがffffffff815d65a0であることがわかります。

```console:httpd(pid=1096)のカーネルモードのスタック
[root@centos74 ~]# cat /proc/1096/stack
[<ffffffff815d65a0>] inet_csk_accept+0x230/0x2d0
[<ffffffff8160597c>] inet_accept+0x3c/0x120
[<ffffffff81569e83>] SYSC_accept4+0x103/0x220
[<ffffffff8156b26e>] SyS_accept4+0xe/0x10
[<ffffffff816b4fc9>] system_call_fastpath+0x16/0x1b
[<ffffffffffffffff>] 0xffffffffffffffff
```



#5 ユーザモードのスタック確認方法
次に、ユーザモードのスタックを確認してみます。
ユーザモードのスタックはgstackコマンドを使って確認します。
main関数を呼び出したあと、最終的にaccept4関数を呼び出していることがわかります。
ただし、apr_socket_acceptの引数が表示されません。理由は現時点で不明。判明次第記載予定。
(他にインストールが必要なdebuginfoがあるのか???)

```console:gstackによるスタックの表示(pid=1096)
[root@centos74 ~]# gstack 1096
#0  0x00007f065a5d5787 in accept4 (fd=4, addr=..., addr_len=0x7fff8bc22a40, flags=524288) at ../sysdeps/unix/sysv/linux/accept4.c:33
#1  0x00007f065acdd43a in apr_socket_accept () from /lib64/libapr-1.so.0
#2  0x00005624e3760123 in ap_unixd_accept (accepted=0x7fff8bc22bc8, lr=0x5624e45be068, ptrans=<optimized out>) at unixd.c:301
#3  0x00007f0651c1d79f in child_main (child_num_arg=child_num_arg@entry=0) at prefork.c:685
#4  0x00007f0651c1da55 in make_child (s=0x5624e45c0330, slot=slot@entry=0) at prefork.c:810
#5  0x00007f0651c1dab6 in startup_children (number_to_start=5) at prefork.c:828
#6  0x00007f0651c1e7c0 in prefork_run (_pconf=<optimized out>, plog=0x5624e45c6378, s=0x5624e45c0330) at prefork.c:986
#7  0x00005624e372915e in ap_run_mpm (pconf=pconf@entry=0x5624e4599158, plog=0x5624e45c6378, s=0x5624e45c0330) at mpm_common.c:96
#8  0x00005624e3721ed6 in main (argc=2, argv=0x7fff8bc22fd8) at main.c:783
```

#6 カーネルモードのスタック確認方法(詳細)
crashコマンドを使って、もう少し詳しく、カーネルモードのスタックを確認してみます。
カーネル関数を表示するため、kernel-debuginfoパッケージをインストールします。
なお、crashコマンドのインストール、使い方は[ここ(crashコマンドの使い方)](https://qiita.com/hana_shin/items/02d8da181c8287580613)を参照してください。

##6.1 kernel-debuginfoパッケージのインストール

```
kernelパッケージの版数を確認する。
[root@centos74 ~]# rpm -qa|grep kernel
kernel-headers-3.10.0-693.el7.x86_64
kernel-tools-libs-3.10.0-693.el7.x86_64
kernel-3.10.0-693.el7.x86_64
kernel-tools-3.10.0-693.el7.x86_64
kernel-devel-3.10.0-693.el7.x86_64

kernelパッケージと同じ版数のdebuginfoパッケージをインストールする。
[root@centos74 ~]# yum --enablerepo=base-debuginfo -y install kernel-debuginfo-3.10.0-693.el7.x86_64

パッケージを確認する。kernel-debuginfoがインストールされたことがわかる。
[root@centos74 ~]# rpm -qa|grep kernel
kernel-headers-3.10.0-693.el7.x86_64
kernel-tools-libs-3.10.0-693.el7.x86_64
kernel-3.10.0-693.el7.x86_64
kernel-tools-3.10.0-693.el7.x86_64
kernel-debuginfo-common-x86_64-3.10.0-693.el7.x86_64
kernel-devel-3.10.0-693.el7.x86_64
kernel-debuginfo-3.10.0-693.el7.x86_64
```

##6.2 I/Oの完了待ちをしている場所の確認
crashコマンドを実行して、ffffffff815d65a0付近の逆アセンブラを表示してみます。
inet_csk_acceptより、schedule_timeoutを呼び出していることがわかります。
ffffffff815d65a0は、I/Oが完了したら、次に実行する命令のアドレスです。

```console:crashコマンドによる確認
[root@centos74 ~]# crash

disコマンドを実行する。-lはソースの行番号を表示するオプションです。
crash> dis -l inet_csk_accept
(中略)
/usr/src/debug/kernel-3.10.0-693.el7/linux-3.10.0-693.el7.x86_64/include/linux/spinlock.h: 351
0xffffffff815d6580 <inet_csk_accept+528>:       mov    0x3f8(%r15),%rax
0xffffffff815d6587 <inet_csk_accept+535>:       lea    0x10(%rax),%rdi
0xffffffff815d658b <inet_csk_accept+539>:       callq  0xffffffff816ab9d0 <_raw_spin_unlock_bh>
0xffffffff815d6590 <inet_csk_accept+544>:       jmpq   0xffffffff815d63f0 <inet_csk_accept+128>
0xffffffff815d6595 <inet_csk_accept+549>:       nopl   (%rax)
/usr/src/debug/kernel-3.10.0-693.el7/linux-3.10.0-693.el7.x86_64/net/ipv4/inet_connection_sock.c: 287
0xffffffff815d6598 <inet_csk_accept+552>:       mov    %rbx,%rdi
0xffffffff815d659b <inet_csk_accept+555>:       callq  0xffffffff816a6da0 <schedule_timeout>
0xffffffff815d65a0 <inet_csk_accept+560>:       mov    %rax,%rbx
```

##6.3 ソースコードの確認
crashコマンドの実行結果より、I/O完了待ちの場所が287行目ということがわかる。
287行目は、schedule_timeoutを呼び出していることがわかります。

```console:inet_csk_accept@net/ipv4/inet_connection_sock.c
 309 struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
 310 {

 335                 error = inet_csk_wait_for_connect(sk, timeo);

 367 }
```

```console:inet_csk_wait_for_connect@net/ipv4/inet_connection_sock.c
 262 static int inet_csk_wait_for_connect(struct sock *sk, long timeo)
 263 {
 264         struct inet_connection_sock *icsk = inet_csk(sk);
 265         DEFINE_WAIT(wait);
 266         int err;
 267 
 268         /*
 269          * True wake-one mechanism for incoming connections: only
 270          * one process gets woken up, not the 'whole herd'.
 271          * Since we do not 'race & poll' for established sockets
 272          * anymore, the common case will execute the loop only once.
 273          *
 274          * Subtle issue: "add_wait_queue_exclusive()" will be added
 275          * after any current non-exclusive waiters, and we know that
 276          * it will always _stay_ after any new non-exclusive waiters
 277          * because all non-exclusive waiters are added at the
 278          * beginning of the wait-queue. As such, it's ok to "drop"
 279          * our exclusiveness temporarily when we get woken up without
 280          * having to remove and re-insert us on the wait queue.
 281          */
 282         for (;;) {
 283                 prepare_to_wait_exclusive(sk_sleep(sk), &wait,
 284                                           TASK_INTERRUPTIBLE);
 285                 release_sock(sk);
 286                 if (reqsk_queue_empty(&icsk->icsk_accept_queue))
 287                         timeo = schedule_timeout(timeo);　★
 288                 lock_sock(sk);
 289                 err = 0;
 290                 if (!reqsk_queue_empty(&icsk->icsk_accept_queue))
 291                         break;
 292                 err = -EINVAL;
 293                 if (sk->sk_state != TCP_LISTEN)
 294                         break;
 295                 err = sock_intr_errno(timeo);
 296                 if (signal_pending(current))
 297                         break;
 298                 err = -EAGAIN;
 299                 if (!timeo)
 300                         break;
 301         }
 302         finish_wait(sk_sleep(sk), &wait);
 303         return err;
 304 }
```

#7 補足
debuginfoパッケージインストールすると、/usr/src/debug配下にソースが展開されます。

```console:glibcの場合
[root@centos74 glibc-2.17-c758a686]# pwd
/usr/src/debug/glibc-2.17-c758a686

[root@centos74 glibc-2.17-c758a686]# ls -F
argp/                       ctype/   gshadow/    io/      misc/     pwd/       shadow/        sunrpc/    wctype/
assert/                     debug/   hesiod/     libidn/  nis/      releng/    signal/        sysdeps/
bits/                       dirent/  iconv/      libio/   nptl/     resolv/    socket/        sysvipc/
build-x86_64-redhat-linux/  dlfcn/   iconvdata/  locale/  nptl_db/  resource/  stdio-common/  termios/
catgets/                    elf/     include/    login/   nscd/     rt/        stdlib/        time/
crypt/                      gmon/    inet/       malloc/  nss/      rtkaio/    streams/       timezone/
csu/                        grp/     intl/       math/    posix/    setjmp/    string/        wcsmbs/
```

```console:aprの場合
[root@centos74 apr-1.4.8]# pwd
/usr/src/debug/apr-1.4.8

[root@centos74 apr-1.4.8]# ls -F
atomic/  file_io/  locks/   misc/  network_io/  poll/    shmem/    support/  threadproc/  user/
dso/     include/  memory/  mmap/  passwd/      random/  strings/  tables/   time/
```

#8 まとめ
httpdプロセス(pid=1096)は、ユーザモード、カーネルモードにおいて、
下記のように関数を呼び出して、I/O完了待ちの状態になっていることがわかりました。

```
main                                                            -*-
 + ap_run_mpm                                                    |
    + prefork_run                                                |
       + startup_children                                        |
          + make_child                                ユーザモードのスタック
             + child_main                                        |
                + ap_unixd_accept                                |
                   + apr_socket_accept                           |
                      + accept4                                 -*-

                         + system_call_fastpath                 -*-
                            + SyS_accept4                        |
                               + SYSC_accept4          カーネルモードのスタック
                                  + inet_accept                  |
                                     + inet_csk_accept           |
                                        + schedule_timeout      -*-
```


#X 参考情報
[crashコマンドの使い方](https://qiita.com/hana_shin/items/02d8da181c8287580613)
[psコマンドの使い方](https://qiita.com/hana_shin/items/a1c5400458f8ca04a809)
[今さらですがyum](https://qiita.com/hana_shin/items/380b0a37726c05b8623f)