下一页 上一页 目录

3. 虚拟文件系统 (VFS)

3.1 Inode 缓存以及与 Dcache 的交互

为了支持多种文件系统,Linux 包含一个特殊的内核接口层,称为 VFS(虚拟文件系统交换)。这类似于 SVR4 衍生系统中发现的 vnode/vfs 接口(最初来源于 BSD 和 Sun 的原始实现)。

Linux inode 缓存实现在单个文件 fs/inode.c 中,该文件包含 977 行代码。有趣的是,在过去的 5-7 年里,它没有进行太多更改:将最新版本与例如 1.3.42 版本进行比较,仍然可以识别出其中的一些代码。

Linux inode 缓存的结构如下

  1. 一个全局哈希表 inode_hashtable,其中每个 inode 都通过超级块指针的值和 32 位 inode 编号进行哈希。没有超级块的 inode (inode->i_sb == NULL) 将被添加到由 anon_hash_chain 引导的双向链表中。匿名 inode 的示例是由 net/socket.c:sock_alloc() 创建的套接字,通过调用 fs/inode.c:get_empty_inode() 创建。
  2. 一个全局的类型“使用中”列表 (inode_in_use),其中包含 i_count>0i_nlink>0 的有效 inode。由 get_empty_inode()get_new_inode() 新分配的 inode 将被添加到 inode_in_use 列表中。
  3. 一个全局的类型“未使用”列表 (inode_unused),其中包含 i_count = 0 的有效 inode。
  4. 一个每个超级块类型的“脏”列表 (sb->s_dirty),其中包含 i_count>0i_nlink>0i_state & I_DIRTY 的有效 inode。当 inode 被标记为脏时,如果它也被哈希,则会被添加到 sb->s_dirty 列表中。维护每个超级块的 inode 脏列表可以快速同步 inode。
  5. Inode 缓存本身 - 一个名为 inode_cachep 的 SLAB 缓存。当 inode 对象被分配和释放时,它们会从这个 SLAB 缓存中获取和返回。

类型列表锚定于 inode->i_list,哈希表锚定于 inode->i_hash。每个 inode 可以位于一个哈希表和一个且仅一个类型(使用中、未使用或脏)列表中。

所有这些列表都由单个自旋锁保护:inode_lock

Inode 缓存子系统在从 init/main.c:start_kernel() 调用 inode_init() 函数时初始化。该函数被标记为 __init,这意味着它的代码稍后会被丢弃。它传递一个参数 - 系统上的物理页面数。这是为了 inode 缓存可以根据可用内存量配置自身,即如果有足够的内存,则创建一个更大的哈希表。

关于 inode 缓存的唯一统计信息是未使用 inode 的数量,存储在 inodes_stat.nr_unused 中,用户程序可以通过文件 /proc/sys/fs/inode-nr/proc/sys/fs/inode-state 访问。

我们可以从在活动内核上运行的 gdb 中检查其中一个列表,如下所示


(gdb) printf "%d\n", (unsigned long)(&((struct inode *)0)->i_list)
8
(gdb) p inode_unused
$34 = 0xdfa992a8
(gdb) p (struct list_head)inode_unused
$35 = {next = 0xdfa992a8, prev = 0xdfcdd5a8}
(gdb) p ((struct list_head)inode_unused).prev
$36 = (struct list_head *) 0xdfcdd5a8
(gdb) p (((struct list_head)inode_unused).prev)->prev
$37 = (struct list_head *) 0xdfb5a2e8
(gdb) set $i = (struct inode *)0xdfb5a2e0
(gdb) p $i->i_ino
$38 = 0x3bec7
(gdb) p $i->i_count
$39 = {counter = 0x0}

请注意,我们从地址 0xdfb5a2e8 中减去 8 以获得 struct inode 的地址 (0xdfb5a2e0),根据 include/linux/list.hlist_entry() 宏的定义。

为了理解 inode 缓存的工作原理,让我们跟踪 ext2 文件系统上常规文件的 inode 在打开和关闭时的生命周期


fd = open("file", O_RDONLY);
close(fd);

open(2) 系统调用在 fs/open.c:sys_open 函数中实现,实际工作由 fs/open.c:filp_open() 函数完成,该函数分为两个部分

  1. open_namei():填充包含 dentry 和 vfsmount 结构的 nameidata 结构。
  2. dentry_open():给定一个 dentry 和 vfsmount,此函数分配一个新的 struct file 并将它们链接在一起;它还调用文件系统特定的 f_op->open() 方法,该方法在 inode 从 open_namei() (通过 dentry->d_inode 提供 inode) 读取时在 inode->i_fop 中设置。

open_namei() 函数通过 path_walk() 与 dentry 缓存交互,path_walk() 又调用 real_lookup()real_lookup() 调用文件系统特定的 inode_operations->lookup() 方法。此方法的作用是在父目录中找到具有匹配名称的条目,然后执行 iget(sb, ino) 以获取相应的 inode - 这将我们带到 inode 缓存。当 inode 被读取时,dentry 通过 d_add(dentry, inode) 实例化。在我们讨论这个问题时,请注意,对于具有磁盘 inode 编号概念的 UNIX 风格的文件系统,查找方法的工作是将它的字节序映射到当前的 CPU 格式,例如,如果原始(文件系统特定的)目录条目中的 inode 编号是小端 32 位格式,则可以执行


unsigned long ino = le32_to_cpu(de->inode);
inode = iget(sb, ino);
d_add(dentry, inode);

