我不是这个主题的专家。我从未从头编写过文件系统;我只在 proc 文件系统上工作过,而且我在那里并没有做太多真正的文件系统 hacking,只是对已有的东西进行了扩展。所以,如果您在这里看到任何错误或遗漏(在一个关于如此庞大主题的如此短的文章中,肯定会有遗漏),请回复,以便我修复它们并让其他人了解它们。
在 Linux 中,所有文件都通过虚拟文件系统交换 (Virtual Filesystem Switch),或 VFS 访问。这是一个代码层,它实现了通用文件系统操作,并将请求定向到正确的特定代码来处理请求。两种主要类型的代码模块利用 VFS 服务,设备驱动程序和文件系统。由于 设备驱动程序 在 KHG 的其他地方已经介绍过,我们在这里将不再明确介绍它们。本次导览将重点介绍文件系统。由于 VFS 并非存在于真空中,我们将展示它与最受欢迎的 Linux 文件系统 ext2 文件系统的关系。
一个警告:如果不充分理解与文件相关的系统调用,您可能无法理解文件系统。VFS 的大部分和普通 Linux 文件系统中的大部分代码都与完成正常的系统调用直接相关,如果您不理解它所基于的系统调用,您将无法理解系统的其余部分是如何工作的。
VFS 的源代码位于 Linux 内核源代码的 fs/ 子目录中,以及一些其他相关部分,例如缓冲区缓存和处理每种可执行文件格式的代码。每个特定的文件系统都保存在较低的子目录中;例如,ext2 文件系统源代码保存在 fs/ext2/ 中。
下表给出了 fs/ 子目录中文件的名称,并解释了每个文件的基本用途。中间列,标记为system(系统),旨在显示文件(主要)专用于哪个主要子系统。EXE 表示它用于识别和加载可执行文件。DEV 表示它用于设备驱动程序支持。BUF 表示缓冲区缓存。VFS 表示它是 VFS 的一部分,并将某些功能委托给特定于文件系统的代码。VFSg 表示此代码是完全通用的,并且从不将其操作的一部分委托给特定的文件系统代码(无论如何,我注意到),并且您在编写文件系统时不必担心它。
文件名 | system | 用途 |
---|---|---|
binfmt_aout.c | EXE | 识别和执行旧式 a.out 可执行文件。 |
binfmt_elf.c | EXE | 识别和执行新的 ELF 可执行文件 |
binfmt_java.c | EXE | 识别和执行 Java 应用程序和小程序 |
binfmt_script.c | EXE | 识别和执行#!-style 脚本 |
block_dev.c | DEV | 块设备的通用 read()、write() 和 fsync() 函数。 |
buffer.c | BUF | 缓冲区缓存,它缓存从块设备读取的块。 |
dcache.c | VFS | 目录缓存,它缓存目录名称查找。 |
devices.c | DEV | 通用设备支持函数,例如注册表。 |
dquot.c | VFS | 通用磁盘配额支持。 |
exec.c | VFSg | 通用可执行文件支持。调用 binfmt_* 文件中的函数。 |
fcntl.c | VFSg | fcntl() 处理。 |
fifo.c | VFSg | fifo 处理。 |
file_table.c | VFSg | 系统上打开文件的动态可扩展列表。 |
filesystems.c | VFS | 所有编译到内核中的文件系统都从这里通过调用init_name_fs(). |
进行初始化 | VFSg | inode.c |
系统上打开的 inode 的动态可扩展列表。 | VFS | ioctl.c |
ioctl 的第一阶段处理;如果需要,将处理传递给文件系统或设备驱动程序。 | VFSg | locks.c |
支持 fcntl() 锁定、flock() 锁定和强制锁定。 | VFS | namei.c |
给定路径名,填充 inode。实现多个与名称相关的系统调用。 | VFS | noquot.c无配额:优化以避免#ifdef |
在 dquot.c 中的使用 | VFS | open.c |
大量系统调用,包括(惊喜)open()、close() 和 vhangup()。 | VFSg | pipe.c |
管道。 | VFS | read_write.c |
read()、write()、readv()、writev()、lseek()。 | VFS | readdir.c |
用于读取目录的几种不同接口。 | VFS | select.c |
select() 系统调用的核心 | VFS | stat.c |
stat() 和 readlink() 支持。 | VFS | super.c |
将文件系统附加到内核init_name_fs()如果您查看任何文件系统中
int init_ext2_fs(void) { return register_filesystem(&ext2_fs_type); }的代码,您会发现它可能包含大约一行代码。例如,在 ext2fs 中,它看起来像这样(来自 fs/ext2/super.c)它所做的只是向保存在 fs/super.c 中的注册表注册文件系统。ext2_fs_type
static struct file_system_type ext2_fs_type = { ext2_read_super, "ext2", 1, NULL };是一个非常简单的结构的ext2_read_super条目是指向函数的指针,该函数允许挂载文件系统(以及其他功能;稍后会详细介绍)。"ext2"是文件系统类型的名称,当您键入mount ... -t ext21) 时,用于确定使用哪个文件系统来挂载设备。表示它需要挂载设备的设备(不像 proc 文件系统或网络文件系统),而NULL
是必需的,用于填充将用于在文件系统注册表中保存文件系统类型链接列表的空间,该注册表保存在(在表中查找!)fs/super.c 中。
static struct file_system_type sysv_fs_type[3] = { {sysv_read_super, "xenix", 1, NULL}, {sysv_read_super, "sysv", 1, NULL}, {sysv_read_super, "coherent", 1, NULL} }; int init_sysv_fs(void) { int i; int ouch; for (i = 0; i < 3; i++) { if ((ouch = register_filesystem(&sysv_fs_type[i])) != 0) return ouch; } return ouch; }
将文件系统连接到磁盘文件系统代码和内核之间其余的通信发生在挂载承载该类型文件系统的设备时。当您挂载包含 ext2 文件系统的设备时,ext2_read_super()被调用。如果它成功读取超级块并能够挂载文件系统,它将用包含指向名为super_block结构的指针的信息填充super_operations
结构,该结构包含指向执行与超级块相关的常见操作的函数的指针;在这种情况下,是指向特定于 ext2 的函数的指针。结构的指针的信息填充超级块是定义设备上整个文件系统的块。它有时是虚构的,例如 DOS 文件系统的情况——也就是说,文件系统可能实际上在磁盘上没有作为真正超级块的块。如果没有,它必须编造一些东西。与整个文件系统相关的操作(而不是单个文件)被认为是超级块操作。结构包含指向操作 inode、超级块以及引用或更改整个文件系统状态的函数的指针 (statfs()和).
remount()
您可能已经注意到这里有很多指针,尤其是指向函数的指针。好消息是所有繁琐的指针工作都已完成;那是 VFS 的工作。文件系统的作者需要做的就是用指向函数的指针填充(通常是静态的)结构,并将指向这些结构的指针传递回 VFS,以便它可以访问文件系统和文件。例如,super_operations 结构看起来像这样(来自):
struct super_operations { void (*read_inode) (struct inode *); int (*notify_change) (struct inode *, struct iattr *); void (*write_inode) (struct inode *); void (*put_inode) (struct inode *); void (*put_super) (struct super_block *); void (*write_super) (struct super_block *); void (*statfs) (struct super_block *, struct statfs *, int); int (*remount_fs) (struct super_block *, int *, char *); };<linux/fs.h>
static struct super_operations ext2_sops = { ext2_read_inode, NULL, ext2_write_inode, ext2_put_inode, ext2_put_super, ext2_write_super, ext2_statfs, ext2_remount };那是 VFS 部分。这是 fs/ext2/super.c 中该结构的 ext2 实例的更简单的声明表示它需要挂载设备的设备(不像 proc 文件系统或网络文件系统),而首先,请注意未使用的条目已简单地设置为表示它需要挂载设备的设备(不像 proc 文件系统或网络文件系统),而。这是非常正常的 Linux 行为;每当函数指针存在合理的默认行为,并且您想要的合理默认行为时,您几乎肯定能够提供一个指针并轻松获得默认值。其次,请注意声明是多么简单和清晰。所有痛苦的东西,例如sb->s_op->write_super(sb);
都隐藏在 VFS 实现中。
挂载文件系统当文件系统被挂载时(哪个文件负责挂载文件系统?查看上面的表格,发现它是 fs/super.c。您可能想在 fs/super.c 中跟随),do_umount()文件系统代码和内核之间其余的通信发生在挂载承载该类型文件系统的设备时。当您挂载包含 ext2 文件系统的设备时,调用 read_super,最终调用(在 ext2 文件系统的情况下),, 它返回超级块。该超级块包括一个指向函数指针结构的指针,我们在ext2_sops上面的定义中看到了它。它还包括许多其他数据;如果您愿意,可以查看 include/linux/fs.h 中的struct super_block
查找文件
一旦文件系统被挂载,就可以访问该文件系统上的文件。这里有两个主要步骤:查找名称以找到它指向的 inode,然后访问 inode。/当 VFS 查看名称时,它包括路径。除非文件名是绝对路径(它以/字符开头),否则它是相对于发出包含路径的系统调用的进程的当前目录的。它使用特定于文件系统的代码来查找指定文件系统上的文件。它一次获取路径名的一个组件(文件名组件用
字符分隔),并查找它。如果它是一个目录,则在先前查找返回的目录中查找下一个组件。每个被查找的组件,无论是文件还是目录,都返回一个 inode 号,它唯一地标识它,并通过它访问其内容。
如果该文件最终是一个指向另一个文件的符号链接,则 VFS 使用从符号链接检索的新名称重新开始。为了防止无限递归,符号链接的深度有限制;内核最多只会连续跟踪一定数量的符号链接,然后放弃。当 VFS 和文件系统共同将名称解析为 inode 号时(那是 namei.c 中的namei()函数),就可以访问 inode。函数查找并返回由 inode 号指定的 inode。函数稍后用于释放对 inode 的访问。它有点像iget()malloc()statfs()free(),不同之处在于,多个进程可以同时打开一个 inode,并且维护一个引用计数以了解它何时空闲以及何时不空闲。
返回给应用程序代码的整数文件句柄是该进程的文件表中的偏移量。该文件表槽位保存着使用当 VFS 和文件系统共同将名称解析为 inode 号时(那是 namei.c 中的函数查找到的 inode 号,直到文件关闭或进程终止。因此,每当进程使用文件句柄对“文件”执行任何操作时,它实际上是在操作相关的 inode。
该 inode 号和inode结构必须来自某个地方,并且 VFS 无法自行创建它们。它们必须来自文件系统。那么 VFS 如何在文件系统中查找名称并获取inode?
它从路径名的开头开始,查找路径中第一个目录的 inode。然后它使用该 inode 查找路径中的下一个目录。当它到达末尾时,它找到了它试图查找的文件或目录的 inode。但是由于它需要一个inode才能开始,那么它如何开始第一次查找呢?超级块中保留一个名为s_mounted的 inode 指针,它指向文件系统的 inode 结构。此 inode 在挂载文件系统时分配,并在卸载文件系统时释放。通常,在 ext2 文件系统中,s_mountedinode 是文件系统根目录的 inode。从那里,可以查找所有其他 inode。
每个 inode 都包含一个指向函数指针结构的指针。听起来很熟悉?这是inode_operations结构。该结构的元素之一称为lookup(),它用于在同一文件系统上查找另一个 inode。通常,文件系统只有一个lookup()函数,该函数在文件系统上的每个 inode 中都是相同的,但可以有几个不同的lookup()函数,并根据文件系统的需要分配它们;proc 文件系统就是这样做的,因为 proc 文件系统中的不同目录有不同的用途。inode_operations结构看起来像这样(像我们正在查看的大多数内容一样,定义在例如,super_operations 结构看起来像这样(来自):
struct inode_operations { struct file_operations * default_file_ops; int (*create) (struct inode *,const char *,int,int,struct inode **); int (*lookup) (struct inode *,const char *,int,struct inode **); int (*link) (struct inode *,struct inode *,const char *,int); int (*unlink) (struct inode *,const char *,int); int (*symlink) (struct inode *,const char *,int,const char *); int (*mkdir) (struct inode *,const char *,int,int); int (*rmdir) (struct inode *,const char *,int); int (*mknod) (struct inode *,const char *,int,int,int); int (*rename) (struct inode *,const char *,int,struct inode *,const char *,int); int (*readlink) (struct inode *,char *,int); int (*follow_link) (struct inode *,struct inode *,int,int,struct inode **); int (*readpage) (struct inode *, struct page *); int (*writepage) (struct inode *, struct page *); int (*bmap) (struct inode *,int); void (*truncate) (struct inode *); int (*permission) (struct inode *, int); int (*smap) (struct inode *,int); };
这些函数中的大多数直接映射到系统调用。
在 ext2 文件系统中,目录、文件和符号链接具有不同的inode_operations(这是正常的)。文件 fs/ext2/dir.c 包含ext2_dir_inode_operations,文件 fs/ext2/file.c 包含ext2_file_inode_operations,文件 fs/ext2/symlink.c 包含ext2_symlink_inode_operations.
在inode_operations结构中没有考虑到许多与文件(和目录)相关的系统调用;这些系统调用可以在file_operations结构中找到。file_operations结构与编写 设备驱动程序 时使用的结构相同,并且包含专门针对文件而不是 inode 的操作
struct file_operations { int (*lseek) (struct inode *, struct file *, off_t, int); int (*read) (struct inode *, struct file *, char *, int); int (*write) (struct inode *, struct file *, const char *, int); int (*readdir) (struct inode *, struct file *, void *, filldir_t); int (*select) (struct inode *, struct file *, int, select_table *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); int (*mmap) (struct inode *, struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); void (*release) (struct inode *, struct file *); int (*fsync) (struct inode *, struct file *); int (*fasync) (struct inode *, struct file *, int); int (*check_media_change) (kdev_t dev); int (*revalidate) (kdev_t dev); };还有一些与系统调用没有直接关系的函数——在不适用的地方,它们可以简单地设置为表示它需要挂载设备的设备(不像 proc 文件系统或网络文件系统),而.
VFS 的作用是
因此,特定文件系统代码的作用是为每个挂载的文件系统提供一个超级块,为文件系统上的每个文件提供一个唯一的 inode,并提供代码来执行文件系统和文件特定的操作,这些操作由系统调用请求并由 VFS 分类。
版权所有 (C) 1996 Michael K. Johnson, johnsonm@redhat.com。