umount nfs报错device is busy的问题

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

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

1 问题描述

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

2 构造内核打开文件

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

源码文件如下:

3 调试

挂载:

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

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

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

kprobe抓进程信息:

kprobe_func_name=nfs_file_open
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

加载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 /mnt # 找不到进程
fuser -m /mnt # 找不到进程

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

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

4 代码分析

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

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

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

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

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

5 mmap()可以找到进程

下面的内容对你没啥卵用,不用看了。只是我吃饱撑的尝试一下,顺便再记录一下。

mmap.c:

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    // 打开文件,修改挂载点
    int fd = open("/mnt/file", O_RDONLY);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 获取文件信息
    struct stat file_stat;
    if (fstat(fd, &file_stat) == -1) {
        perror("fstat");
        close(fd);
        exit(EXIT_FAILURE);
    }

    // 检查文件是否为空
    if (file_stat.st_size == 0) {
        fprintf(stderr, "File is empty\n");
        close(fd);
        exit(EXIT_FAILURE);
    }

    // 内存映射文件
    void *mapped = mmap(NULL, file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (mapped == MAP_FAILED) {
        perror("mmap");
        close(fd);
        exit(EXIT_FAILURE);
    }

    // 映射成功后即可关闭文件描述符
    close(fd);

    // 将文件内容输出到标准输出
    if (fwrite(mapped, 1, file_stat.st_size, stdout) != file_stat.st_size) {
        fprintf(stderr, "Error writing to stdout\n");
    }

    printf("will loop\n");
    while (1) {
        ;
    }

    // 解除内存映射
    if (munmap(mapped, file_stat.st_size) == -1) {
        perror("munmap");
        exit(EXIT_FAILURE);
    }

    return EXIT_SUCCESS;
}
gcc -o mmap mmap.c
./mmap &
lsof <挂载点> # 能找到进程
fuser -m <挂载点> # 能找到进程

说了不用看了,你还看。