因此,当我们打开文件时,我们会命中 iget(sb, ino),它实际上是 iget4(sb, ino, NULL, NULL),它执行以下操作

  1. 尝试在 inode_lock 的保护下,在哈希表中查找具有匹配超级块和 inode 编号的 inode。如果找到 inode,则其引用计数 (i_count) 递增;如果递增之前它为 0 并且 inode 不是脏的,则将其从当前所在的任何类型列表 (inode->i_list) 中删除(当然必须是 inode_unused 列表),并插入到 inode_in_use 类型列表中;最后,inodes_stat.nr_unused 递减。
  2. 如果 inode 当前被锁定,我们会等到它被解锁,以便 iget4() 保证返回一个解锁的 inode。
  3. 如果在哈希表中未找到 inode,则这是我们第一次遇到此 inode,因此我们调用 get_new_inode(),并将指向哈希表中应插入位置的指针传递给它。
  4. get_new_inode()inode_cachep SLAB 缓存分配一个新的 inode,但此操作可能会阻塞 (GFP_KERNEL 分配),因此它必须释放保护哈希表的 inode_lock 自旋锁。由于它已释放自旋锁,因此之后必须重试在哈希表中搜索 inode;如果这次找到,它将返回(在通过 __iget 递增引用后)在哈希表中找到的 inode,并销毁新分配的 inode。如果仍然在哈希表中找不到,那么我们刚刚分配的新 inode 就是要使用的 inode;因此,它被初始化为所需的值,并调用文件系统特定的 sb->s_op->read_inode() 方法来填充 inode 的其余部分。这使我们从 inode 缓存返回到文件系统代码 - 请记住,当我们文件系统特定的 lookup() 方法调用 iget() 时,我们来到了 inode 缓存。当 s_op->read_inode() 方法从磁盘读取 inode 时,inode 被锁定 (i_state = I_LOCK);在 read_inode() 方法返回后,它被解锁,并且所有等待它的进程都被唤醒。

现在,让我们看看当我们关闭此文件描述符时会发生什么。close(2) 系统调用在 fs/open.c:sys_close() 函数中实现,该函数调用 do_close(fd, 1),它会撕裂(替换为 NULL)进程文件描述符表的描述符,并调用 filp_close() 函数,该函数完成大部分工作。有趣的事情发生在 fput() 中,它检查这是否是对文件的最后一次引用,如果是,则调用 fs/file_table.c:_fput(),它调用 __fput(),这是与 dcache 交互(因此与 inode 缓存交互 - 请记住 dcache 是 inode 缓存的主管!)发生的地方。fs/dcache.c:dput() 执行 dentry_iput(),通过 iput(inode) 将我们带回 inode 缓存,因此让我们理解 fs/inode.c:iput(inode)

  1. 如果传递给我们的参数为 NULL,我们绝对不做任何事情并返回。
  2. 如果存在文件系统特定的 sb->s_op->put_inode() 方法,则立即调用它,不持有任何自旋锁(因此它可以阻塞)。
  3. 获取 inode_lock 自旋锁,并且 i_count 递减。如果这不是对 inode 的最后一次引用,那么我们只需检查是否对它有太多引用,以至于 i_count 可以环绕分配给它的 32 位,如果是,我们打印警告并返回。请注意,我们在持有 inode_lock 自旋锁时调用 printk() - 这很好,因为 printk() 永远不会阻塞,因此可以在任何上下文中调用它(甚至从中断处理程序中!)。
  4. 如果这是最后一个活动引用,则需要完成一些工作。

iput() 在最后一个 inode 引用上执行的工作相当复杂,因此我们将其分成一个单独的列表

  1. 如果 i_nlink == 0(例如,当我们持有文件打开时,该文件被取消链接),则 inode 从哈希表和其类型列表中删除;如果页面缓存中为此 inode 持有任何数据页,则通过 truncate_all_inode_pages(&inode->i_data) 删除它们。然后调用文件系统特定的 s_op->delete_inode() 方法,该方法通常删除 inode 的磁盘副本。如果文件系统未注册 s_op->delete_inode() 方法(例如,ramfs),那么我们调用 clear_inode(inode),如果已注册,则调用 s_op->clear_inode(),并且如果 inode 对应于块设备,则此设备的引用计数将通过 bdput(inode->i_bdev) 递减。
  2. 如果 i_nlink != 0,那么我们检查在同一哈希桶中是否还有其他 inode,如果没有,则如果 inode 不是脏的,我们将其从其类型列表中删除并添加到 inode_unused 列表,递增 inodes_stat.nr_unused。如果同一哈希桶中存在 inode,那么我们将其从类型列表中删除并添加到 inode_unused 列表。如果这是一个匿名 inode (NetApp .snapshot),那么我们将其从类型列表中删除并完全清除/销毁它。

3.2 文件系统注册/注销

Linux 内核提供了一种机制,使新的文件系统可以以最小的努力编写。其历史原因是

  1. 在人们仍然使用非 Linux 操作系统来保护他们在遗留软件中的投资的世界中,Linux 必须通过支持大量的不同文件系统来提供互操作性 - 其中大多数文件系统本身不值得存在,而只是为了与现有的非 Linux 操作系统兼容。
  2. 文件系统编写者的接口必须非常简单,以便人们可以通过编写只读版本来尝试逆向工程现有的专有文件系统。因此,Linux VFS 使实现只读文件系统变得非常容易;95% 的工作是通过添加完整的写入支持来完成它们。作为一个具体的例子,我大约花了 10 个小时为 Linux 编写了只读 BFS 文件系统,但是花了几个星期才完成它以获得完整的写入支持(甚至今天一些纯粹主义者声称它不完整,因为它“没有紧凑化支持”)。
  3. VFS 接口是导出的,因此所有 Linux 文件系统都可以作为模块实现。

让我们考虑在 Linux 下实现文件系统所需的步骤。实现文件系统的代码可以是动态可加载模块或静态链接到内核,并且在 Linux 下完成它的方式非常透明。所有需要做的就是填写一个 struct file_system_type 结构,并使用 register_filesystem() 函数向 VFS 注册它,如下面的示例所示,来自 fs/bfs/inode.c


#include <linux/module.h>
#include <linux/init.h>

static struct super_block *bfs_read_super(struct super_block *, void *, int);

static DECLARE_FSTYPE_DEV(bfs_fs_type, "bfs", bfs_read_super);

static int __init init_bfs_fs(void)
{
        return register_filesystem(&bfs_fs_type);
}

static void __exit exit_bfs_fs(void)
{
        unregister_filesystem(&bfs_fs_type);
}

module_init(init_bfs_fs)
module_exit(exit_bfs_fs)

module_init()/module_exit() 宏确保,当 BFS 被编译为模块时,函数 init_bfs_fs()exit_bfs_fs() 分别变为 init_module()cleanup_module();如果 BFS 静态链接到内核,则 exit_bfs_fs() 代码会消失,因为它是不必要的。

struct file_system_typeinclude/linux/fs.h 中声明


struct file_system_type {
        const char *name;
        int fs_flags;
        struct super_block *(*read_super) (struct super_block *, void *, int);
        struct module *owner;
        struct vfsmount *kern_mnt; /* For kernel mount, if it's FS_SINGLE fs */
        struct file_system_type * next;
};

