Linux跨文件系统的文件夹和文件映射关系

VFS(Virtual File System)

Everything is a file 是Unix的设计理念,由其派生的Linux也如此。

Unix系统I/O模型最为显著的特征之一就是I/O通用性,也就是同一套系统调用open(), read(),write(),close()所执行的I/O操作,可以在所有文件类型上被执行。

文件系统有很多种实现。 Linux系统中通过VFS(Virtual File System),定义一系列通用接口。所有与文件交互程序都会按照接口进行操作。 每种文件系统都会提供VFS接口实现

依赖倒置原则(DIP):高层的模块不依赖底层, 而依赖高层的抽象。

文件模型

主要由以下对象模型组成:

  • Superblock object:存放文件系统信息。
  • inode object:存放具体文件一般信息, 文件控制信息,唯一标识一个文件。
  • file object:存放文件与进程间交互信息。
  • dentry object:存放dentry与文件相关信息,便于文件检索。

图片来在《Understanding The Linux Kernel》

可以看到在inode object和superblock object和实际的文件系统有直接关系的, 而file object和dentry object与文件系统是没有映射的,其中,dentry object起到了中间纽带的作用。

 

文件使用

 

每一个进程将打开的文件的描述符(file descriptor)保存在一个数组中, 当调用open()时,系统调用返回文件描述符,供后续read(),write()等使用, 这些系统调用获取file的数据结构后,通过VFS函数的调用,实现相关功能。

下图展示了相关数据结构之间的关系。

图片来自 https://myaut.github.io/dtrace-stap-book/kernel/fs.html

从上图可以看到, 进程通过task_struct中的files可以获得file object,包括文件打开方式f_mode,如O_RDONLY、O_RDWR, 文件偏移位置f_pos,如seek函数使用到, 文件相关的系统调用都可以通过file object对文件进行操作。

每一个文件可以通过inode object和dentry object进行标识, inode object包含一些文件本身的一些信息, 如权限信息,比如i_uid,i_gid, dentry object包含文件的目录结构信息,方便通过文件名进行查找。

虽然dentry和inode标识文件系统的文件, 但是系统在不同位置可以安装多个文件系统, 这个位置就是mountpoint, linux中通过vfsmount 结构的mnt_sb指向标识文件系统super_block, 通过mnt_root指向安装点, 就可以方便跨文件系统的文件管理。

文件映射

下面用调试工具看下跨文件系统映射关系

环境准备:

Windows10 上安装Vagrant & Vbox6.1 &CentOS8

虚拟机启动前在增加一块虚拟硬盘。 启动虚拟机后, 执行mount可以看到, 原有文件系统

/dev/sda1 on / type xfs (rw,relatime,...)

使用fdisk对新增加的硬盘,/dev/sdb 新增一个分区,使用mkfs.ext4格式化为ext4格式

创建目录,/hello , /hello/world 将新硬盘的挂在到 world目录上, 生成helloworld.txt

# mkdir -p /hello/world
# mount /dev/sdb1 /hello/world
# echo "helloworld" > /hello/world/helloworld.txt
调试步骤

下面通过调试工具查看相关数据结构信息。

  •  systemtap
  •  crash

systemtap 安装手册https://sourceware.org/systemtap/SystemTap_Beginners_Guide.pdf

crash 开始无法执行 执行crash /lib/modules/4.18.0-305.19.1.el8_4.x86_64+debug/后成功后续再研究。

systemtap脚本如下:script.stp

