初探Linux內(nèi)核態(tài)——通過proc文件系統(tǒng)作快速問題定位

文章翻譯自 Peeking into Linux kernel-land using /proc filesystem for quick’n’dirty troubleshooting

這篇博客的內(nèi)容完全是關(guān)于現(xiàn)代Linux內(nèi)核的。換句話說,指的是與RHEL6一樣使用的2.6.3x系列內(nèi)核,而不是古老的RHEL5所使用的2.6.18內(nèi)核(都什么鬼了??。?,雖然大部分企業(yè)都還在使用RHEL5。另外,這篇文章也不會(huì)涉及內(nèi)核調(diào)試器或者SystemTap腳本之類的東西,完全是最最簡單地在有用的proc文件系統(tǒng)節(jié)點(diǎn)上執(zhí)行“cat /proc/PID/xyz”這樣的命令。

定位一個(gè)程序“運(yùn)行緩慢”的問題

下面要舉的這個(gè)例子是這樣的:一個(gè)DBA反映說他們的find命令一直運(yùn)行緩慢,半天都沒有什么輸出,他們想知道這是為什么。聽到這個(gè)問題的時(shí)候我就大概有直覺造成這個(gè)問題的原因,但是他們還是想知道怎么系統(tǒng)地追蹤這類問題,并找到解決方案。剛好出問題的現(xiàn)場還在……

還好,系統(tǒng)是運(yùn)行在OEL6上的,內(nèi)核比較新,確切地說是2.6.39 UEK2。

首先,讓我們看看find進(jìn)程是否還在:

[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(請記好這個(gè)將會(huì)伴隨整篇博客的數(shù)字)。

那么,我們就從最基礎(chǔ)的開始分析它的瓶頸:如果它不是被什么操作卡住了(例如從cache中加載它所需要的內(nèi)容),它應(yīng)該是100%的CPU占用率;如果它的瓶頸在IO或者資源競爭,那么它應(yīng)該是很低的CPU占用率,或者是%0。

我們先看下top:

[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

結(jié)果很清楚:這個(gè)進(jìn)程的CPU占用率很低,幾乎為零。但是CPU占用低也分情況:一種是進(jìn)程完全卡住了,根本沒有機(jī)會(huì)獲得時(shí)間片;另一種是進(jìn)程在不停進(jìn)入等待的狀態(tài)(例如poll動(dòng)作就是時(shí)不時(shí)超時(shí)后,進(jìn)程進(jìn)入休眠狀態(tài))。雖然這剩下的細(xì)節(jié)top還不足以完全給我們展示,但是至少我們知道了這個(gè)進(jìn)程沒有在燒CPU時(shí)間。

通常情況下,如果進(jìn)程處于這種狀態(tài)(%0的CPU占用一般說明進(jìn)程是卡在了某個(gè)系統(tǒng)調(diào)用,因?yàn)檫@個(gè)系統(tǒng)調(diào)用阻塞了,內(nèi)核需要把進(jìn)程放到休眠狀態(tài)),我都會(huì)用strace跟蹤一下這個(gè)進(jìn)程具體卡在了哪個(gè)系統(tǒng)調(diào)用。而且,如果進(jìn)程不是完全卡住了,那進(jìn)程中的系統(tǒng)調(diào)用情況也會(huì)在strace的輸出中有所展示(因?yàn)橐话阕枞南到y(tǒng)調(diào)用會(huì)在超時(shí)返回后,過一段時(shí)間再進(jìn)入阻塞等待的狀態(tài))。

讓我們試試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自己也卡住了!半天沒有輸出,也不響應(yīng)Ctrl-C,我不得不通過Ctrl-Z把它扔到后臺再殺掉它。想簡單處理還真是不容易啊。

那只好再試試pstack了(Linux上的pstack只是用shell腳本包了一下GDB)。盡管pstack看不到內(nèi)核態(tài)的內(nèi)容,但是至少它能告訴我們是哪個(gè)系統(tǒng)調(diào)用最后執(zhí)行的(通常pstack輸出的用戶態(tài)調(diào)用棧最頂部是一個(gè)libc庫的系統(tǒng)調(diào)用):

[root@oel6 ~]# pstack 27288

^C
^Z
[1]+  Stopped                 pstack 27288

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

呵呵,pstack也卡住了,什么輸出都沒有!

至此,我們還是不知道我們的程序是怎么卡住了,卡在哪里了。

好吧,還怎么進(jìn)行下去呢?還有一些常用的信息可以搜集——進(jìn)程的status字段和WCHAN字段,這些使用古老的ps就能查看(或許最開始就應(yīng)該用ps看看這個(gè)進(jìn)程是不是已經(jīng)成僵尸進(jìn)程了):

[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

需要注意的是,你要多運(yùn)行幾次ps以確保進(jìn)程還在同一個(gè)狀態(tài)(不然在不湊巧的時(shí)候獲取了一個(gè)錯(cuò)誤的狀態(tài)就麻煩了),我這里為了簡短就只貼一次輸出了。

進(jìn)程此時(shí)正處于D狀態(tài),按照man手冊的說法,這通常是因?yàn)榇疟PIO導(dǎo)致的。而WCHAN字段(表示導(dǎo)致程序處于休眠/等待狀態(tài)的函數(shù)調(diào)用)則有點(diǎn)兒被切掉了。顯然我可以翻一下ps的man手冊,看看怎么把這個(gè)字段調(diào)寬一點(diǎn)好完整打印出來,不過既然我都知道了這個(gè)信息來自于proc文件系統(tǒng),就沒這個(gè)必要了。

直接從它的源頭讀一下看看(再次說明一下,多試幾次看看,畢竟我們還不知道這個(gè)進(jìn)程到底是不是完全卡死了呢):

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

額……這個(gè)進(jìn)程居然在等待某些RPC調(diào)用。RPC調(diào)用通常意味著這個(gè)程序在跟其他進(jìn)程通信(不管是本地還是遠(yuǎn)程)??倸w,我們還是不知道為什么會(huì)卡住。

到底是不是完全卡住了?

在我們揭開這篇文章最后的謎底之前,我們還是先搞清楚這個(gè)進(jìn)程到底是不是完全卡住了。

其實(shí),在新一點(diǎn)的Linux內(nèi)核中,/proc/PID/status 這個(gè)文件可以告訴我們這點(diǎn):

[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

(主要看State字段和最后兩行:voluntary_ctxt_switches和 nonvoluntary_ctxt_switches)

進(jìn)程處于D——Disk Sleep(不可打斷休眠狀態(tài)),voluntary_ctxt_switches 和 nonvoluntary_ctxt_switches的數(shù)量則能告訴我們這個(gè)進(jìn)程被分給時(shí)間片的次數(shù)。過幾秒再看看這些數(shù)字有沒有增加,如果沒有增加,則說明這個(gè)進(jìn)程是完全卡死的,目前我在追蹤的例子就是這個(gè)情況,反之,則說明進(jìn)程不是完全卡住的狀態(tài)。

順便提一下,還有兩種辦法也可以獲取進(jìn)程的上下文切換數(shù)量(第二種在老舊的內(nèi)核上也能工作):

[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_switches字段(等于 nr_voluntary_switches + nr_involuntary_switches),值是27054,跟/proc/PID/schedstat 中的第三個(gè)字段是一致的:

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

等一段時(shí)間,也是一樣的結(jié)果,數(shù)量沒有增長……

通過/proc文件系統(tǒng)初探Linux內(nèi)核態(tài)世界

看情況我們的程序是卡死無疑了,strace和pstack這些使用ptrace系統(tǒng)調(diào)用來attach到進(jìn)程上來進(jìn)行跟蹤的調(diào)試器也沒啥用,因?yàn)檫M(jìn)程已經(jīng)完全卡住了,那么ptrace這種系統(tǒng)調(diào)用估計(jì)也會(huì)把自己卡住。(順便說一下,我曾經(jīng)用strace來跟蹤attach到其他進(jìn)程上的strace,結(jié)果strace把那個(gè)進(jìn)程搞掛了。別說我沒警告過你)!

沒了strace和pstack,我們還能怎么查程序卡在了哪個(gè)系統(tǒng)調(diào)用呢?當(dāng)然是 /proc/PID/syscall 了!

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

這鬼輸出怎么看呢?很簡單,0x加很大的數(shù)字一般就是內(nèi)存地址(使用pmap類似的工具可以看具體他們指向了什么內(nèi)容),如果是很小的數(shù)字的話,很可能就是一些索引號(例如打開的文件描述符,就像/proc/PID/fd的內(nèi)容一樣)。在這個(gè)例子中,因?yàn)槭莝yscall文件,你可以”合理猜測“這是一個(gè)系統(tǒng)調(diào)用號:進(jìn)程卡在了262號系統(tǒng)調(diào)用!

需要注意的是系統(tǒng)調(diào)用號在不同的系統(tǒng)上可能是不一致的,所以你必須從一個(gè)合適的.h文件中去查看它具體指向了哪個(gè)調(diào)用。通常情況下,在/usr/include中搜索 syscall* 是個(gè)很好的切入點(diǎn)。在我的系統(tǒng)上,這個(gè)系統(tǒng)調(diào)用是在 /usr/include/asm/unistd_64.h中定義的:

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

根據(jù)上面顯示的結(jié)果可以看到這個(gè)系統(tǒng)調(diào)用叫 newfstatat,我們只需要man newfstatat就可以知道這是干啥的了。針對系統(tǒng)調(diào)用有一個(gè)實(shí)用小技巧分享:如果man手冊中沒有發(fā)現(xiàn)一個(gè)系統(tǒng)調(diào)用,那么請嘗試刪除一些特定的前綴或者后綴(例如man pread64不行就嘗試man pread)。還或者,干脆google之……

總之,這個(gè)叫“new-fstat-at”的系統(tǒng)調(diào)用的作用就是讓你可以像 stat 系統(tǒng)調(diào)用一樣讀取文件的屬性,我們的程序就是卡在這個(gè)操作上,終于在調(diào)試這個(gè)程序的道路上邁出了一大步,不容易!但是為啥會(huì)卡在這個(gè)調(diào)用呢?

好吧,終于要亮真本事了。隆重介紹:/proc/PID/stack,能讓你看到一個(gè)進(jìn)程內(nèi)核態(tài)的調(diào)用棧信息的神器,而且只是通過cat一個(gè)proc文件?。?!

[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

可以看到最頂部的函數(shù)就是我們現(xiàn)在卡在的地方,正是上面WCHAN字段顯示的內(nèi)容:rpc_wait_bit_killable。(注意:實(shí)際上不一定最頂部的函數(shù)就是我們想要的,因?yàn)閮?nèi)核可能也執(zhí)行了 schedule 之類的函數(shù)來讓程序進(jìn)入休眠或者運(yùn)行。這里沒有顯示可能是因?yàn)榭ㄖ魇且驗(yàn)槠渌{(diào)用卡主了才進(jìn)入睡眠狀態(tài),而不是相反的邏輯)。