其中的字段解释如下

read_super() 函数的工作是填充超级块的字段,分配根 inode 并初始化与此文件系统挂载实例关联的任何文件系统私有信息。因此,通常 read_super() 会执行

  1. 使用缓冲区缓存 bread() 函数,从通过 sb->s_dev 参数指定的设备读取超级块。如果它预计立即读取更多后续元数据块,那么使用 breada() 来安排异步读取额外的块是有意义的。
  2. 验证超级块是否包含有效的魔数以及总体上“看起来”是否正常。
  3. 初始化 sb->s_op 以指向 struct super_block_operations 结构。此结构包含文件系统特定的函数,用于实现诸如“读取 inode”、“删除 inode”等操作。
  4. 使用 d_alloc_root() 分配根 inode 和根 dentry。
  5. 如果文件系统未以只读方式挂载,则将 sb->s_dirt 设置为 1,并将包含超级块的缓冲区标记为脏(TODO:我们为什么要这样做?我在 BFS 中这样做是因为 MINIX 这样做...)。

3.3 文件描述符管理

在 Linux 下,用户文件描述符和内核 inode 结构之间存在多个级别的间接性。当进程进行 open(2) 系统调用时,内核返回一个小的非负整数,该整数可用于对此文件进行后续 I/O 操作。此整数是指向 struct file 指针数组的索引。每个文件结构通过 file->f_dentry 指向一个 dentry。每个 dentry 通过 dentry->d_inode 指向一个 inode。

每个任务都包含一个字段 tsk->files,它是指向 include/linux/sched.h 中定义的 struct files_struct 的指针


/*
 * Open file table structure
 */
struct files_struct {
        atomic_t count;
        rwlock_t file_lock;
        int max_fds;
        int max_fdset;
        int next_fd;
        struct file ** fd;      /* current fd array */
        fd_set *close_on_exec;
        fd_set *open_fds;
        fd_set close_on_exec_init;
        fd_set open_fds_init;
        struct file * fd_array[NR_OPEN_DEFAULT];
};

file->count 是一个引用计数,由 get_file()(通常由 fget() 调用)递增,并由 fput()put_filp() 递减。fput()put_filp() 之间的区别在于 fput() 通常为常规文件执行更多工作,例如释放 flock 锁、释放 dentry 等,而 put_filp() 仅操作文件表结构,即递减计数,从 anon_list 中删除文件,并将其添加到 free_list,在 files_lock 自旋锁的保护下。

如果子线程是使用在 clone 标志参数中设置了 CLONE_FILESclone() 系统调用创建的,则父进程和子进程之间可以共享 tsk->files。这可以在 kernel/fork.c:copy_files() (由 do_fork() 调用) 中看到,如果设置了 CLONE_FILES,则只递增 file->count,而不是像经典 UNIX fork(2) 的传统那样复制文件描述符表。

当文件打开时,为其分配的文件结构安装到 current->files->fd[fd] 插槽中,并且在位图 current->files->open_fds 中设置一个 fd 位。所有这些都在 current->files->file_lock 读写自旋锁的写保护下完成。当描述符关闭时,current->files->open_fds 中的 fd 位被清除,并且 current->files->next_fd 被设置为等于 fd,作为下次此进程想要打开文件时查找第一个未使用描述符的提示。

3.4 文件结构管理

文件结构在 include/linux/fs.h 中声明


struct fown_struct {
        int pid;                /* pid or -pgrp where SIGIO should be sent */
        uid_t uid, euid;        /* uid/euid of process setting the owner */
        int signum;             /* posix.1b rt signal to be delivered on IO */
};

struct file {
        struct list_head        f_list;
        struct dentry           *f_dentry;
        struct vfsmount         *f_vfsmnt;
        struct file_operations  *f_op;
        atomic_t                f_count;
        unsigned int            f_flags;
        mode_t                  f_mode;
        loff_t                  f_pos;
        unsigned long           f_reada, f_ramax, f_raend, f_ralen, f_rawin;
        struct fown_struct      f_owner;
        unsigned int            f_uid, f_gid;
        int                     f_error;

        unsigned long           f_version;
  
        /* needed for tty driver, and maybe others */
        void                    *private_data; 
};

让我们看一下 struct file 的各种字段

  1. f_list:此字段将文件结构链接到以下列表之一(且仅限一个):a) sb->s_files 列表,其中包含此文件系统上所有打开的文件,如果相应的 inode 不是匿名的,则 dentry_open() (由 filp_open() 调用) 将文件链接到此列表;b) fs/file_table.c:free_list,包含未使用的文件结构;c) fs/file_table.c:anon_list,当通过 get_empty_filp() 创建新的文件结构时,它将被放置在此列表上。所有这些列表都受 files_lock 自旋锁保护。
  2. f_dentry:与此文件对应的 dentry。dentry 是在 nameidata 查找时由 open_namei()(或更确切地说是它调用的 path_walk())创建的,但实际的 file->f_dentry 字段由 dentry_open() 设置为包含这样找到的 dentry。
  3. f_vfsmnt:指向包含该文件的文件系统的 vfsmount 结构的指针。这由 dentry_open() 设置,但在 nameidata 查找时由 open_namei()(或更确切地说是它调用的 path_init())找到。
  4. f_op:指向 file_operations 的指针,其中包含可以在文件上调用的各种方法。这是从 inode->i_fop 复制的,inode->i_fop 是在 nameidata 查找期间由文件系统特定的 s_op->read_inode() 方法放置在那里的。我们将在本节稍后详细查看 file_operations 方法。
  5. f_count:由 get_file/put_filp/fput 操作的引用计数。
  6. f_flags:从 open(2) 系统调用复制到那里的 O_XXX 标志(由 filp_open() 稍作修改),由 dentry_open() 设置,并在清除 O_CREATO_EXCLO_NOCTTYO_TRUNC 之后 - 永久存储这些标志没有意义,因为它们不能通过 F_SETFL(或通过 F_GETFL 查询)fcntl(2) 调用来修改。
  7. f_mode:用户空间标志和模式的组合,由 dentry_open() 设置。转换的目的是将读取和写入访问权限存储在单独的位中,以便可以轻松进行诸如 (f_mode & FMODE_WRITE)(f_mode & FMODE_READ) 之类的检查。
  8. f_pos:用于下一次读取或写入文件的当前文件位置。在 i386 下,它的类型为 long long,即 64 位值。
  9. f_reada, f_ramax, f_raend, f_ralen, f_rawin:用于支持预读 - 太复杂,凡人无法讨论 ;)
  10. f_owner:文件 I/O 的所有者,通过 SIGIO 机制接收异步 I/O 通知(参见 fs/fcntl.c:kill_fasync())。
  11. f_uid, f_gid - 在 get_empty_filp() 中创建文件结构时,设置为打开文件的进程的用户 ID 和组 ID。如果文件是套接字,则由 ipv4 netfilter 使用。
  12. f_error:由 NFS 客户端用于返回写入错误。它在 fs/nfs/file.c 中设置,并在 mm/filemap.c:generic_file_write() 中检查。
  13. f_version - 用于使缓存无效的版本控制机制,每当 f_pos 更改时递增(使用全局 event)。
  14. private_data:文件系统(例如,coda 在此处存储凭据)或设备驱动程序可以使用的每个文件的私有数据。设备驱动程序(在存在 devfs 的情况下)可以使用此字段来区分多个实例,而不是编码在 file->f_dentry->d_inode->i_rdev 中的经典次设备号。

