在我們使用 Linux 系統時,如果網絡或者磁盤等 I/O 出問題,會發現進程卡住了,即使用 kill -9
也無法殺掉進程,很多常用的調試工具,比如 strace
, pstack
等也都失靈了,是怎么回事?
此時,我們使用 ps
查看進程列表,可以看到卡住的進程狀態顯示為 D。
man ps
中描述 D 狀態是 Uninterruptible Sleep。
Linux 進程有兩種睡眠狀態:
- Interruptible Sleep,可中斷睡眠,在 ps 命令中顯示 S。處在這種睡眠狀態的進程是可以通過給它發送信號來喚醒的。
- Uninterruptible Sleep,不可中斷睡眠,在 ps 命令中顯示 D。處在這種睡眠狀態的進程無法立即處理任何發送給它的信號,這也是無法用 kill 殺掉它的原因。
在 Stack Overflow 有一個解答:
kill -9
只是給進程發送了一個SIGKILL
信號,當一個進程處於特殊狀態時(信號處理,或者系統調用中)會無法處理任何信號,包括SIGKILL
也不能被正確處理,導致進程不能被立即殺掉,也就是我們常說的D
狀態(不可中斷的睡眠狀態)。那些常用的調試工具 (比如strace
、pstack
等)一般也是利用某個特殊的信號來實現的,在這種狀態下也是無法使用。
可見 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()
這種簡單的系統調用並沒有 屏蔽 SIGKILL
,SIGQUIT
,SIGABRT
等信號,還可以對它做些常規的處理。
我們再來模擬一個更復雜的 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✿)