多虧了這個(gè)神器,我們現(xiàn)在可以從頭到尾推導(dǎo)出程序卡主的整個(gè)過程和造成最終 rpc_wait_bit_killable 函數(shù)卡主的原因了:

? 最底部的 system_call_fastpath 是一個(gè)非常常見的系統(tǒng)調(diào)用處理函數(shù),正是它調(diào)用了我們剛才一直有疑問的 newfstatat 系統(tǒng)調(diào)用。然后,再往上可以看到一堆nfs相關(guān)的子函數(shù)調(diào)用,這樣我們基本可以斷定正nfs相關(guān)的下層代碼導(dǎo)致了程序卡住。我沒有推斷說問題是出在nfs的代碼里是因?yàn)槔^續(xù)往上可以看到rpc相關(guān)的函數(shù),可以推斷是nfs為了跟其他進(jìn)程進(jìn)行通信又調(diào)用了rpc相關(guān)的函數(shù)——在這個(gè)例子中,可能是[kworker/N:N], [nfsiod], [lockd] or [rpciod]等內(nèi)核的IO線程,但是這個(gè)進(jìn)程因?yàn)槟承┰颍ú聹y是網(wǎng)絡(luò)鏈接斷開之類的問題)再也沒有收到響應(yīng)的回復(fù)。