现在让我们看一下 file_operations 结构,其中包含可以在文件上调用的方法。让我们回顾一下,它是从 inode->i_fop 复制的,inode->i_fop 是由 s_op->read_inode() 方法设置的。它在 include/linux/fs.h 中声明


struct file_operations {
        struct module *owner;
        loff_t (*llseek) (struct file *, loff_t, int);
        ssize_t (*read) (struct file *, char *, size_t, loff_t *);
        ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
        int (*readdir) (struct file *, void *, filldir_t);
        unsigned int (*poll) (struct file *, struct poll_table_struct *);
        int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
        int (*mmap) (struct file *, struct vm_area_struct *);
        int (*open) (struct inode *, struct file *);
        int (*flush) (struct file *);
        int (*release) (struct inode *, struct file *);
        int (*fsync) (struct file *, struct dentry *, int datasync);
        int (*fasync) (int, struct file *, int);
        int (*lock) (struct file *, int, struct file_lock *);
        ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
        ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
};

  1. owner:指向拥有相关子系统的模块的指针。只有驱动程序需要将其设置为 THIS_MODULE,文件系统可以很高兴地忽略它,因为它们的模块计数在挂载/卸载时控制,而驱动程序需要在打开/释放时控制它。
  2. llseek:实现 lseek(2) 系统调用。通常省略它并使用 fs/read_write.c:default_llseek(),它会做正确的事情(TODO:强制所有当前将其设置为 NULL 的人使用 default_llseek - 这样我们在 llseek() 中节省了一个 if())。
  3. read:实现 read(2) 系统调用。文件系统可以将 mm/filemap.c:generic_file_read() 用于常规文件,并将 fs/read_write.c:generic_read_dir()(它只是返回 -EISDIR)用于此处的目录。
  4. write:实现 write(2) 系统调用。文件系统可以将 mm/filemap.c:generic_file_write() 用于常规文件,并在此处忽略目录。
  5. readdir:由文件系统使用。对于常规文件被忽略,并为目录实现 readdir(2)getdents(2) 系统调用。
  6. poll:实现 poll(2)select(2) 系统调用。
  7. ioctl:实现驱动程序或文件系统特定的 ioctl。请注意,诸如 FIBMAPFIGETBSZFIONREAD 之类的通用文件 ioctl 由更高级别实现,因此它们永远不会读取 f_op->ioctl() 方法。
  8. mmap:实现 mmap(2) 系统调用。文件系统可以将 generic_file_mmap 用于此处的常规文件,并在目录上忽略它。
  9. open:在 open(2) 时由 dentry_open() 调用。文件系统很少使用它,例如,coda 尝试在打开时在本地缓存文件。
  10. flush:在此文件的每个 close(2) 时调用,不一定是最后一个(请参阅下面的 release() 方法)。唯一使用它的是 NFS 客户端,用于刷新所有脏页。请注意,这可能会返回错误,该错误将传递回发出 close(2) 系统调用的用户空间。
  11. release:在此文件的最后一个 close(2) 时调用,即当 file->f_count 达到 0 时。虽然定义为返回 int,但 VFS 忽略返回值(参见 fs/file_table.c:__fput())。
  12. fsync:直接映射到 fsync(2)/fdatasync(2) 系统调用,最后一个参数指定它是 fsync 还是 fdatasync。VFS 在此周围几乎没有完成任何工作,除了将文件描述符映射到文件结构 (file = fget(fd)) 和 down/up inode->i_sem 信号量。Ext2 文件系统当前忽略最后一个参数,并且对于 fsync(2)fdatasync(2) 执行完全相同的操作。
  13. fasync:当 file->f_flags & FASYNC 更改时调用此方法。
  14. lock:POSIX fcntl(2) 文件区域锁定机制的文件系统特定部分。这里唯一的错误是,因为它在文件系统无关部分 (posix_lock_file()) 之前被调用,如果它成功但标准 POSIX 锁定代码失败,那么它将永远不会在文件系统相关级别上解锁。
  15. readv:实现 readv(2) 系统调用。
  16. writev:实现 writev(2) 系统调用。

3.5 超级块和挂载点管理

在 Linux 下,有关已挂载文件系统的信息保存在两个单独的结构中 - super_blockvfsmount。这样做的原因是 Linux 允许在多个挂载点下挂载相同的文件系统(块设备),这意味着相同的 super_block 可以对应于多个 vfsmount 结构。

让我们首先看一下 include/linux/fs.h 中声明的 struct super_block


struct super_block {
        struct list_head        s_list;         /* Keep this first */
        kdev_t                  s_dev;
        unsigned long           s_blocksize;
        unsigned char           s_blocksize_bits;
        unsigned char           s_lock;
        unsigned char           s_dirt;
        struct file_system_type *s_type;
        struct super_operations *s_op;
        struct dquot_operations *dq_op;
        unsigned long           s_flags;
        unsigned long           s_magic;
        struct dentry           *s_root;
        wait_queue_head_t       s_wait;

