在 linux 系统中,当进程由于网络或磁盘等 i/o 问题而卡住时,通常会进入不可中断睡眠状态(uninterruptible sleep),在 ps 命令中显示为 d 状态。这是因为这些进程正处于内核态的系统调用中,无法立即响应信号,包括 kill -9。

当使用 ps 查看进程列表时,可以看到卡住的进程状态显示为 D。

根据 man ps 的描述,D 状态表示进程处于不可中断睡眠状态。这种状态的进程无法立即处理任何发送给它的信号,因此无法通过 kill 命令终止。
Linux 进程有两种睡眠状态:
-
Interruptible Sleep(可中断睡眠),在
ps命令中显示为 S。这种状态的进程可以通过发送信号来唤醒。 -
Uninterruptible Sleep(不可中断睡眠),在
ps命令中显示为 D。这种状态的进程无法立即处理任何信号,这就是为什么kill命令无法终止它们的主要原因。
在 Stack Overflow 上有一个相关的解答,指出 D 状态的进程通常是处于某个内核态的系统调用中。要确定是哪个系统调用以及在等待什么,可以通过 Linux 下的 procfs(即 /proc 目录)查看进程的当前内核调用栈。
通过模拟 JuiceFS 客户端进程(因为 JuiceFS 基于 FUSE,是用户态的文件系统,容易模拟 I/O 故障),我们可以看到 ls 命令卡在了 vfs_fstatat 调用上,这会向 FUSE 设备发送 getattr 请求,并等待回应。如果 JuiceFS 客户端进程被暂停,ls 命令就会卡住。
$ cat /proc/`pgrep ls`/stack [] request_wait_answer+0x197/0x280 [ ] __fuse_request_send+0x67/0x90 [ ] fuse_request_send+0x27/0x30 [ ] fuse_simple_request+0xcc/0x1a0 [ ] fuse_do_getattr+0x120/0x330 [ ] fuse_update_attributes+0x68/0x70 [ ] fuse_getattr+0x3d/0x50 [ ] vfs_getattr_nosec+0x2f/0x40 [ ] vfs_getattr+0x26/0x30 [ ] vfs_fstatat+0x78/0xc0 [ ] SYSC_newstat+0x2e/0x60 [ ] SyS_newstat+0xe/0x10 [ ] entry_SYSCALL_64_fastpath+0x22/0xcb [ ] 0xffffffffffffffff
在这种情况下,按 Ctrl+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,也可以终止进程,因为 vfs_lstatat() 等简单的系统调用并没有屏蔽 SIGKILL、SIGQUIT、SIGABRT 等信号。
在更复杂的 I/O 错误模拟中,给 JuiceFS 配置一个无法写入的存储类型,并挂载后,使用 cp 尝试写入数据,cp 也会卡住。
root@localhost:~# cat /proc/`pgrep cp`/stack [] request_wait_answer+0x197/0x280 [ ] __fuse_request_send+0x67/0x90 [ ] fuse_request_send+0x27/0x30 [ ] fuse_flush+0x17f/0x200 [ ] filp_close+0x32/0x80 [ ] __close_fd+0xa3/0xd0 [ ] SyS_close+0x23/0x50 [ ] entry_SYSCALL_64_fastpath+0x22/0xcb [ ] 0xffffffffffffffff
cp 卡在 close_fd() 是因为 JuiceFS 的写入操作是异步的,当 cp 调用 write() 时,数据会先缓存在 JuiceFS 客户端进程中,并异步写入后端存储。cp 完成写入后会调用 close 确保数据写入完成,对应 FUSE 的 flush 操作。如果后端存储写入失败,flush 操作会卡住,导致 cp 也卡住。
在这种情况下,按 Ctrl+C 或使用 kill 可以中断 cp 的运行,因为 JuiceFS 实现了文件系统操作的中断处理,让它放弃当前操作(如 flush),返回 EINTR,从而在遇到网络故障时中断访问 JuiceFS 的应用。
如果停止 JuiceFS 客户端进程,使其无法处理任何 FUSE 请求(包括中断请求),则无法通过 kill -9 终止进程,进程状态会变为 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 [] request_wait_answer+0x12d/0x280 [ ] __fuse_request_send+0x67/0x90 [ ] fuse_request_send+0x27/0x30 [ ] fuse_flush+0x17f/0x200 [ ] filp_close+0x32/0x80 [ ] __close_fd+0xa3/0xd0 [ ] SyS_close+0x23/0x50 [ ] entry_SYSCALL_64_fastpath+0x22/0xcb [ ] 0xffffffffffffffff
内核调用栈显示进程卡在 FUSE 的 flush 调用上。只要恢复 JuiceFS 客户端进程,就可以立即中断 cp 并使其退出。
像 close 这种涉及数据安全性的操作是不可重启的(non-restartable),因此不能被 SIGKILL 等信号随意中断,必须由 FUSE 实现端响应中断操作才能中断。
因此,只要 JuiceFS 客户端进程能够健康响应中断,就不必担心访问 JuiceFS 的应用会卡死。或者,可以通过终止 JuiceFS 客户端进程来结束当前挂载点,中断所有正在访问该挂载点的应用。










