通过/proc查看Linux内核态调用栈来定位问题

http://www.tianchao.de/unix%20as%20ide/2013/08/05/using-proc-filesystem-for-troubleshooting.html

 

前几天碰到一个问题:一个进程运行过程中挂死了,把gdb挂上去之后bt打印的内容为空,后来通过查看 /proc 文件系统,查看程的调用栈,才发现是发消息给内核态程序时,内核态一直没有响应,导致用户态进程挂死。刚好在网上看到一篇描述通过 /proc 文件系统来定位问题的文章,这篇文章讲解得比较清楚,因此尝试翻译出来。原文地址:Peeking into Linux kernel-land using /proc filesystem for quick’n'dirty troubleshooting

这篇博客是基于现代Linux的。换句话说,是RHEL6所对应的2.6.3x内核版本,而不是古老的RHEL5所对应的2.6.18内核版本(神马玩意儿?!),很不幸是后者才是企业中最常见的版本。并且,在这里我不打算使用内核调试器或者SystemTap脚本,只使用平凡而古老的cat /proc/PID/xyz,而不是那些便捷的/proc文件系统工具。

定位一个“运行慢”的进程

我打算介绍一个系统性定位问题的例子,我在手提电脑上重现了这个例子。一个DBA想知道为什么他的find命令运行起来"非常慢",并且很长时间都没有返回任何结果。了解环境之后,我对这个问题的起因有一个直觉的答案,但是他问我,对于这种正在发生中的问题,有没有系统性的方法立刻进行定位。

幸运的是,这个系统运行的是OEL6,因此刚好有一个新内核。确切的说2.6.39 UEK2。

那么,让我们试着定位一下。首先,看看find进程是否还活着:

[root@oel6 ~]# ps -ef | grep find
root     27288 27245  4 11:57 pts/0 00:00:01 find . -type f
root     27334 27315  0 11:57 pts/1 00:00:00 grep find

是的,他还在 —— PID 27288 (在整个定位问题的过程中我将会一直使用这个pid)。

让我们从最基本的开始,先看下这个进程的瓶颈在什么地方 —— 如果不是被什么操作阻塞的话(例如从缓存中读取需要的数据),CPU占用率应该是100%。如果瓶颈是IO或者连接问题,CPU占用率应该很低,或者就是0%。

[root@oel6 ~]# top -cbp 27288
top - 11:58:15 up 7 days,  3:38,  2 users,  load average: 1.21, 0.65, 0.47
Tasks:   1 total,   0 running,   1 sleeping,   0 stopped,   0 zombie
Cpu(s):  0.1%us,  0.1%sy,  0.0%ni, 99.8%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Mem:   2026460k total,  1935780k used,  90680k free,    64416k buffers
Swap:  4128764k total,   251004k used,  3877760k free,   662280k cached

  PID USER    PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
27288 root    20   0  109m 1160  844 D  0.0  0.1   0:01.11 find . -type f

top的结果显示这个进程的CPU占用率是0%,或者非常接近0%(因此输出被四舍五入为0%)。这两种情况实际上有着重要的差别,一种情况是进程完全挂死,根本没有机会获得CPU,另一种情况是进程不时的退出等待状态(例如,某些轮询操作不时的超时,而进程选择继续sleep)。因此,Linux上的top并不是一个适合显示这种差别的工具 —— 但是至少我们知道了进程并不是占用了大量的CPU。

让我们用其他命令试试。通常当一个进程看起来好像挂死时(0%的CPU占用率通常意味着进程挂在某些阻塞性的系统调用上 —— 这会导致内核让进程进入休眠状态),我会在这个进程上运行strace来跟踪进程挂在哪个系统调用上。同样的,如果进程并没有完全挂死,而是不时的从系统调用中返回并且被短暂的唤醒,这种情况也会呈现在strace中(阻塞性的系统调用将会完成并很快的再次进入):

[root@oel6 ~]# strace -cp 27288
Process 27288 attached - interrupt to quit

^C
^Z
[1]+  Stopped                 strace -cp 27288