        struct list_head        s_dirty;        /* dirty inodes */
        struct list_head        s_files;

        struct block_device     *s_bdev;
        struct list_head        s_mounts;       /* vfsmount(s) of this one */
        struct quota_mount_options s_dquot;     /* Diskquota specific options */

       union {
                struct minix_sb_info    minix_sb;
                struct ext2_sb_info     ext2_sb;
                ..... all filesystems that need sb-private info ...
                void                    *generic_sbp;
        } u;
       /*
         * The next field is for VFS *only*. No filesystems have any business
         * even looking at it. You had been warned.
         */
        struct semaphore s_vfs_rename_sem;      /* Kludge */

        /* The next field is used by knfsd when converting a (inode number based)
         * file handle into a dentry. As it builds a path in the dcache tree from
         * the bottom up, there may for a time be a subpath of dentrys which is not
         * connected to the main tree.  This semaphore ensure that there is only ever
         * one such free path per filesystem.  Note that unconnected files (or other
         * non-directories) are allowed, but not unconnected diretories.
         */
        struct semaphore s_nfsd_free_path_sem;
};

super_block 结构中的各种字段是

  1. s_list:所有活动超级块的双向链表;请注意,我没有说“所有已挂载文件系统”,因为在 Linux 下,可以有与单个超级块对应的已挂载文件系统的多个实例。
  2. s_dev:对于需要挂载块的文件系统,即对于 FS_REQUIRES_DEV 文件系统,这是块设备的 i_dev。对于其他文件系统(称为匿名文件系统),这是一个整数 MKDEV(UNNAMED_MAJOR, i),其中 iunnamed_dev_in_use 数组中第一个未设置的位,介于 1 到 255 之间(含)。请参阅 fs/super.c:get_unnamed_dev()/put_unnamed_dev()。已经多次建议匿名文件系统不应使用 s_dev 字段。
  3. s_blocksize, s_blocksize_bits:块大小和 log2(块大小)。
  4. s_lock:指示超级块当前是否被 lock_super()/unlock_super() 锁定。
  5. s_dirt:当超级块更改时设置,并在每次写回磁盘时清除。
  6. s_type:指向相应文件系统的 struct file_system_type 的指针。文件系统的 read_super() 方法不需要设置它,因为 VFS fs/super.c:read_super() 会在文件系统特定的 read_super() 成功时为您设置它,并在失败时重置为 NULL。
  7. s_op:指向 super_operations 结构的指针,其中包含文件系统特定的方法,用于读取/写入 inode 等。正确初始化 s_op 是文件系统的 read_super() 方法的工作。
  8. dq_op:磁盘配额操作。
  9. s_flags:超级块标志。
  10. s_magic:文件系统的魔数。由 minix 文件系统用于区分自身的多种风格。
  11. s_root:文件系统根目录的 dentry。从磁盘读取根 inode 并将其传递给 d_alloc_root() 以分配 dentry 并实例化它是 read_super() 的工作。某些文件系统将“root”拼写为 “/” 以外的其他内容,因此使用更通用的 d_alloc() 函数将 dentry 绑定到名称,例如,pipefs 将自身挂载在 “pipe:” 上作为其自己的根目录而不是 “/”。
  12. s_wait:等待超级块解锁的进程的等待队列。
  13. s_dirty:所有脏 inode 的列表。回想一下,如果 inode 是脏的 (inode->i_state & I_DIRTY),那么它就在特定于超级块的脏列表中,通过 inode->i_list 链接。
  14. s_files:此超级块上所有打开文件的列表。用于决定文件系统是否可以重新挂载为只读,请参阅 fs/file_table.c:fs_may_remount_ro(),它遍历 sb->s_files 列表,如果存在为写入打开的文件 (file->f_mode & FMODE_WRITE) 或具有待处理取消链接的文件 (inode->i_nlink == 0),则拒绝重新挂载。
  15. s_bdev:对于 FS_REQUIRES_DEV,这指向描述文件系统挂载在其上的设备的 block_device 结构。
  16. s_mounts:所有 vfsmount 结构的列表,每个挂载的此超级块实例一个。
  17. s_dquot:更多磁盘配额的东西。

超级块操作在 include/linux/fs.h 中声明的 super_operations 结构中描述


struct super_operations {
        void (*read_inode) (struct inode *);
        void (*write_inode) (struct inode *, int);
        void (*put_inode) (struct inode *);
        void (*delete_inode) (struct inode *);
        void (*put_super) (struct super_block *);
        void (*write_super) (struct super_block *);
        int (*statfs) (struct super_block *, struct statfs *);
        int (*remount_fs) (struct super_block *, int *, char *);
        void (*clear_inode) (struct inode *);
        void (*umount_begin) (struct super_block *);
};

  1. read_inode:从文件系统读取 inode。它仅从 fs/inode.c:get_new_inode() 中的 iget4()(以及因此的 iget())调用。如果文件系统想要使用 iget(),则必须实现 read_inode() - 否则 get_new_inode() 将会 panic。当 inode 正在被读取时,它被锁定 (inode->i_state = I_LOCK)。当函数返回时,inode->i_wait 上的所有等待者都被唤醒。文件系统的 read_inode() 方法的工作是找到包含要读取的 inode 的磁盘块,并使用缓冲区缓存 bread() 函数将其读入并初始化 inode 结构的各种字段,例如 inode->i_opinode->i_fop,以便 VFS 级别知道可以在 inode 或相应文件上执行哪些操作。不实现 read_inode() 的文件系统是 ramfs 和 pipefs。例如,ramfs 有自己的 inode 生成函数 ramfs_get_inode(),所有 inode 操作都根据需要调用它。
  2. write_inode:将 inode 写回磁盘。类似于 read_inode(),因为它需要找到磁盘上的相关块,并通过调用 mark_buffer_dirty(bh) 与缓冲区缓存交互。当需要单独同步 inode 或作为同步整个文件系统的一部分时,会在脏 inode(那些标记为脏的 inode,使用 mark_inode_dirty())上调用此方法。
  3. put_inode:每当引用计数减少时调用。
  4. delete_inode: 当 inode->i_countinode->i_nlink 都达到 0 时调用。文件系统删除 inode 在磁盘上的副本,并在 VFS inode 上调用 clear_inode() 以“极其彻底地终止它”。
  5. put_super: 在 umount(2) 系统调用的最后阶段调用,以通知文件系统应该释放由文件系统持有的关于此实例的任何私有信息。通常,这将 brelse() 包含超级块的块,并 kfree() 为空闲块、inode 等分配的任何位图。
  6. write_super: 当超级块需要写回磁盘时调用。它应该找到包含超级块的块(通常保存在 sb-private 区域),并 mark_buffer_dirty(bh) 。它还应该清除 sb->s_dirt 标志。
  7. statfs: 实现 fstatfs(2)/statfs(2) 系统调用。请注意,作为参数传递给 struct statfs 的指针是一个内核指针,而不是用户指针,因此我们不需要对用户空间进行任何 I/O 操作。如果未实现,则 statfs(2) 将失败并返回 ENOSYS
  8. remount_fs: 每当文件系统被重新挂载时调用。
  9. clear_inode: 从 VFS 级别 clear_inode() 调用。将私有数据附加到 inode 结构(通过 generic_ip 字段)的文件系统必须在此处释放它。
  10. umount_begin: 在强制卸载期间调用,以预先通知文件系统,以便它可以尽最大努力确保没有任何东西使文件系统繁忙。目前仅由 NFS 使用。这与通用 VFS 级别的强制卸载支持的想法无关。

