通过虚拟文件系统(VFS),程序可以利用标准的Unix系统调用对不同的文件系统,甚至不同介质上的文件系统进行读写操作,如下图所示:使用cp(1)
命令从ext3文件系统格式的硬盘拷贝数据到ext2文件系统格式的可移动磁盘上。两种不同的文件系统,两种不同的介质,连接到同一个VFS上。
1.文件系统抽象层
vfs可以使得用户可以直接使用open()
,read()
,write()
这样的系统调用而无须考虑具体文件系统和实际物理介质。vfs把各种不同的文件系统抽象后采用统一的方式进行操作。而之所以可以使用这种通用接口对所有类型的文件系统进行操作,是因为内核在它的底层文件系统接口上建立了一个抽象层。该抽象层使Linux能够支持各种文件系统,即便是它们在功能和行为上存在很大差异。下图展示了一次用户控件的write()
调用具体触发的流程。
2.Unix文件系统
Unix使用了四种和文件系统相关的传统抽象概念:文件,目录项,索引节点和安装点。
从本质上讲,文件系统是特殊的数据分层存储结构,它包含文件,目录和相关的控制信息。文件系统的通用操作包括创建,删除和安装等。
文件系统将文件的相关信息和文件本身这两个概念加以区分,例如访问控制权限,大小,拥有者,创建时间等。文件相关信息,有时被称为文件的元数据,被存储在一个单独的数据结构中,该结构被称为索引节点(inode: index node)。所有这些信息都和文件系统的控制信息密切相关,文件系统的控制信息存储在超级块中,超级块是一种包含文件系统信息的数据结构。
3.VFS对象及其数据结构
VFS其实采用的是面向对象的设计思路,使用一组数据结构来代表通用文件对象。因为内核纯粹使用C代码实现,所以内核中的数据结构都使用C语言的结构体实现,而这些结构体包含数据的同时也包含操作这些数据结构的指针,其中的操作函数由具体文件系统实现。VFS主要有四个对象类型:
- 超级块对象,它代表一个具体的已安装文件系统。
- 索引节点对象,它代表一个具体文件。
- 目录项对象,它代表一个目录项,是路径的一个组成部分。
- 文件对象,它代表由进程打开的文件。
注意:因为VFS将目录作为一个文件处理,所以不存在目录对象。 每个主要对象中都包含一个操作对象,这些操作对象描述了内核针对主要对象可以使用的方法:
super_operations
对象,其中包括内核针对特定文件系统所能调用的方法,比如write_inode()
和sync_fs()
等方法。inode_operations
对象,其中包括内核针对特定文件所能调用的方法,比如create()
,link()
,getattr()
,mkdir()
等。dentry_operations
对象,其中包括内核针对特定目录所能调用的方法,比如d_compare()
和d_delete()
等方法。file_operations
对象,其中包括进程针对已打开文件所能调用的方法,比如read()
,mmap()
,sendfile()
,flush()
等。
4.超级块对象
各种文件系统都必须实现超级块对象,该对象用于存储特定文件系统的信息,通常对应于存放在磁盘特定扇区中的文件系统超级块或文件系统控制块(所以成为超级块对象)。超级块对象中最重要的一个域是s_op
,它指向超级块的操作寒暑表。超级块操作函数表由super_operations
结构体表示,该结构体中的每一项都是一个指向超级块操作函数指针,超级块操作函数执行文件系统和索引节点的底层操作。
5.索引节点对象
索引节点对象包含了内核在操作文件或目录时需要的全部信息。对于Unix风格的文件系统来说,这些信息可以从磁盘索引节点直接读入。索引节点对象必须在内存中创建,以便于文件系统使用。一个索引节点代表文件系统中(但索引节点仅当文件被访问时,才在内存中创建)的一个文件,它也可以是设备或管道这样的特殊文件。
6.目录项对象
vfs把目录当做文件对待,所以在路径/bin/vi
中,bin和vi都属于文件--bin是特殊的目录文件而vi是一个普通文件,路径中的每个组成部分都由一个索引节点对象表示。虽然它们可以统一由索引节点表示,但vfs经常需要执行目录相关操作,比如路径名查找等。路径名查找需要解析路径中的每一个组成部分,不但要确保它有效,而且还需要再进一步寻找路径中的下一个部分。在一个路径中(包括普通文件在内),每一个部分都是目录项对象。
目录项对象有三种有效状态:被使用,未被使用和负状态。
- 被使用:一个被使用的目录项对应一个有效地索引节点并且表明该对象存在一个或多个使用者。一个目录项处于被使用状态,意味着它正被vfs使用并且指向有效地数据。
- 未被使用:一个未被使用的目录项对应一个有效地索引节点,但是应指明vfs当前并未使用它。该目录项对象仍然指向一个有效对象,而且被保留在缓存中以便需要时再使用它,由于该目录项不会过早地被撤销,所以以后再需要他时,不必重新创建,与未缓存的目录项相比,查找更快,但如果要回收内存,可以撤销未使用的目录项。
- 负状态:没有对应的有效索引节点。
内核将目录项对象缓存在目录项缓存中(dcache)。目录项缓存包括三个主要部分:1."被使用的"目录项链表;2."最近被使用的"双向链表:该链表包含未被使用的和负状态的目录项对象;3.散列表和相应地散列函数:用来快速的将给定路径解析为相关目录项对象。
举例说明:假设你需要在自己目录中编译一个源文件,/home/xx/src/sun.c
,每一次对文件进行访问(比如说,首先要打开它,然后要存储它,还要进行编译等),vfs都必须沿着嵌套的目录依次解析全部路径:/
,home
,xx
,src
,sun.c
。为了避免每次访问该路径名都进行这种耗时的操作,vfs会现在目录项缓存中搜索路径名,如果没找到,再通过遍历文件系统为每个路径分量解析路径,解析完毕后,再将目录项对象加入dcache中,一遍以后可以快速查找到它。
7.文件对象
文件对象是已打开的文件在内存中的表示。文件对象仅仅在进程观点上代表已打开文件。
8.和文件系统相关的数据结构
除了以上几种vfs基础对象外,内核还使用了另外一些标准数据结构来管理文件系统的其他相关数据。第一个对象是file_system_type
,用来描述各种特定文件系统类型,比如ext3,ext4或UDF。第二个结构是vfsmount,用来描述一个安装文件系统的实例。
9.和进程相关的数据结构
系统中每个进程都有自己的一组打开的文件,像根文件系统,当前工作目录,安装点等。有三个数据结构将vfs层和系统的进程紧密联系在一起:file_struct
,fs_struct
,namespace
结构体。
file_struct
由进程描述符中的files目录项指向,所有与单个进程相关的信息(如打开的文件及文件描述符)都包含在其中:
struct files_struct {
atomic_t count; //结构的使用计数
struct fdtable *fdt; //指向其他fd表的指针
struct fdtable fdtab; //基fd表
spinlock_t file_lock; //单个文件锁
int next_fd; //缓存下一个可用的fd
struct embedded_fd_set close_on_exec_init; //exec()时关闭的文件描述符链表
struct embedded_fd_set open_fds_init; //打开的文件描述符表
struct file *fd_array[NR_OPEN_DEFAULT]; //缺省的文件对象数组
};
fs_struct
由进程描述符的fs域指向,它包含了文件系统和进程相关的信息。
struct fs_struct {
int users; //用户数目
rwlock_t lock; //锁
int umask; //掩码
int in_exec; //当前正在执行的文件
struct path root; //根目录路径
struct path pwd; //当前工作目录的路径
}
namespace
由进程描述符中的mmt_namespace
域指向。2.4版内核后,单进程命名空间被加入到内核,它使得每一个进程在系统中都看到唯一的安装文件系统--不仅仅是唯一的根目录,而且是唯一的文件系统层次结构。
struct mmt_namespace {
atomic_t count; //结构的使用计数
struct vfsmount *root; //根目录的安装点对象
struct list_head list; //安装点链表
wait_queue_head_t poll; //轮询的等待队列
int event; //事件计数
}
参考
《Linux内核设计与实现》