同樣的,我們可以利用以上的方法來查看哪些輔助IO的內(nèi)核線程為什么會(huì)卡在網(wǎng)絡(luò)相關(guān)的操作上,盡管kworkers就不簡簡單單是NFS的RPC通信了。在另外一次問題重現(xiàn)的嘗試(通過NFS拷貝大文件)中,我剛好捕捉到一次kwrokers等待網(wǎng)絡(luò)的情況:

[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

通過開啟內(nèi)核的tracing肯定可以確切找到是內(nèi)核的哪兩個(gè)線程之間再通信,不過限于文章篇幅,這里就不展開了,畢竟這只是一個(gè)實(shí)用(且簡單)的問題追蹤定位練習(xí)。

診斷和“修復(fù)”

多虧了現(xiàn)代內(nèi)核提供的棧信息存樣,我們得以系統(tǒng)地追蹤到我們的find命令卡在了哪里——內(nèi)核中的NFS代碼。而且一般情況下,NFS相關(guān)卡死,最需要懷疑的就是網(wǎng)絡(luò)問題。你猜我是怎么重現(xiàn)上面的這個(gè)問題的?其實(shí)就是在功過NFS掛在虛擬中的一塊磁盤,執(zhí)行find命令,然后讓虛擬機(jī)休眠……這樣就可以重現(xiàn)類似網(wǎng)絡(luò)(配置或者防火墻)導(dǎo)致的鏈接默默斷掉但是并沒有通知TCP另一端的進(jìn)程的情況。

因?yàn)?rpc_wait_bit_killable 是可以直接被安全干掉的函數(shù),這里我們選擇通過 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 ~]#