那么,让我们看看当我们挂载一个磁盘上的 (FS_REQUIRES_DEV) 文件系统时会发生什么。 mount(2) 系统调用的实现在 fs/super.c:sys_mount() 中,这只是一个包装器,用于为 do_mount() 函数复制选项、文件系统类型和设备名称,而 do_mount() 函数才是真正完成工作的函数。

  1. 文件系统驱动程序在需要时加载,并且其模块的引用计数会递增。请注意,在挂载操作期间,文件系统模块的引用计数会递增两次 - 一次是 do_mount() 调用 get_fs_type(),另一次是 get_sb_dev() 调用 get_filesystem()(如果 read_super() 成功)。第一次递增是为了防止在我们位于 read_super() 方法内部时模块被卸载,第二次递增是为了指示模块正在被此挂载实例使用。显然,do_mount() 在返回之前会递减计数,因此总体而言,每次挂载后计数仅增加 1。
  2. 由于在我们的例子中,fs_type->fs_flags & FS_REQUIRES_DEV 为真,因此超级块通过调用 get_sb_bdev() 进行初始化,该调用获取对块设备的引用,并与文件系统的 read_super() 方法交互以填充超级块。如果一切顺利,则 super_block 结构被初始化,并且我们对文件系统模块有一个额外的引用,以及对底层块设备的引用。
  3. 一个新的 vfsmount 结构被分配并链接到 sb->s_mounts 列表和全局 vfsmntlist 列表。 vfsmount 字段 mnt_instances 允许查找与此实例挂载在同一超级块上的所有实例。 mnt_list 字段允许查找系统范围内所有超级块的所有实例。 mnt_sb 字段指向此超级块,而 mnt_rootsb->s_root dentry 有一个新的引用。

3.6 虚拟文件系统示例:pipefs

作为一个不需要块设备即可挂载的 Linux 文件系统的简单示例,让我们考虑来自 fs/pipe.c 的 pipefs。文件系统的序言相当简单,几乎不需要解释


static DECLARE_FSTYPE(pipe_fs_type, "pipefs", pipefs_read_super,
        FS_NOMOUNT|FS_SINGLE);

static int __init init_pipe_fs(void)
{
        int err = register_filesystem(&pipe_fs_type);
        if (!err) {
                pipe_mnt = kern_mount(&pipe_fs_type);
                err = PTR_ERR(pipe_mnt);
                if (!IS_ERR(pipe_mnt))
                        err = 0;
        }
        return err;
}

static void __exit exit_pipe_fs(void)
{
        unregister_filesystem(&pipe_fs_type);
        kern_umount(pipe_mnt);
}

module_init(init_pipe_fs)
module_exit(exit_pipe_fs)

文件系统的类型为 FS_NOMOUNT|FS_SINGLE,这意味着它不能从用户空间挂载,并且在整个系统中只能有一个超级块。 FS_SINGLE 文件也意味着它必须在通过 register_filesystem() 成功注册后通过 kern_mount() 挂载,这正是 init_pipe_fs() 中发生的情况。此函数中唯一的 bug 是,如果 kern_mount() 失败(例如,因为 kmalloc()add_vfsmnt() 中失败),则文件系统将保持已注册状态,但模块初始化失败。这将导致 cat /proc/filesystems 出现 Oops。(刚刚向 Linus 发送了一个补丁,提到虽然这在今天不是一个真正的 bug,因为 pipefs 不能编译为模块,但应该考虑到将来它可能会模块化来编写)。

register_filesystem() 的结果是 pipe_fs_type 被链接到 file_systems 列表中,因此可以读取 /proc/filesystems 并在其中找到 "pipefs" 条目,其中 "nodev" 标志指示未设置 FS_REQUIRES_DEV/proc/filesystems 文件实际上应该得到增强以支持所有新的 FS_ 标志(我为此制作了一个补丁),但这无法完成,因为它会破坏所有使用它的用户应用程序。尽管 Linux 内核接口每分钟都在变化(仅为了更好),但在用户空间兼容性方面,Linux 是一个非常保守的操作系统,它允许许多应用程序在很长一段时间内使用而无需重新编译。

kern_mount() 的结果是

  1. 通过设置 unnamed_dev_in_use 位图中的一个位来分配一个新的未命名(匿名)设备号;如果不再有位,则 kern_mount() 失败并返回 EMFILE
  2. 一个新的超级块结构通过 get_empty_super() 的方式分配。 get_empty_super() 函数遍历以 super_block 为首的超级块列表,并查找空条目,即 s->s_dev == 0。如果未找到此类空超级块,则使用 kmalloc()GFP_USER 优先级分配一个新的超级块。在 get_empty_super() 中检查了系统范围内的最大超级块数量,因此如果它开始失败,可以调整可调参数 /proc/sys/fs/super-max
  3. 文件系统特定的 pipe_fs_type->read_super() 方法,即 pipefs_read_super(),被调用,它分配根 inode 和根 dentry sb->s_root,并将 sb->s_op 设置为 &pipefs_ops
  4. 然后 kern_mount() 调用 add_vfsmnt(NULL, sb->s_root, "none"),它分配一个新的 vfsmount 结构,并将其链接到 vfsmntlistsb->s_mounts
  5. pipe_fs_type->kern_mnt 被设置为这个新的 vfsmount 结构,并将其返回。 kern_mount() 的返回值是 vfsmount 结构的原因是,即使是 FS_SINGLE 文件系统也可以多次挂载,因此它们的 mnt->mnt_sb 将指向同一个东西,这从多次调用 kern_mount() 返回会很愚蠢。

