虚拟文件系统是内核的子系统,实现了供用户空间编程用的文件系统相关的接口,其抽象特性使得不同的文件系统看起来用起来都一样,如将 ext2, ext3 格式的文件系统 mount 到不同的目录,ls 指令可以查看两个不同的文件系统内容。
Unix Filesystems
文件系统是数据的分层存储,这些数据遵循特定的结构。在 Unix 中,文件系统挂载在称为 namespace 的全局层次结构中的特定挂载点,这使所有挂载的文件系统显示为单个树中的条目。文件是一个有序的字节串,首字节表示文件头,末字节表示文件尾。目录实际上也是文件,列出了包含在其中的文件,由于这种观念,文件的操作同样适合于目录操作。Unix 系统对文件的概念和其相关信息作了区分,如访问权限,产生时间等,这些称为文件元数据,存储在另一个数据结构中,inode(index node),所有的信息(文件系统及其控制信息)联合在一起存储在超级块中。
Unix 文件系统将这些概念(文件、目录项、索引节点、超级块)实现为其物理磁盘布局的一部分。非 Unix 文件系统,如FAT或NTFS,仍然可以在Linux中工作,但是它们的文件系统代码必须表现出这些概念。VFS 就是设计来和这些文件系统工作的。
VFS Objects and Their Data Structures
VFS 是面向对象的。。。包括 4 个基本对象类型:
1. 超级块对象,代表一个特定的挂载的文件系统,对应存储在磁盘指定区域的文件系统超级块。
2. 索引节点对象,代表内核对文件或目录操作所需的所有信息,可以从磁盘获取。
3. 目录项对象,是一个路径中包括文件在内的所有组件,如 /bin/vi 中, /,bin,vi 均为目录项对象,不对应磁盘上任何数据结构
4. 文件对象,代表和一个进程关联的打开的文件,不对应磁盘上任何数据结构,指向相关联的目录项对象
既然面向对象了,那数据抽象和行为抽象应有体现,每个对象都包含一个 operations 对象
- super_operations object - write_inode() / sync_fs() /... kernel 对指定文件系统调用
- inode_operations object - create() /link() /... kernel 对指定文件调用
- dentry_operations object - d_compare() / d_delete() / ... kernel 对指定目录项调用
- file_operations object - read() / write() / ... kernel 进程对打开的的文件调用
目录项状态:used,unused,negative
used dentry → a valid inode (d_inode points to an associated inode && d_count > 0)
unused dentry → a valid inode (d_inode points to an inode && d_count = 0 )
negative dentry → not associated with a valid inode (d_inode = NULL)
目录项缓存:把路径名解析成目录项对象后缓存起来,避免每次访问时重复解析。
Data Structures Associated with Filesystems
file_system_type 描述每个文件系统的功能和行为。
#include <linux/fs.h>
struct file_system_type {
const char *name; /* filesystem’s name */
int fs_flags; /* filesystem type flags */
/* the following is used to read the superblock off the disk */
struct super_block *(*get_sb) (struct file_system_type *, int,
char *, void *);
/* ... */
};
vfsmount 记录所有的挂载点信息。
#include <linux/mount.h>
struct vfsmount {
struct list_head mnt_hash; /* hash table list */
struct vfsmount *mnt_parent; /* parent filesystem */
struct dentry *mnt_mountpoint; /* dentry of this mount point */
struct dentry *mnt_root; /* dentry of root of this fs */
/* ... */
};
Data Structures Associated with a Process
三个数据结构将 VFS layer 和系统进程联系在一起,files_struct, fs_struct, and namespace。
与进程相关的文件和文件描述符信息都包含在 files_struct 内,fd_array 指向打开的文件对象列表。
#include <linux/fdtable.h>
struct files_struct {
atomic_t count; /* usage count */
struct fdtable *fdt; /* pointer to other fd table */
struct fdtable fdtab; /* base fd table */
spinlock_t file_lock; /* per-file lock */
int next_fd; /* cache of next available fd */
struct embedded_fd_set close_on_exec_init; /* list of close-on-exec fds */
struct embedded_fd_set open_fds_init /* list of open fds */
struct file *fd_array[NR_OPEN_DEFAULT]; /* base files array */
};
fs_struct 包含与进程相关的文件系统信息,并由进程描述符中的 fs 字段指向。保存了当前目录和根目录。
#include <linux/fs_struct.h>
struct fs_struct {
int users; /* user count */
rwlock_t lock; /* per-structure lock */
int umask; /* umask */
int in_exec; /* currently executing a file */
struct path root; /* root directory */
struct path pwd; /* current working directory */
};
namespace 使每个进程对于已挂载的文件系统的拥有唯一视图,不只是根目录,而是整个文件系统层次结构。
#include <linux/mnt_namespace.h>
struct mnt_namespace {
atomic_t count; /* usage count */
struct vfsmount *root; /* root directory */
struct list_head list; /* list of mount points */
wait_queue_head_t poll; /* polling waitqueue */
int event; /* event count */
};
ext2 文件系统
一个磁盘可以划分成多个分区,每个分区必须先用格式化工具(例如某种mkfs命令)格式化成某种格式的文件系统,然后才能存储文件,格式化的过程会在磁盘上写一些管理存储布局的信息。下图是一个磁盘分区格式化成ext2文件系统后的存储布局。
文件系统中存储的最小单位是块(Block),一个块究竟多大是在格式化时确定的,例如mke2fs的-b选项可以设定块大小为1024、 2048或4096字节。而上图中启动块( Boot Block)的大小是确定的,就是1KB,启动块是由PC标准规定的,用来存储磁盘分区信息和启动 信息,任何文件系统都不能使用启动块。启动块之后才是ext2文件系统的开始,ext2文件系统将 整个分区划成若干个同样大小的块组( Block Group),每个块组都由以下部分组成。
- 超级块( Super Block)
描述整个分区的文件系统信息,例如块大小、文件系统版本号、上次mount的时间等等。 超级块在每个块组的开头都有一份拷贝。 - 块组描述符表( GDT, Group Descriptor Table)
由很多块组描述符组成,整个分区分成多少个块组就对应有多少个块组描述符。每个块组 描述符( Group Descriptor) 存储一个块组的描述信息,例如在这个块组中从哪里开始 是inode表,从哪里开始是数据块,空闲的inode和数据块还有多少个等等。和超级块类 似,块组描述符表在每个块组的开头也都有一份拷贝,这些信息是非常重要的,一旦超级 块意外损坏就会丢失整个分区的数据,一旦块组描述符意外损坏就会丢失整个块组的数 据,因此它们都有多份拷贝。通常内核只用到第0个块组中的拷贝,当执行e2fsck检查文 件系统一致性时,第0个块组中的超级块和块组描述符表就会拷贝到其它块组,这样当 第0个块组的开头意外损坏时就可以用其它拷贝来恢复,从而减少损失。 - 块位图( Block Bitmap)
用来描述整个块组中哪些块已用哪些块空闲的, 它本身占一个块,其中的每个bit代表本块组中的一个块,这个bit为1表示该块已用,这 个bit为0表示该块空闲可用。
为什么用df命令统计整个磁盘的已用空间非常快呢?因为只需要查看每个块组的块位图即 可,而不需要搜遍整个分区。相反,用du命令查看一个较大目录的已用空间就非常慢,因 为不可避免地要搜遍整个目录的所有文件。 - inode位图( inode Bitmap)
一个文件除了数据需要存储之外,一些描述信息也需要存储,例如文件类型 (常规、目录、符号链接等),权限,文件大小,创建/修改/访问时间等,也就是ls -l命 令看到的那些信息,这些信息存在inode中而不是数据块中。每个文件都有一个inode,一 个块组中的所有inode组成了inode表。 - 数据块( Data Block)
对于常规文件,文件的数据存储在数据块中。
对于目录,该目录下的所有文件名和目录名存储在数据块中,注意文件名保存在它 所在目录的数据块中,除文件名之外, ls -l 命令看到的其它信息都保存在该文件 的inode中。
对于符号链接,如果目标路径名较短则直接保存在inode中以便更快地查找,如果目 标路径名较长则分配一个数据块来保存。
设备文件、 FIFO和socket等特殊文件没有数据块,设备文件的主设备号和次设备号 保存在inode中。
VFS 文件操作如下图