[root@oel6 ~]# kill -9 %%
[1]+  Stopped                 strace -cp 27288
[root@oel6 ~]# 
[1]+  Killed                  strace -cp 27288

天啊,strace命令也挂住了!strace很长时间都没有打印任何东西,并也不能响应CTRL+C,因此我不得不用CTRL+Z,并杀死它。简单的诊断手段就这些了。

让我们再试试pstack(在Linux上,pstack就是GDB调试器的一个shell包装)。尽管pstack并不能查看内核态信息,它仍然能够告诉我们是哪个系统调用被执行了(通常,有一个相应的libc库调用显示在用户态堆栈的顶端上):

[root@oel6 ~]# pstack 27288

^C
^Z
[1]+  Stopped                pstack 27288

[root@oel6 ~]# kill %%
[1]+  Stopped                pstack 27288
[root@oel6 ~]# 
[1]+  Terminated              pstack 27288

pstatck也挂死了,什么都没返回!

因此,我们还是不知道我们的进程是100%(无可救药的)挂死了还是99.99%的挂住了(进程还在运行只是在睡眠) —— 以及在哪儿挂住了。

好了,还有别的可以看吗?还有一个更普通的东西可以坚持 —— 进程状态和WCHAN字段,可以通过古老而美好的ps(也许我早就应该运行这个命令,以确认进程到底是不是僵死了):

[root@oel6 ~]# ps -flp 27288
F S UID     PID  PPID  C PRI  NI ADDR SZ **WCHAN**  STIME TTY         TIME CMD
0 D root     27288 27245  0  80   0 - 28070 **rpc_wa** 11:57 pts/0  00:00:01 find . -type f

你应该多运行几次ps命令,以确保进程一直是同一个状态(你肯定不想被一个偶然的单独采样所误导),为了简洁一点这里只显示一次结果。

进程状态是D(不可中断睡眠状态,也就是不会被任何外部信号唤醒),这个状态通常与磁盘IO相关(ps帮助上也这样说)。并且WCHAN字段(表示导致进程睡眠或者等待的函数)被截断了一点。我可以用ps选项(参考帮助)把这个字段打印得跟宽一点,但是既然这个信息是来自proc文件系统,就让我们直接到源头去查询吧(再强调一次,既然我们不确定我们的进程到底是完全挂死了还是仅仅只是经常处于睡眠状态,那么最好把这个命令多执行几次以获取多次采样结果):

[root@oel6 ~]# cat /proc/27288/wchan
rpc_wait_bit_killable

嗯,进程是在等待某个RPC调用。RPC通常意味着进程是在和其它进程通信(可能是本地服务进程或者远程服务进程)。但是我们还是不知道为什么挂住。

进程有什么活动或者完全挂死了?

在我们进入这篇文章中真正有营养的部分之前,让我们先弄清楚进程到底有没有完全挂死。在最新的系统内核上/proc/PID/status 可以告诉我们答案:

[root@oel6 ~]# cat /proc/27288/status 
Name:   find
State:  D (disk sleep)
Tgid:   27288
Pid:    27288
PPid:   27245
TracerPid:  0
Uid:    0   0   0   0
Gid:    0   0   0   0
FDSize: 256
Groups: 0 1 2 3 4 6 10 
VmPeak:   112628 kB
VmSize:   112280 kB
VmLck:         0 kB
VmHWM:      1508 kB
VmRSS:      1160 kB
VmData:      260 kB
VmStk:       136 kB
VmExe:       224 kB
VmLib:      2468 kB
VmPTE:        88 kB
VmSwap:        0 kB
Threads:    1
SigQ:   4/15831
SigPnd: 0000000000040000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000000000
SigCgt: 0000000180000000
CapInh: 0000000000000000
CapPrm: ffffffffffffffff
CapEff: ffffffffffffffff
CapBnd: ffffffffffffffff
Cpus_allowed:   ffffffff,ffffffff
Cpus_allowed_list:  0-63
Mems_allowed:   
00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,
00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,
00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,
00000000,00000000,00000000,00000000,00000001
Mems_allowed_list:  0
voluntary_ctxt_switches:    9950
nonvoluntary_ctxt_switches: 17104

