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
Comments are closed.