Linux 進程卡住了怎么辦?


在我們使用 Linux 系統時,如果網絡或者磁盤等 I/O 出問題,會發現進程卡住了,即使用 kill -9 也無法殺掉進程,很多常用的調試工具,比如 strace, pstack 等也都失靈了,是怎么回事?

此時,我們使用 ps 查看進程列表,可以看到卡住的進程狀態顯示為 D。

man ps 中描述 D 狀態是 Uninterruptible Sleep。

Linux 進程有兩種睡眠狀態:

  1. Interruptible Sleep,可中斷睡眠,在 ps 命令中顯示 S。處在這種睡眠狀態的進程是可以通過給它發送信號來喚醒的。
  2. Uninterruptible Sleep,不可中斷睡眠,在 ps 命令中顯示 D。處在這種睡眠狀態的進程無法立即處理任何發送給它的信號,這也是無法用 kill 殺掉它的原因。

在 Stack Overflow 有一個解答:

kill -9 只是給進程發送了一個 SIGKILL 信號,當一個進程處於特殊狀態時(信號處理,或者系統調用中)會無法處理任何信號,包括 SIGKILL 也不能被正確處理,導致進程不能被立即殺掉,也就是我們常說的 D 狀態(不可中斷的睡眠狀態)。那些常用的調試工具 (比如 stracepstack 等)一般也是利用某個特殊的信號來實現的,在這種狀態下也是無法使用。

可見 D 狀態的進程一般是處在某個內核態的系統調用中,那怎么知道是哪個系統調用,又是在等待什么呢?幸好 Linux 下提供了 procfs(就是 Linux 下的 /proc 目錄), 通過它就可以看到任何一個進程的當前內核調用棧。下面我們用訪問 JuiceFS 的進程來模擬一下(因為 JuiceFS 客戶端基於 FUSE,是用戶態的文件系統,比較容易模擬 I/O 故障)。

先將 JuiceFS 掛載到前台(在 ./juicefs mount 命令中加一個 -f 參數),然后用 Cltr+Z 把這個進程停掉,這時候用 ls /jfs 去訪問掛載點,會發現 ls 卡住了。

通過下面的命令可以看到 ls 卡在了 vfs_fstatat 調用上,它會給 FUSE 設備發送 getattr 請求,在等待回應。而 JuiceFS 客戶端進程已經被我們停掉了,所以它就卡住了:

$ cat /proc/`pgrep ls`/stack
[<ffffffff813277c7>] request_wait_answer+0x197/0x280
[<ffffffff81327d07>] __fuse_request_send+0x67/0x90
[<ffffffff81327d57>] fuse_request_send+0x27/0x30
[<ffffffff8132b0ac>] fuse_simple_request+0xcc/0x1a0
[<ffffffff8132c0f0>] fuse_do_getattr+0x120/0x330
[<ffffffff8132df28>] fuse_update_attributes+0x68/0x70
[<ffffffff8132e33d>] fuse_getattr+0x3d/0x50
[<ffffffff81220c6f>] vfs_getattr_nosec+0x2f/0x40
[<ffffffff81220ee6>] vfs_getattr+0x26/0x30
[<ffffffff81220fc8>] vfs_fstatat+0x78/0xc0
[<ffffffff8122150e>] SYSC_newstat+0x2e/0x60
[<ffffffff8122169e>] SyS_newstat+0xe/0x10
[<ffffffff8186281b>] entry_SYSCALL_64_fastpath+0x22/0xcb
[<ffffffffffffffff>] 0xffffffffffffffff

這時候按 Ctrl+C 也不能退出。

root@localhost:~# ls /jfs
^C
^C^C^C^C^C

但是用 strace 卻能喚醒它,並且開始處理之前的中斷信號,然后就退出了。

root@localhost:~# strace -p `pgrep ls`
strace: Process 26469 attached
--- SIGINT {si_signo=SIGINT, si_code=SI_KERNEL} ---
rt_sigreturn({mask=[]})                 = -1 EINTR (Interrupted system call)
--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=13290, si_uid=0} ---
rt_sigreturn({mask=[]})                 = -1 EINTR (Interrupted system call)
。。。
tgkill(26469, 26469, SIGINT)            = 0
--- SIGINT {si_signo=SIGINT, si_code=SI_TKILL, si_pid=26469, si_uid=0} ---
+++ killed by SIGINT +++

