通過/proc查看Linux內核態調用棧來定位問題

前幾天碰到一個問題:一個進程運行過程中掛死了,把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(不可中斷睡眠)。然後看看voluntaryctxtswitches 和nonvoluntaryctxtswitches的數值 —— 它可以告訴你進程佔用(或者釋放)了多少次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)

轉載:http://blog.csdn.net/hintonic/article/details/18005779

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章