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脚本如下:
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 script4.stp -c "cat /hello/world/helloworld.txt"
输出结果:
root@localhost trace]# stap script4.stp -c "cat /hello/world/helloworld.txt" helloworld READ 4 : file '.cmd' dentry ffff9938d97ca3c0 of size '0' on device debugfs ino_p 43648 dentry_p ffff9938d97cab40 file_p stap_3c0744183555cd482fd82f7186f56a23_11542 ino_m 1 dentry_m ffff9938cc806cc0 file_m / 。。。 READ 3 : file 'locale.alias' dentry ffff9938d979b9c0 of size '2997' on device sda1 ino_p 16798337 dentry_p ffff9938c18e6180 file_p locale ino_m 128 dentry_m ffff9938de4976c0 file_m / 。。。 READ 3 : file 'helloworld.txt' dentry ffff9938d84ecf00 of size '11' on device sdb1 ino_p 2 dentry_p ffff9938c180a6c0 file_p / ino_m 2 dentry_m ffff9938c180a6c0 file_m / READ 3 : file 'helloworld.txt' dentry ffff9938d84ecf00 of size '11' on device sdb1 ino_p 2 dentry_p ffff9938c180a6c0 file_p / ino_m 2 dentry_m ffff9938c180a6c0 file_m /
对于 helloworld.txt的上一级的dentry,可以看到不是想象中的“world”而是“/”。 可以通过crash看下
crash> dentry ffff9938d84ecf00|grep parent d_parent = 0xffff9938c180a6c0, crash> dentry 0xffff9938c180a6c0 |grep name d_name = { name = 0xffff9938c180a6f8 "/" d_iname = "/\000\311}\302\177\000\000\220f\311}\302\177\000\000\000\000\000\000\000\000\000\000)\255\003\000\000\000\000",
可以看到和日志中打印应的dentry parent的dentry ffff9938c180a6c0, 一样
crash> dentry ffff9938c180a6c0|grep super crash> super_block 0xffff9938d8d66800|grep s_id s_id = "sdb1\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000",
也就是helloworld.txt 文件上级的dentry是文件系统 /dev/sdb1的根目录。他的挂载点是 /hello/world, 通过crash看下mount信息
crash> mount MOUNT SUPERBLK TYPE DEVNAME DIRNAME ffff9938cce15600 ffff9938ccc0e000 rootfs rootfs / 。。。 ffff9938db416300 ffff9938d8d66800 ext4 /dev/sdb1 /hello/world crash> struct mount ffff9938db416300|grep mnt_mountpoint mnt_mountpoint = 0xffff9938c18089c0, crash> dentry 0xffff9938c18089c0|grep name d_name = { name = 0xffff9938c18089f8 "world" d_iname = "world\000\000\000\324\016\002\200\001\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000",
可以看到, 跨文件系统,dentry维护的目录结构不是我们看到的目录结构, 如果查找是dentry不在内存中的话, 就需要通过磁盘进行查找, 这时候就需要通过用到mount中相关信息了。 磁盘查找的过程,更类似和我们平时查找文件的逻辑。
另外 dentrynegativity状态问题,通常由于关联文件被删除, 或者dentry解析一个不存在的路径,会出现无效的dentry, LWN 上这篇文章 Dentry negativity 描述了针对这种情况还除了一个补丁, 不过目前尚未被采纳。
参考及引用
图片来自 twitter Ines B @moraimauy
Comments are closed.