這個時候如果用 kill -9 的話,也是可以把它殺掉的:

root@localhost:~# ls /jfs
^C
^C^C^C^C^C
^C^CKilled

因為 vfs_lstatat() 這種簡單的系統調用並沒有 屏蔽 SIGKILLSIGQUITSIGABRT 等信號,還可以對它做些常規的處理。

我們再來模擬一個更復雜的 I/O 錯誤,給 JuiceFS 配置一個無法寫入的存儲類型,並掛載上,用 cp 嘗試往里寫入數據,這時候 cp 也會卡住:

root@localhost:~# cat /proc/`pgrep cp`/stack
[<ffffffff813277c7>] request_wait_answer+0x197/0x280
[<ffffffff81327d07>] __fuse_request_send+0x67/0x90
[<ffffffff81327d57>] fuse_request_send+0x27/0x30
[<ffffffff81331b3f>] fuse_flush+0x17f/0x200
[<ffffffff81218fd2>] filp_close+0x32/0x80
[<ffffffff8123ac53>] __close_fd+0xa3/0xd0
[<ffffffff81219043>] SyS_close+0x23/0x50
[<ffffffff8186281b>] entry_SYSCALL_64_fastpath+0x22/0xcb
[<ffffffffffffffff>] 0xffffffffffffffff

怎么卡在 close_fd() ?這是因為往 JFS 寫數據是異步的,當 cp 調用 write() 時,數據會先緩存在 JuiceFS 的客戶端進程里同時會異步寫入到后端存儲,等 cp 寫完數據,它會調用 close 來確保數據寫入完成,對應 FUSE 的 flush 操作。JuiceFS 的客戶端在遇到 flush 操作時,需要確保全部寫入的數據都持久化到后端存儲,而后端存儲寫入失敗了,它就在多次重試的過程中,所以 flush 操作卡住了,還沒有回復給 cp,所以 cp 也卡住了。

這個時候如果用 Cltr+C 或者 kill 是可以中斷 cp 的運行,因JuiceFS 實現了各種文件系統操作的中斷處理,讓它放棄當前操作(比如 flush), 返回 EINTR這樣在遇到各種網絡故障時可以中斷正在訪問 JuiceFS 的應用

這時如果我停止 JuiceFS 客戶端進程,讓它不能再處理任何 FUSE 請求(包括中斷請求),這個時候如果嘗試去殺它,就殺不掉了,包括 kill -9 也殺不掉,用 ps 查看進程狀態,已經是 D 狀態了。

root      1592  0.1  0.0  20612  1116 pts/3    D+   12:45   0:00 cp parity /jfs/aaa

但這個時候是可以用 cat /proc/1592/stack 來看它的內核調用棧

root@localhost:~# cat /proc/1592/stack
[<ffffffff8132775d>] request_wait_answer+0x12d/0x280
[<ffffffff81327d07>] __fuse_request_send+0x67/0x90
[<ffffffff81327d57>] fuse_request_send+0x27/0x30
[<ffffffff81331b3f>] fuse_flush+0x17f/0x200
[<ffffffff81218fd2>] filp_close+0x32/0x80
[<ffffffff8123ac53>] __close_fd+0xa3/0xd0
[<ffffffff81219043>] SyS_close+0x23/0x50
[<ffffffff8186281b>] entry_SYSCALL_64_fastpath+0x22/0xcb
[<ffffffffffffffff>] 0xffffffffffffffff

內核調用棧顯示它卡在 FUSE 的 flush 調用上,這個時候只要恢復 JuiceFS 客戶端進程,就可以立即中斷 cp 讓它退出。

close 這種涉及到數據安全性的操作,不是 restartable, 也就不能被 SIGKILL 等隨意中斷,比如要 FUSE 的實現端響應中斷操作才能中斷。

因此,只要 JuiceFS 的客戶端進程能夠健康的響應中斷,就不用擔心訪問 JuiceFS 的應用卡死。或者殺掉 JuiceFS 客戶端進程也可以結束當前的掛載點,中斷所有在訪問當前掛載點的應用

如有幫助的話歡迎關注我們項目 Juicedata/JuiceFS 喲! (0ᴗ0✿)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM