赶上操作系统快要结课,Linux的VFS看样是不会讲了,限于时间没法系统地学习 Linux Kernel ,只能和大家做一个简单的分享。之前已经学习了Linux的启动过程,本文承接上文,可以从一个更底层的角度观察一下Linux的文件系统。
Linux通用文件模型(Common File Model)
文件系统
计算机的文件系统是一种存储和组织计算机数据的方法,它使得对其访问和查找变得容易,文件系统使用文件和树形目录的抽象逻辑概念代替了硬盘和光盘等物理设备使用数据块的概念,用户使用文件系统来保存数据不必关心数据实际保存在硬盘(或者光盘)的地址为多少的数据块上,只需要记住这个文件的所属目录和文件名。在写入新数据之前,用户不必关心硬盘上的那个块地址没有被使用,硬盘上的存储空间管理(分配和释放)功能由文件系统自动完成,用户只需要记住数据被写入到了哪个文件中。
文件系统通常使用硬盘和光盘这样的存储设备,并维护文件在设备中的物理位置。但是,实际上文件系统也可能仅仅是一种访问数据的界面而已,实际的数据是通过网络协议(如NFS、SMB、9P等)提供的或者内存上,甚至可能根本没有对应的文件(如proc文件系统)。
文件系统是一套实现了数据的存储、分级组织、访问和获取等操作的抽象数据类型(Abstract data type)。…元数据(Metadata)包含文件的基本信息。…
文件都有文件名与数据,这在 Linux 上被分成两个部分:用户数据 (user data) 与元数据 (metadata)。用户数据,即文件数据块 (data block),数据块是记录文件真实内容的地方;而元数据则是文件的附加属性,如文件大小、创建时间、所有者等信息。
图为Linux的/proc,即所谓procfs(PROCess File System),可以从上图看到,/proc/*文件均不占存储空间,是存于内存的伪文件系统,可以通过内核访问进程信息等。
文件系统有以下三种类型:
- 基于磁盘的文件系统,如ext2;微软开发的MS_DOS、nfts等。
- 基于网络的文件系统:如NFS。
- 特殊的文件系统:如sysfs和上文说的procfs。
诶,那什么叫基于网络的文件系统呢?共享文件吗?
网络文件系统(英语:Network File System,缩写作 NFS)是一种分布式文件系统协议,力求客户端主机可以访问服务器端文件,并且其过程与访问本地存储时一样,它由太阳微系统(已被甲骨文公司收购)开发,于1984年发布。
它基于开放网络运算远程过程调用(ONC RPC)协议:一个开放、标准的RFC协议,任何人或组织都可以依据标准实现它。相关编号在“外部链接”。
不知道大家有没有看过Linus的相关视频,这里推荐一个翼王的搭建100TB容量的文件服务器,在内网中搭建NAS,接入万兆光纤,在线剪辑甚至比在硬盘操作还要快,我想NFS应该也只局限于公司、工作室或者家庭内部使用吧。
通用文件模型
VFS的关键思想就是引入一个通用的文件模型,可以支持所有的文件系统。
Linux的内核支持装载EXT(第一个Linux文件系统)、常见的NTFS、FAT等等不同类型的文件系统,不同的文件系统有不同的数据组织方式,但是同一个操作系统中当然不能有多种数据的存取方式,因此为了支持各种文件系统,Linux内核提供了一个给文件系统公共接口也给用户进程(比如C/C++库)提供统一接口的抽象层,是介于文件系统和用户进程之间的一个抽象层,虚拟文件系统VFS。这样,每个特定的文件系统都必须将其物理组织转换为VFS的通用文件模型,也可以理解为,VFS是不同类型文件系统由Linux内核整合呈现的一个视图。
所谓通用文件模型,其实是严格反映了传统Unix文件系统提供的文件模型,可以将通用文件模型视为面向对象的一个模型(出于效率的考量没有使用C++等Object Oriented语言),对象在VFS中被实现为数据结构,「或者说,出于效率的考量没有使用C++等Object Oriented语言,使用C语言开发,因此对象被实现为了数据结构。」VFS的对象类型包括:超级块对象、索引节点对象、目录项对象和文件对象。换句话说就是,将所有不同类型文件系统的信息保存为内核中统一的数据结构,分别对应为:超级块数据结构、索引节点数据结构、目录项数据结构和文件数据结构。
下面分别来详细地学习各个VFS数据结构。数据结构当然同时包含数据域domains和数据操作operations啦,下文中domains/operations的表格根源是官方文档,这里选取的部分来自文献[3]。
超级块(Superblock)
内核为每一个已挂载的文件系统分配一个超级块,所有超级块对象组成一个链表,作为构成VFS的一部分存在。所有超级块用双向循环列表的方式链接在一起,每个具体的文件系统都需要提供这些超级块操作的具体实现,超级块操作可以实现文件系统的挂载、卸载、读写inode节点等等。
值得一提的是,从这里就可以看出所谓VFS是一个介于文件系统和用户进程之间的一个抽象层的含义,VFS定义了superblock,并要求每个文件系统都要根据superblock里给出的函数接口(C中是存在于结构体里的函数指针,很像Java的接口类就这么叫了),而对用户呈现出下文中那些VFS可以处理的系统调用,达到了抽象层或者中间层的效果。
超级块的作用
保存了文件系统的各种信息以及可以对其执行的操作。超级块的结构在下面的表里,可以更清楚地发现超级块用来存储文件系统的控制信息,包含了文件系统类型、状态、块大小、VFS的目录项信息、VFS索引节点信息,以及包含了对文件系统操作函数的函数指针等,存放于磁盘的特定扇区。
上文已经分析过了,VFS是介于不同类型文件系统之上,用户程序之下的一个软件层,VFS中的超级块数据结构就用来在顶层抽象不同的文件系统,每个文件系统都对应一个超级块,每个超级块链接到由struct superblock*定义的一个VFS中的superblock链表,从而将所有文件系统串联起来,同时所有索引节点inode都要链接到超级块,自然和目录项dentry指向当前文件目录根系统的那一个也要链接到超级块。
总的来说超级块对应一个文件系统,包含了文件系统的控制信息和其他相关VFS数据结构的信息。
超级块的结构
部分superblock domain
类型 | 域名 | 描述 |
---|---|---|
int | s_type | 文件系统类型 |
unsigned long | s_blocksize | 块大小(block size) |
struct dentry * | s_root | 指向文件系统根目录对应的dentry |
struct list_head | s_inodes | 文件系统中所有文件的inode(使用list_head双向链表存储) |
void * | s_fs_info | 指向具体文件系统实现(如ext2)的特有的数据结构 |
struct superblock* | s_op | superblock的操作函数(结构体里都是函数指针) |
部分superblock operations(存储在s_op中)
函数 | 功能 |
---|---|
alloc_inode(sb) | 为一个inode对象分配空间 |
destroy_inode(inode) | 销毁一个inode对象 |
read_inode(inode) | 从磁盘中读取inode数据,填充作为传入参数的inode对象 |
write_inode(inode, flag) | 使用内存中的inode信息更新磁盘中的inode信息 |
delete_inode(inode) | 删除内存中的inode对象同时删除磁盘上的inode |
索引节点(Inode)
所谓索引index,我一般理解其为指针pointer,而所谓inode就是一个指向实际文件资源的pointer。
索引节点的作用
说到了inode就不得不说dentry,所以这里提一下待会再详细说。下图源自文献[5]。
当文件存储到磁盘上去的时候,文件肯定会存放到一个磁盘位置上,可以这样想象,既然文件数据是存放在磁盘上的,如果我们知道这个文件数据的地址,当我们想要读写文件的时候,我们是不是直接使用这个地址去找到文件就可以了呢? [5]
所谓的索引节点指向磁盘的一块数据,一个文件,而每一个目录项dentry中都包含了文件名和inode索引节点,我们一般不能直接得到索引节点,所以需要目录项提供我们文件名,通过文件名得到绑定的inode指向所需的文件资源。这里就又会牵扯到两个简单的概念,Linux的文件共享用到了文件链接,硬链接是直接文件B直接指向A的inode所指资源,所以删掉A不影响B,软链接(符号链接)是B指向A,A删除后B的inode指向也不再有效。
索引节点的结构
部分inode domain
类型 | 域名 | 描述 |
---|---|---|
struct super_block * | i_sb | 指向inode所在的superblock对象 |
struct list_head | i_dentry | 这是一个双向链表的头节点,链表中保存的是指向该inode的dentry对象 |
unsigned long | i_ino | inode编号 |
umode_t | i_mode | 文件类型和访问权限域 |
unsigned int | i_nlink | 指向该inode的硬链接数量,为0时意味着该inode要销毁了 |
uid_t | i_uid | inode所有者的id |
struct timespec | i_atime | 上次访问的时间戳 |
unsigned long | i_blocks | 文件的块数目 |
unsigned short | i_bytes | 文件最后一个块的字节大小 |
struct inode_operations * | i_op | inode operations,inode的操作函数 |
部分inode operations(存储在i_op中)
函数 | 功能 |
---|---|
create(dir, dentry, mode, nameidata) | 创建一个inode |
lookup(dir, dentry, nameidata) | 在一个目录文件中查找和dentry包含的文件名匹配的inode |
link(old_dentry, dir, new_dentry) | 创建一个指向new_dentry的硬链接,保存在old_dentry中,该old_dentry和new_dentry指向同一个inode,即同一个文件。 |
symlink(dir, dentry, symname) | 创建一个新的inode,该inode是一个软连接文件,指向参数dentry |
mkdir(dir, dentry, mode) | 为dentry创建一个目录文件的inode |
目录项(Dentry)
参见Inode中的图。如/home/usr/local/fk.c,所谓目录,就是路径中的home(家目录)等路径组成项,而Linux将目录也视为文件,而不是文件夹的概念,所以称之为目录条目,或者目录项dentry。
dentry结构的主要用途就是建立文件名和相关的inode之间的联系。一个文件系统中的dentry对象都被放在一个Hash散列表中,同时不再使用的dentry对象被放到超级块指向的一个LRU链表中,在某个时间点会删除比较老的对象以释放内存。 [2]
当目录被读入内存,VFS就会把目录转换成基于dentry数据结构的一个目录项对象,也就是dentry结构体定义的一个目录项,超级块里记录了挂在了目录的信息,进程查找路径的时候,对路径名包含的每个目录,VFS都创建一个这样的对象。
目录项的作用
dentry数据结构用于将inode(表示文件)和目录项(表示文件路径)关联起来。inode中是没有文件路径的,所以需要dentry来将文件路径和文件关联起来。dentry在磁盘中没有对应的镜像,所以不需要考虑该dentry是否需要更新。
内核在进行路径查找的时候会为每一级都建立一个dentry,比如/home/damon/cppfile/test.cpp。就需要建立5个dentry(第一个是根目录/,第二个是home,第5个是test.cpp)。
dentry cache将已经查找过的路径缓存在内存中,这样下次查找的时候就不需要重新读取每一级的目录文件进行查找,可以直接通过dentry cache获得对应的inode。[3]
目录项的结构
部分dentry domain
类型 | 域名 | 描述 |
---|---|---|
atomic_t | d_count | dentry对象的使用计数器 |
struct inode * | d_inode | dentry指向的inode |
struct dentry * | d_parent | 指向上级目录的dentry |
struct qstr | d_name | 文件名 |
struct list_head | d_subdirs | 如果当前dentry是目录的dentry,那么双向链表保存的是所有子目录的dentry |
struct super_block * | d_sb | dentry对应的super block |
struct dentry_operations* | d_op | Dentry methods,dentry的操作函数 |
部分dentry operations(存储在d_op中)
函数 | 功能 |
---|---|
d_revalidate(dentry, nameidata) | 判断当前dentry对象是仍然有效,应该是dentry cache中使用的 |
d_hash(dentry, name) | 计算哈希值,应该也是dentry cache中使用的 |
d_delete(dentry) | 在d_count为0时,删除dentry,默认的VFS函数什么都不做 |
文件(File)
文件对象,文件数据结构用于存放打开文件与进程之间进行交互的有关信息。
文件,是一组在逻辑上具有完整意义的信息项的系列。在Linux中,除了普通文件,其他诸如目录、设备、套接字等也以文件被对待。总之,“一切皆文件”。 [4]
“一切皆是文件”是 Unix/Linux 的基本哲学之一。不仅普通的文件,目录、字符设备、块设备、 套接字等在 Unix/Linux 中都是以文件被对待;它们虽然类型不同,但是对其提供的却是同一套操作界面。
其数据结构自然包含了它对应的dentry目录项,和文件操作等相关信息。
文件数据结构
部分file domain
类型 | 域名 | 描述 |
---|---|---|
struct dentry * | f_dentry | 和该file对应的dentry |
struct vfsmount * | f_vfsmnt | file所在的文件系统(文件系统挂载的数据结构) |
unsigned int | f_flags | 打开文件时使用的flag |
mode_t | f_mode | 进程access mode |
struct file_operations * | f_op | file operations,文件操作函数 |
部分dentry operations(存储在d_op中)
函数 | 功能 |
---|---|
open(inode, file) | 打开文件 |
llseek(file, offset, origin) | 移动文件指针 |
read(file, buf, count, offset) | 读文件 |
write(file, buf, count, offset) | 写文件 |
VFS数据结构间关系
有点像之前学过的设备管理,不过也确实都是将资源抽象成数据结构再提供接口和系统调用统一管理的方式,当然形式相仿。所以也找来一个图描述数据结构之间的关系。图源自文献[2]。
红色字体是内核中的全局链表。
VFS的部分系统调用整理
VFS的部分系统调用整理在这,看到这些或熟悉或陌生的指令,都是由VFS进行处理的!表源自文献[1]。
System call name | Description |
---|---|
mount( ) umount( ) | 挂载/卸载文件系统 |
sysfs( ) | 获取文件系统信息 |
statfs( ) fstatfs( ) ustat( ) | 获取文件系统统计信息 |
chroot( ) pivot root( ) | 更改根目录 |
chdir( ) fchdir( ) getcwd( ) | 操作当前目录 |
mkdir( ) rmdir( ) | 创建和销毁目录 |
getdents( ) readdir( ) link( ) unlink( ) rename( ) | 处理目录项 |
readlink( ) symlink( ) | 操作软链接 |
chown( ) fchown( ) lchown( ) | 修改文件所有者 |
chmod( ) fchmod( ) utime( ) | 修改文件属性 |
stat( ) fstat( ) lstat( ) access( ) | 读取文件状态 |
open( ) close( ) creat( ) umask( ) | 打开和关闭文件 |
dup( ) dup2( ) fcntl( ) | 处理文件描述符 |
select( ) poll( ) | 异步I / O通知 |
truncate( ) ftruncate( ) | 变更档案大小 |
lseek( ) llseek( ) | 更改文件指针 |
read( ) write( ) readv( ) writev( ) sendfile( ) readahead( ) | 进行文件I / O操作 |
pread( ) pwrite( ) | 查找文件并访问它 |
mmap( ) munmap( ) madvise( ) mincore( ) | 处理文件内存映射 |
fdatasync( ) fsync( ) sync( ) msync( ) | 同步文件数据 |
flock( ) | 操纵文件锁 |
注册和挂载文件系统
这部分要用到两个数据结构,一个是file数据结构中的struct vfsmount,因为Linux中目录也是文件,要将一个文件系统挂载到内核已有的文件系统的目录树上,也就是将新的文件系统以文件的形式放入原文件系统,需要一个挂载点记录管理挂载的文件系统的信息,这个挂载点用的就是struct vfsmount表示;另一个是struct file_system_type,就是内核启动或者安装内核模块时注册文件系统的类型。
我们都知道Linux里面使用mount/unmount(mount [-t fstype] something somewhere)[2]系统调用挂载/卸载一个文件系统,而内核在挂载之前必须要支持挂载的文件系统,所以在内核启动或者安装内核模块时使用register_filesystem()注册文件系统类型到内核,同时将文件类型加入全局单链表file_systems。
如上所述,每个挂载的文件系统对应一个struct vfsmount结构体的实例,并且挂载的时候,比如在内核文件系统的根目录/挂载squashfs文件系统,/目录项会出现一个挂载点,再在根文件系统squashfs的/mnt中挂载ramfs文件系统,mnt目录项就出现一个挂载点vfsmount实例,/和mnt上的两个挂载点出现了父子关系,这种关系存储在struct vfsmount中。
在下图中,根文件系统为squashfs,根目录为“/”,然后创建/tmp目录,并挂载为ramfs,之后又创建了/tmp/usbdisk/volume9和/tmp/usbdisk/volume1两个目录,并将/tmp/dev/sda1和/tmp/dev/sdb1两个分区挂载到这两个目录上。其中/tmp/dev/sda1设备上有如下文件:
gccbacktrace/
——> gcc_backtrace.c
——>man_page.log
——>readme.txt
notes-fs.txt
smb.conf
挂载完成后,VFS中相关的数据结构的关系如图所示。
mount系统调用在内核中的入口点是sys_mount函数,该函数将装载的选项从用户态复制一份,然后调用do_mount()函数进行挂载,这个函数做的事情就是通过特定文件系统读取超级块和inode信息,然后建立VFS的数据结构并建立上图中的关系。
在父文件系统中的某个目录上挂载另一个文件系统后,该目录原来的内容就被隐藏了。例如,/tmp/samba/是非空的,然后,我将/tmp/dev/sda1挂载到/tmp/samba上,那这时/tmp/samba/目录下就只能看到/tmp/dev/sda1设备上的文件,直到将该设备卸载,原来目录中的文件才会显示出来。这是通过struct vfsmount中的mnt_mountpoint和mnt_root两个成员来实现的,这两个成员分别保存了在父文件系统中挂载点的dentry和在当前文件系统中挂载点的dentry,在卸载当前挂载点之后,可以找回挂载目录在父文件系统中的dentry对象。
—引自文献[2]
参考文献
[1] The Common File Model - Linux Kernel Reference
[3] VFS中的数据结构(superblock、dentry、inode、file)
小结
其实是作为实验报告来用的♪(\^∇\^*)。
其他的比如VFS的I/O,关于Linux的VFS还有很多知识亟待学习,今后有时间再继续学习。
2023-10-29
时隔多年搭了台FreeBSD OS的NAS再回来看这篇文章,感慨当时年少,感慨时间如梭。