现在文件系统已注册并在内核中挂载,我们可以使用它了。 pipefs 文件系统的入口点是 pipe(2) 系统调用,在架构相关的函数 sys_pipe() 中实现,但真正的工作是由可移植的 fs/pipe.c:do_pipe() 函数完成的。让我们看看 do_pipe()。与 pipefs 的交互发生在 do_pipe() 调用 get_pipe_inode() 以分配一个新的 pipefs inode 时。对于此 inode,inode->i_sb 设置为 pipefs 的超级块 pipe_mnt->mnt_sb,文件操作 i_fop 设置为 rdwr_pipe_fops,读取器和写入器的数量(保存在 inode->i_pipe 中)设置为 1。存在单独的 inode 字段 i_pipe 而不是将其保存在 fs-private 联合中的原因是,管道和 FIFO 共享相同的代码,并且 FIFO 可以存在于其他文件系统上,这些文件系统使用同一联合中的其他访问路径,这是非常糟糕的 C 代码,只能靠纯粹的运气工作。因此,是的,2.2.x 内核只能靠纯粹的运气工作,并且一旦你稍微重新排列 inode 中的字段,它就会停止工作。

每个 pipe(2) 系统调用都会递增 pipe_mnt 挂载实例上的引用计数。

在 Linux 下,管道不是对称的(双向或 STREAM 管道),即文件的两侧具有不同的 file->f_op 操作 - 分别是 read_pipe_fopswrite_pipe_fops。对读取侧的写入返回 EBADF,对写入侧的读取也是如此。

3.7 磁盘文件系统示例:BFS

作为磁盘上的 Linux 文件系统的简单示例,让我们考虑 BFS。 BFS 模块的序言在 fs/bfs/inode.c


static DECLARE_FSTYPE_DEV(bfs_fs_type, "bfs", bfs_read_super);

static int __init init_bfs_fs(void)
{
        return register_filesystem(&bfs_fs_type);
}

static void __exit exit_bfs_fs(void)
{
        unregister_filesystem(&bfs_fs_type);
}

module_init(init_bfs_fs)
module_exit(exit_bfs_fs)

使用了特殊的 fstype 声明宏 DECLARE_FSTYPE_DEV(),它将 fs_type->flags 设置为 FS_REQUIRES_DEV,以表明 BFS 需要一个真正的块设备才能挂载。

模块的初始化函数向 VFS 注册文件系统,而清理函数(仅当 BFS 配置为模块时才存在)取消注册它。

文件系统注册后,我们可以继续挂载它,这将调用我们的 fs_type->read_super() 方法,该方法在 fs/bfs/inode.c:bfs_read_super() 中实现。它执行以下操作

  1. set_blocksize(s->s_dev, BFS_BSIZE):由于我们即将通过缓冲区缓存与块设备层交互,我们必须初始化一些东西,即设置块大小,并通过字段 s->s_blocksizes->s_blocksize_bits 通知 VFS。
  2. bh = bread(dev, 0, BFS_BSIZE):我们读取通过 s->s_dev 传递的设备块 0。此块是文件系统的超级块。
  3. 超级块针对 BFS_MAGIC 数字进行验证,如果有效,则存储在 sb-private 字段 s->su_sbh 中(实际上是 s->u.bfs_sb.si_sbh)。
  4. 然后我们使用 kmalloc(GFP_KERNEL) 分配 inode 位图,并将所有位清除为 0,除了前两位,我们将其设置为 1 以指示我们永远不应分配 inode 0 和 1。Inode 2 是根,并且相应的位无论如何都会在稍后的几行中设置为 1 - 文件系统在挂载时应具有有效的根 inode!
  5. 然后我们初始化 s->s_op,这意味着我们可以从此时通过 iget() 调用 inode 缓存,这将导致 s_op->read_inode() 被调用。这将找到包含指定 inode (由 inode->i_inoinode->i_dev 指定) 的块并将其读入。如果我们无法获取根 inode,则我们释放 inode 位图并将超级块缓冲区释放回缓冲区缓存并返回 NULL。如果根 inode 读取正常,则我们分配一个名为 / 的 dentry(作为根)并使用此 inode 实例化它。
  6. 现在我们遍历文件系统上的所有 inode,并将它们全部读入,以便在我们内部 inode 位图中设置相应的位,并计算其他一些内部参数,如最后一个 inode 的偏移量以及最后一个文件的起始/结束块。我们读取的每个 inode 都通过 iput() 返回到 inode 缓存 - 我们不会将对其的引用保持超过需要的时间。
  7. 如果文件系统未以只读方式挂载,我们则将超级块缓冲区标记为脏,并设置 s->s_dirt 标志(TODO:我为什么要这样做?最初,我这样做是因为 minix_read_super() 这样做,但 minix 和 BFS 似乎都没有在 read_super() 中修改超级块)。
  8. 一切顺利,因此我们将此初始化的超级块返回给 VFS 级别的调用者,即 fs/super.c:read_super()

read_super() 函数成功返回后,VFS 通过在 fs/super.c:get_sb_bdev() 中调用 get_filesystem(fs_type) 来获取对文件系统模块的引用,以及对块设备的引用。

现在,让我们检查一下当我们在文件系统上执行 I/O 操作时会发生什么。我们已经检查了当调用 iget() 时如何读取 inode 以及如何在 iput() 上释放它们。读取 inode 会设置 inode->i_opinode->i_fop 等内容;打开文件会将 inode->i_fop 传播到 file->f_op 中。

