umount nfs报错device is busy的问题

点击这里查看配套的教学视频

点击跳转到nfs课程所有目录

1 问题描述

卸载nfsv3挂载点时报错device is busy,但用lsof +D <挂载点>fuser -m <挂载点>都无法找到使用挂载点的进程。

2 调试

用以下命令打开nfs日志开关(参考《nfs调试方法》):

echo 0xFFFF > /proc/sys/sunrpc/nfs_debug
# echo 0 > /proc/sys/sunrpc/nfs_debug # 在生产环境中关闭日志请执行这个命令

kprobe抓进程信息(参考《内核调试方法》):

kprobe_func_name=nfs_file_open # 或者 nfs_file_read
cd /sys/kernel/debug/tracing/
cat available_filter_functions | grep ${kprobe_func_name}
echo 1 > tracing_on
echo "p:p_${kprobe_func_name} ${kprobe_func_name}" >> kprobe_events
echo 1 > events/kprobes/p_${kprobe_func_name}/enable
echo stacktrace > events/kprobes/p_${kprobe_func_name}/trigger # 打印栈
# echo '!stacktrace' > events/kprobes/p_${kprobe_func_name}/trigger # 关闭栈
# echo 0 > events/kprobes/p_${kprobe_func_name}/enable
# echo "-:p_${kprobe_func_name}" >> kprobe_events
echo 0 > trace # 清除trace信息
cat trace_pipe

3 用户态快速打开关闭文件

挂载:

mount -t nfs -o vers=3 localhost:/tmp /mnt

在用户态通过创建两个线程,不断打开又关闭文件,编译运行thread-open-file-short-time.c:

gcc -o thread-open-file-short-time thread-open-file-short-time.c -lpthread
./thread-open-file-short-time

这时无法卸载nfs,且用lsof +D <挂载点>fuser -m <挂载点>都无法找到使用挂载点的进程:

umount /mnt # umount.nfs: /mnt: device is busy
lsof +D /mnt # 找不到进程
fuser -m /mnt # 找不到进程

用上面的kprobe trace抓到以下信息:

thread-open-fil-956   [010] ....  7723.365916: p_nfs_file_open: (nfs_file_open+0x0/0x60 [nfs])

956是线程id,用以下命令查看完整的进程名:

ps -eLf | grep 956

4 内核打开文件

4.1 构造

在内核空间打开文件,用lsof +D <挂载点>fuser -m <挂载点>无法找到进程。

源码文件如下:

挂载:

mount -t nfs -o vers=3 localhost:/tmp /mnt

加载ko,打开并读文件/mnt/dir/file,注意这个操作不要在生产环境中尝试:

mkdir /mnt/dir -p
echo something > /mnt/dir/file # 创建文件
echo 3 > /proc/sys/vm/drop_caches
insmod kernel-open-file.ko

日志请查看nfs-umount-device-is-busy-log.txt:

...
[  122.567308] NFS: open file(dir/file)
...
[  122.571219] NFS: read(dir/file, 4096@0)
...

这时我们卸载nfs挂载点就能得到一样的报错信息,且无法找到使用挂载点的进程:

umount /mnt # umount.nfs: /mnt: device is busy
lsof +D /mnt # 找不到进程
fuser -m /mnt # 找不到进程

移除ko后,在内核关闭了文件,就能正常卸载nfs挂载点了:

rmmod kernel_open_file # 在内核中关闭文件
umount /mnt # 正常卸载,不报错

4.2 代码分析

系统调用的跟踪调试请查看《文件系统延迟卸载》

只有在用户空间打开文件时会把文件描述符放到files_struct -> fdt中:

openat
  do_sys_open
    do_sys_openat2
      fd_install
        rcu_assign_pointer(fdt->fd[fd], file);

在内核空间打开文件时,不会把文件描述符加到fdtable中,fuser -mlsof +D无法遍历到文件描述符,所以无法找到打开文件的进程。

可以用kprobe-fd_install.c调试, 其中mydebug_dump_stack()相关的用法可以查看《mydebug模块》