# Linux跨文件系统的文件夹和文件映射关系
Date: 2021-03-06


#### **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与文件相关信息，便于文件检索。

![](https://garlicspace.com/wp-content/uploads/2021/03/微信图片_20210306211146.png)

> 图片来在《Understanding The Linux Kernel》

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

 

#### **文件使用**

 

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

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

![](https://garlicspace.com/wp-content/uploads/2021/03/vfs.png)

> 图片来自 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,

```

 

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

![](https://garlicspace.com/wp-content/uploads/2021/03/dentry-1024x648.jpg)

 

 

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

 

#### **参考及引用**

图片来自 twitter Ines B @moraimauy

[Virtual File System](https://myaut.github.io/dtrace-stap-book/kernel/fs.html)