让我们检查 link(2) 系统调用的代码路径。系统调用的实现在 fs/namei.c:sys_link()

  1. 用户空间名称通过 getname() 函数复制到内核空间,该函数执行错误检查。
  2. 这些名称是使用与 dcache 交互的 path_init()/path_walk() 转换的 nameidata。结果存储在 old_ndnd 结构中。
  3. 如果 old_nd.mnt != nd.mnt,则返回 "跨设备链接" EXDEV - 无法在文件系统之间链接,在 Linux 中,这转化为 - 无法在文件系统的挂载实例之间(或特别是在文件系统之间)链接。
  4. 通过 lookup_create() 创建与 nd 对应的新 dentry 。
  5. 调用通用 vfs_link() 函数,该函数检查我们是否可以在目录中创建新条目,并调用 dir->i_op->link() 方法,这将我们带回到文件系统特定的 fs/bfs/dir.c:bfs_link() 函数。
  6. bfs_link() 内部,我们检查是否尝试链接目录,如果是,则拒绝并返回 EPERM 错误。这与标准 (ext2) 的行为相同。
  7. 我们尝试通过调用辅助函数 bfs_add_entry() 将新的目录条目添加到指定的目录,该函数遍历所有条目以查找未使用的槽 (de->ino == 0),并在找到时将名称/inode 对写入相应的块并将其标记为脏(以非超级块优先级)。
  8. 如果我们成功添加了目录条目,则操作无法失败,因此我们递增 inode->i_nlink,更新 inode->i_ctime,并将此 inode 也标记为脏,并将新的 dentry 与 inode 实例化。

其他相关的 inode 操作(如 unlink()/rename() 等)以类似的方式工作,因此详细检查所有这些操作并没有太多收获。

3.8 执行域和二进制格式

Linux 支持从磁盘加载用户应用程序二进制文件。更有趣的是,二进制文件可以以不同的格式存储,并且操作系统对程序的响应可以通过系统调用偏离规范(规范是 Linux 行为),以便模拟在其他 UNIX 版本(COFF 等)中找到的格式,并模拟其他版本(Solaris、UnixWare 等)的系统调用行为。这就是执行域和二进制格式的用途。

每个 Linux 任务都有一个存储在其 task_struct (p->personality) 中的个性。当前存在的个性(在官方内核中或作为附加补丁)包括对 FreeBSD、Solaris、UnixWare、OpenServer 和许多其他流行的操作系统的支持。 current->personality 的值分为两部分

  1. 高三字节 - bug 模拟:STICKY_TIMEOUTSWHOLE_SECONDS 等。
  2. 低字节 - 个性本身,一个唯一的数字。

通过更改个性,我们可以更改操作系统处理某些系统调用的方式,例如,向 current->personality 添加 STICKY_TIMEOUT 会使 select(2) 系统调用保留最后一个参数(超时)的值,而不是存储未休眠的时间。一些有 bug 的程序依赖于有 bug 的操作系统(非 Linux),因此 Linux 提供了一种在源代码不可用且 bug 无法修复的情况下模拟 bug 的方法。

执行域是由单个模块实现的连续个性范围。通常,单个执行域实现单个个性,但有时可以在单个模块中实现“接近”的个性,而无需过多的条件。

执行域在 kernel/exec_domain.c 中实现,并且与 2.2.x 相比,在 2.4 内核中完全重写。内核当前支持的执行域列表,以及它们支持的个性范围,可以通过读取 /proc/execdomains 文件获得。执行域,除了 PER_LINUX 域之外,都可以作为动态可加载模块实现。

用户界面是通过 personality(2) 系统调用,如果参数设置为不可能的个性 0xffffffff,则该系统调用设置当前进程的个性或返回 current->personality 的值。显然,此系统调用本身的行为不取决于个性。

执行域注册的内核接口由两个函数组成

exec_domains_lock 为读写锁的原因是,只有注册和取消注册请求会修改列表,而执行 cat /proc/filesystems 会调用 fs/exec_domain.c:get_exec_domain_list(),后者只需要对列表的读取访问权限。注册新的执行域定义了一个“lcall7 处理程序”和一个信号编号转换映射。实际上,ABI 补丁扩展了执行域的概念,以包括额外的信息(如套接字选项、套接字类型、地址族和 errno 映射)。

二进制格式以类似的方式实现,即,在 fs/exec.c 中定义了一个单链表格式,并由读写锁 binfmt_lock 保护。与 exec_domains_lock 一样,除了二进制格式的注册/取消注册之外,binfmt_lock 在大多数情况下都以读取模式获取。注册新的二进制格式通过新的 load_binary()/load_shlib() 函数以及 core_dump() 的能力来增强 execve(2) 系统调用。 load_shlib() 方法仅由旧的 uselib(2) 系统调用使用,而 load_binary() 方法由 do_execve() 中的 search_binary_handler() 调用,后者实现 execve(2) 系统调用。

进程的个性在二进制格式加载时由相应格式的 load_binary() 方法使用一些启发式方法确定。例如,为了确定 UnixWare7 二进制文件,首先使用 elfmark(1) 实用程序标记二进制文件,该实用程序将 ELF 标头的 e_flags 设置为魔术值 0x314B4455,该值在 ELF 加载时被检测到,并将 current->personality 设置为 PER_UW7。如果此启发式方法失败,则使用更通用的方法,例如将 ELF 解释器路径(如 /usr/lib/ld.so.1/usr/lib/libc.so.1)视为指示 SVR4 二进制文件,并将个性设置为 PER_SVR4。可以编写一个小型的实用程序程序,该程序使用 Linux 的 ptrace(2) 功能来单步执行代码,并将正在运行的程序强制转换为任何个性。

一旦个性(以及因此 current->exec_domain)已知,系统调用将按如下方式处理。让我们假设一个进程通过 lcall7 门指令发出系统调用。这会将控制权转移到 arch/i386/kernel/entry.SENTRY(lcall7),因为它是在 arch/i386/kernel/traps.c:trap_init() 中准备的。在适当的堆栈布局转换之后,entry.S:lcall7current 获取指向 exec_domain 的指针,然后在 exec_domain 中 lcall7 处理程序的偏移量(在汇编代码中硬编码为 4,因此您无法在 struct exec_domain 的 C 声明中移动 handler 字段),并跳转到它。因此,在 C 语言中,它看起来像这样


static void UW7_lcall7(int segment, struct pt_regs * regs)
{
       abi_dispatch(regs, &uw7_funcs[regs->eax & 0xff], 1);
}

其中 abi_dispatch() 是函数指针表的包装器,该表实现了此个性的系统调用 uw7_funcs


下一页 上一页 目录