probe syscall.read {
        file = @cast(task_current(), "task_struct", "kernel")->files->fdt->fd[fd]&~3;
        if(!file)
            next;
        dentry = @cast(file, "file", "kernel")->f_path->dentry;
        inode = @cast(dentry, "dentry", "kernel")->d_inode;
        filename = d_name(dentry)

        // get the directory's dentry of file
        dentry_p = @cast(file, "file", "kernel")->f_path->dentry->d_parent;
        ino_p = @cast(file, "file", "kernel")->f_path->dentry->d_parent->d_inode->i_ino;
        file_p = d_name(dentry_p)
        // get the vfsmount's mnt_dentry of file
        dentry_m = @cast(file, "file", "kernel")->f_path->mnt->mnt_root;
        ino_m = @cast(file, "file", "kernel")->f_path->mnt->mnt_root->d_inode->i_ino;
        file_m = d_name(dentry_m)

        printf("READ %d : file '%s'  dentry %x of size '%d' on device %s ino_p %d  dentry_p %x  file_p %s ino_m %d dentry_m %x file_m %s\n",
            fd, filename, dentry ,
            @cast(inode, "inode", "kernel")->i_size,
            kernel_string(@cast(inode, "inode", "kernel")->i_sb->s_id),
            ino_p, dentry_p, file_p,  ino_m, dentry_m, file_m
             );
}

执行如下命令:

# stap script.stp -c "cat /hello/world/helloworld.txt"

输出结果:

# stap script.stp -c "cat /hello/world/helloworld.txt"|grep helloworld
helloworld
READ 3 : file 'helloworld.txt'  dentry ffff8fb682adccc0 of size '11' on device sdb1 ino_p 2  dentry_p ffff8fb6b4d9e840  file_p / ino_m 2 dentry_m ffff8fb6b4d9e840 file_m /
READ 3 : file 'helloworld.txt'  dentry ffff8fb682adccc0 of size '11' on device sdb1 ino_p 2  dentry_p ffff8fb6b4d9e840  file_p / ino_m 2 dentry_m ffff8fb6b4d9e840 file_m /

对于 helloworld.txt的上一级的dentry,可以看到不是想象中的“world”而是“/”。 可以通过crash再次验证一下

crash> dentry ffff8fb682adccc0|grep parent
  d_parent = 0xffff8fb6b4d9e840,
crash> dentry 0xffff8fb6b4d9e840|grep name
  d_name = {
    name = 0xffff8fb6b4d9e878 "/"
  d_iname = "/\000tify_on_release\000d_iops_device",

可以看到和日志中打印应的dentry parent的dentry 0xffff8fb6b4d9e840一样, 是一个名字为”/” dentry. 也就是helloworld.txt 文件上级的dentry是文件系统 /dev/sdb1的根目录。他的挂载点是 /hello/world, 通过crash看下mount信息,以及挂载信息:

crash> mount|grep sdb1
ffff8fb6f8c54300 ffff8fb6fbc5b800 ext4   /dev/sdb1 /hello/world
crash> struct mount ffff8fb6f8c54300|grep mnt_mountpoint
  mnt_mountpoint = 0xffff8fb6fd1c7900,
crash> dentry 0xffff8fb6fd1c7900|grep name
  d_name = {
    name = 0xffff8fb6fd1c7938 "world"
  d_iname = "world\000le\000ocs\000erarchy\000_recursive",

可以看到, 跨文件系统,dentry维护的目录结构不是我们看到的目录结构, 如果查找是dentry不在内存中的话, 就需要通过磁盘进行查找, 这时候就需要通过用到mount中相关信息了。 磁盘查找的过程,更类似和我们平时查找文件的逻辑。

在一个文件系统中超级块的信息是一样的。

crash> struct mount 0xffff8fb6f8c54300|grep sb
    mnt_sb = 0xffff8fb6fbc5b800,
crash> dentry ffff8fb682adccc0|grep sb
  d_sb = 0xffff8fb6fbc5b800,
crash> dentry 0xffff8fb6b4d9e840|grep sb
  d_sb = 0xffff8fb6fbc5b800,

 

按照以上做法可以将上面文件结构整理为下图

 

 

另外 dentry negativity状态问题,通常由于关联文件被删除, 或者dentry解析一个不存在的路径,会出现无效的dentry, LWN 上这篇文章 Dentry negativity 描述了针对这种情况还除了一个补丁, 不过目前尚未被采纳。

 

参考及引用

图片来自 twitter Ines B @moraimauy

Virtual File System

 

 

Comments are closed.