进程状态是D —— Disk Sleep(不可中断睡眠)。然后看看voluntaryctxtswitchesnonvoluntaryctxtswitches的数值 —— 它可以告诉你进程占用(或者释放)了多少次CPU。等几秒钟之后,再次执行该命令,看看这些数值有没有增加。在我这个案例中,这些数值没有增加,据此我可以得出结论,这个进程是完全挂死了(额,至少在执行命令的这几秒钟内是完全挂死的)。所以,现在我更有信心认为这个进程是完全挂死了(而不是在飞行在雷达探测不到地带 —— 在0.04%以下的低CPU占用率下运行)。

顺便说一句,有两个地方可以获得上下文切换次数(并且第二种方法还可以在老的系统内核上工作):

[root@oel6 ~]# cat /proc/27288/sched
find (27288, #threads: 1)
---------------------------------------------------------
se.exec_start                      :     617547410.689282
se.vruntime                        :       2471987.542895
se.sum_exec_runtime                :          1119.480311
se.statistics.wait_start           :             0.000000
se.statistics.sleep_start          :             0.000000
se.statistics.block_start          :     617547410.689282
se.statistics.sleep_max            :             0.089192
se.statistics.block_max            :         60082.951331
se.statistics.exec_max             :             1.110465
se.statistics.slice_max            :             0.334211
se.statistics.wait_max             :             0.812834
se.statistics.wait_sum             :           724.745506
se.statistics.wait_count           :                27211
se.statistics.iowait_sum           :             0.000000
se.statistics.iowait_count         :                    0
se.nr_migrations                   :                  312
se.statistics.nr_migrations_cold   :                    0
se.statistics.nr_failed_migrations_affine:                    0
se.statistics.nr_failed_migrations_running:                   96
se.statistics.nr_failed_migrations_hot:                 1794
se.statistics.nr_forced_migrations :                  150
se.statistics.nr_wakeups           :                18507
se.statistics.nr_wakeups_sync      :                    1
se.statistics.nr_wakeups_migrate   :                  155
se.statistics.nr_wakeups_local     :                18504
se.statistics.nr_wakeups_remote    :                    3
se.statistics.nr_wakeups_affine    :                  155
se.statistics.nr_wakeups_affine_attempts:                  158
se.statistics.nr_wakeups_passive   :                    0
se.statistics.nr_wakeups_idle      :                    0
avg_atom                           :             0.041379
avg_per_cpu                        :             3.588077
nr_switches                        :                27054
nr_voluntary_switches              :                 9950
nr_involuntary_switches            :                17104
se.load.weight                     :                 1024
policy                             :                    0
prio                               :                  120
clock-delta                        :                   72

你需要看看nr_switchs的数值(等于nrvoluntaryswitches +nrinvoluntaryswitches)。

在上面的输出中,总的nr_switches次数是27054,这个值同时也是/proc/PID/schedstat的结果中的第3个字段。

[root@oel6 ~]# cat /proc/27288/schedstat 
1119480311 724745506 27054

并且它不会增加...

用/proc文件系统查看内核态信息

那么,看起来我们的进程很漂亮的挂死了:)stracepstatck都没有用武之地。它们使用ptrace()系统调用来附着到进程上,并查看进程的内存,但是由于进程绝望的挂死了,很可能挂在某个系统调用上,因此我猜测ptrace()调调本身也被挂住了。(顺便说一句,我试过strace那个附着到目标进程的strace进程,结果目标进程崩溃了。记着我警告过你:)。)

那么,怎么看到底挂在哪个系统调用上呢 —— 没法用strace或者pstack?幸运的是我运行的是现代的操作系统内核 —— 跟/proc/PID/syscall打个招呼吧!

[root@oel6 ~]# cat /proc/27288/syscall
262 0xffffffffffffff9c 0x20cf6c8 0x7fff97c52710 0x100 0x100 0x676e776f645f616d 0x7fff97c52658 0x390e2da8ea