進(jìn)程被殺掉了,好了,問題解決 :)

注:文章沒有翻譯完,下面還有一段不是那么有意思的小工具和廣告,沒動(dòng)力翻下去了 :P

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 29.1 引言 本章中我們要討論另一個(gè)常用的應(yīng)用程序:NFS(網(wǎng)絡(luò)文件系統(tǒng)),它為客戶程序提供透明的文件訪問。NF...
    張芳濤閱讀 1,104評論 0 4
  • linux資料總章2.1 1.0寫的不好抱歉 但是2.0已經(jīng)改了很多 但是錯(cuò)誤還是無法避免 以后資料會(huì)慢慢更新 大...
    數(shù)據(jù)革命閱讀 13,246評論 2 33
  • 首先來一張圖說明GNU/Linux的基本的體系結(jié)構(gòu): 用戶模式和內(nèi)核模式 執(zhí)行進(jìn)程的模式有兩種:用戶模式和內(nèi)核模式...
    Joe_HUST閱讀 2,278評論 0 2
  • ** 本文的實(shí)驗(yàn)環(huán)境為 CentOS6.9 ** NFS 是Network File System的縮寫,即網(wǎng)絡(luò)文...
    JSON_NULL閱讀 13,305評論 1 4
  • 精致的容妝勾勒出相同的面龐 戴上面具的我們都一樣 畫著別人的臉 演繹著自己的憂傷 我們的面具是那么美 讓人不愿意摘...
    獨(dú)孤小丹閱讀 240評論 12 7

友情鏈接更多精彩內(nèi)容