好了,我可以拿他干嘛呢? 嗯,这些数字代表某些东西。如果它是一个"0x很大的数",它通常表示一个内存地址(并且,pmap之类的工具可以用来查看它指向那里);但是如果是一个很小的数字,那么很可能是一个数组索引 —— 例如打开的文件描述符数组(可以从/prco/PID/fd读取到),或者是当前进程正在执行的系统调用号 —— 既然在这个例子中,我们正在处理系统调用。那么,这个进程是挂死在#262号系统调用上吗?

注意在不同的OS类型、版本或者平台之间,系统调用号可能不同,因此你需要看看对应的OS上的.h文件。通常应该在/usr/include中搜索"syscall*"。在我的Linux上,系统调用定义在/usr/include/asm/unistd_64.h中:

[root@oel6 ~]# grep 262 /usr/include/asm/unistd_64.h 
#define __NR_newfstatat             262

找到了!系统调用262是某个叫做newfstatat的东西。打开手册看看它到底是什么。关于系统调用名称有一个小小的技巧 —— 如果在手册中找不到这个系统调用,试试去掉后缀或者前缀(例如,用man pread代替man pread64)—— 在这个例子中,查找时去掉"new" ——man fstata。或者直接google。

无论如何,系统调用"new-fstat-at"允许你读取文件属性,非常像通常的"stat"系统调用。那么我们挂在这个文件元数据读取操作上。我们前进了一步,但是仍然不知道为什么会挂在这儿?

好了,跟我的小朋友/proc/PID/statck打个招呼吧,使用它可以读取进程的内核堆栈的调试信息:

[root@oel6 ~]# cat /proc/27288/stack
[] rpc_wait_bit_killable+0x24/0x40 [sunrpc]
[] __rpc_execute+0xf5/0x1d0 [sunrpc]
[] rpc_execute+0x43/0x50 [sunrpc]
[] rpc_run_task+0x75/0x90 [sunrpc]
[] rpc_call_sync+0x42/0x70 [sunrpc]
[] nfs3_rpc_wrapper.clone.0+0x35/0x80 [nfs]
[] nfs3_proc_getattr+0x47/0x90 [nfs]
[] __nfs_revalidate_inode+0xcc/0x1f0 [nfs]
[] nfs_revalidate_inode+0x36/0x60 [nfs]
[] nfs_getattr+0x5f/0x110 [nfs]
[] vfs_getattr+0x4e/0x80
[] vfs_fstatat+0x70/0x90
[] sys_newfstatat+0x24/0x50
[] system_call_fastpath+0x16/0x1b
[] 0xffffffffffffffff

最上面的函数就是在内核代码中挂住的地方 —— 它跟WCHAN输出完全吻合(注意,实际上有更多的函数在调用栈上,例如内核scheduler()函数,它使进程休眠或者唤醒进程,但是这些函数没有显示出来,很可能是因为它们是等待条件的结果而不是原因)。

感谢它打印出了完整的内核态堆栈,我们可以从下而上的看一下函数调用,从而理解是怎么最终调用到rpc_wait_bit_killable的,这个函数结束了对调度器的调用并使进程进入睡眠模式。

底端的system_call_fastpath是一个通用的内核调用处理函数,它为我们处理过的newfstatat系统调用执行内核代码。然后继续向上,我们可以看到好几个NFS函数。这是100%无可抵赖的证据,证明我们处在某些NFS代码路径下(under NFS codepath)。我没有说在NFS代码路径中(in NFS codepath),当你继续向上看的时候,你会看到最上面的NFS函数接着调用了某些RPC函数(rpc_call_sync)以便跟其它进程通信 —— 在这个例子中可能是[kworker/N:N]、 [nfsiod]、 [lockd] 或者 [rpciod]内核IO线程。并且因为某些原因一直没有从这些线程收到应答(通常的怀疑点是网络连接丢失、数据包丢失或者仅仅是网络连通性问题)。

要想看看到底是哪个辅助线程挂在网络相关的代码上,你同样可以收集内核堆栈信息,尽管kworkers做的事情远不止NFS RPC通信。在另外一个单独的试验中(只是通过NFS拷贝一个大文件),我抓取到了一个kworkder在网络代码中等待的信息:

[root@oel6 proc]# for i in `pgrep worker` ; do ps -fp $i ; cat /proc/$i/stack ; done
UID     PID  PPID  C STIME TTY        TIME CMD
root        53   2  0 Feb14 ?       00:04:34 [kworker/1:1]

[] __cond_resched+0x2a/0x40
[] lock_sock_nested+0x35/0x70
[] tcp_sendmsg+0x29/0xbe0
[] inet_sendmsg+0x48/0xb0
[] sock_sendmsg+0xef/0x120
[] kernel_sendmsg+0x41/0x60
[] xs_send_kvec+0x8e/0xa0 [sunrpc]
[] xs_sendpages+0x173/0x220 [sunrpc]
[] xs_tcp_send_request+0x5d/0x160 [sunrpc]
[] xprt_transmit+0x83/0x2e0 [sunrpc]
[] call_transmit+0xa8/0x130 [sunrpc]
[] __rpc_execute+0x66/0x1d0 [sunrpc]
[] rpc_async_schedule+0x15/0x20 [sunrpc]
[] process_one_work+0x13e/0x460
[] worker_thread+0x17c/0x3b0
[] kthread+0x96/0xa0
[] kernel_thread_helper+0x4/0x10

如果准确的知道哪个内核线程在和其它内核线程通信,就有可能打开内核跟踪,但是在这篇文章中我不想走到那一步  —— 这篇文章的描述的是一个实践性的、简单的问题定位练习!

诊断和"修复"

无论如何,感谢新Linux内核提供的内核堆栈信息收集方法(我不知道到底是在哪个具体版本引入的),使我们得以系统性的找出find命令到底挂在哪儿 —— 在Linux内核的NFS代码里。并且当你雏形NFS相关的挂起时,最通常的怀疑点是网络问题。如果你想知道我是怎么重现出这个问题的,我从一个虚拟机里挂载了一个NFS卷,然后启动find命令,接着挂起虚拟机。这种操作导致了与网络(配置、防火墙)问题相同的症状,例如使一个网络连接默默的断开,而不通知TCP端点,或者因某种原因使数据包无法送达。

既然在堆栈最顶端的函数是一个可杀死的、可安全杀死的函数(rpc_wait_bit_killable),我们可以用kill -9杀死它:

[root@oel6 ~]# ps -fp 27288
UID     PID  PPID  C STIME TTY        TIME CMD
root     27288 27245  0 11:57 pts/0 00:00:01 find . -type f
[root@oel6 ~]# kill -9 27288

[root@oel6 ~]# ls -l /proc/27288/stack
ls: cannot access /proc/27288/stack: No such file or directory

[root@oel6 ~]# ps -fp 27288
UID     PID  PPID  C STIME TTY        TIME CMD
[root@oel6 ~]#

进程不见了。

穷人的内核线程分析

/proc/PID/stack看起来就像一个简单的文本proc文件,你一样可以在内核线程上进行穷人的堆栈分析!下面这个例子演示了如何收集当前系统调用和内核堆栈信息,以及如何以穷人的方式集成进一个半层次化的分析器:

[root@oel6 ~]# export LC_ALL=C ; for i in {1..100} ; do cat /proc/29797/syscall | awk '{ print $1 }' ; 
cat /proc/29797/stack | /home/oracle/os_explain -k ; usleep 100000 ; done | sort -r | uniq -c 
     69 running
      1 ffffff81534c83
      2 ffffff81534820
      6 247
     25 180

    100    0xffffffffffffffff 
      1     thread_group_cputime 
     27     sysenter_dispatch 
      3     ia32_sysret 
      1      task_sched_runtime 
     27      sys32_pread 
      1      compat_sys_io_submit 
      2      compat_sys_io_getevents 
     27       sys_pread64 
      2       sys_io_getevents 
      1       do_io_submit 
     27        vfs_read 
      2        read_events 
      1        io_submit_one 
     27         do_sync_read 
      1         aio_run_iocb 
     27          generic_file_aio_read 
      1          aio_rw_vect_retry 
     27           generic_file_read_iter 
      1           generic_file_aio_read 
     27            mapping_direct_IO 
      1            generic_file_read_iter 
     27             blkdev_direct_IO 
     27              __blockdev_direct_IO 
     27               do_blockdev_direct_IO 
     27                dio_post_submission 
     27                 dio_await_completion 
      6                  blk_flush_plug_list

它给出关于进程在内核中的什么地方耗费时间的粗略信息。上面的一段单独列出了系统调用号的信息 —— “running”表示进程处于用户态(而不是在系统调用中)。因此,在收集信息期间,69%的时间进程跑在用户态。25%的时间花在#180号系统调用上(在我的系统上是nfsservctl),而6%的时间花在#247号系统调用上(waitid)。

在这个输出里还可以看到更多的“函数” —— 但是由于某些原因它们没有被恰当的翻译城函数名称。嗯,这个地址应该代表某些东西,因此我们手工碰碰运气:

[root@oel6 ~]# cat /proc/kallsyms | grep -i ffffff81534c83
ffffffff81534c83 t ia32_sysret

看起来这些信息是一个32位架构兼容的系统调用的返回函数 —— 但是这个函数本身不是一个系统调用(只是一个内部的辅助函数),也许这就是为什么/proc/stack没有翻译它。也许显示地址是因为在/proc视图上没有“读一致性”,当时属主线程修改了这些内存结构和入口,读线程可能读取了不稳定的数据。

让我们也检查一下其它地址:

[root@oel6 ~]# cat /proc/kallsyms | grep -i ffffff81534820
[root@oel6 ~]#

什么都没有?嗯,然而问题定位并不是一定得终止 —— 让我们看看这个地址附近有没有其它有趣得信息。我仅仅移走了地址尾部的两个字符:

[root@oel6 ~]# cat /proc/kallsyms | grep -i ffffff815348 
ffffffff8153480d t sysenter_do_call
ffffffff81534819 t sysenter_dispatch
ffffffff81534847 t sysexit_from_sys_call
ffffffff8153487a t sysenter_auditsys
ffffffff815348b9 t sysexit_audit

似乎sysenter_dispatch函数是在/proc/PID/stack输出的原始地址前1个字节开始的。因此我们很可能已经执行了一个字节(可能是一个为了动态跟踪探针陷阱而留下的NOP操作)。但是,似乎这些堆栈信息都是在system_dispatch函数内,它本身不是一个系统调用,而是一个系统调用辅助函数。

更多关于堆栈分析器的信息

注意有不同类型的堆栈采集器 —— Linux Perf、Oprofile和Solaris DTrace用于采集当前正在运行的线程的指令指针寄存器(32位Intel CPU上的EIP,或者x64上的RIP)和堆栈指针寄存器(32位CPU上的ESP,和64位CPU上的RSP)。因此,这些工具只显示了在采集信息时恰好在CPU上运行的线程的信息!当定位高CPU占用率问题时,这是很完美的,但是对于定位挂死的进程或者长时间睡眠或者等待的进程,却一点用也没有,

Linux、Solaris、HP-UX上的pstack工具,AIX上的procstack工具,ORADEBUG SHORT_STACK工具,以及直接读取/proc/PID/stack文件,为CPU分析工具提供了一个很好的附加(而不是替代)工具。如果进程正在睡眠,不是在CPU上运行,可以从存储的上下文信息中读取堆栈的起始点 —— 在上下文切换时OS调度器把上下文信息存储到了内核内存中。

当然,CPU事件分析工具通常可以做得比pstack更多,OProfile、Perf甚至DTrace可以设置和采集CPU内部的性能计数器来统计类似等待主存的CPU周期数、L1/L2缓存命中率等等。仔细看看Kevin Closson关于这些主题的论述:(Perf,Oprofile)

你可能感兴趣的:(linux)