为了支持多种文件系统,Linux 包含一个特殊的内核接口层,称为 VFS(虚拟文件系统交换)。这类似于 SVR4 衍生系统中发现的 vnode/vfs 接口(最初来源于 BSD 和 Sun 的原始实现)。
Linux inode 缓存实现在单个文件 fs/inode.c 中,该文件包含 977 行代码。有趣的是,在过去的 5-7 年里,它没有进行太多更改:将最新版本与例如 1.3.42 版本进行比较,仍然可以识别出其中的一些代码。
Linux inode 缓存的结构如下
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() 创建。inode_in_use),其中包含 i_count>0 和 i_nlink>0 的有效 inode。由 get_empty_inode() 和 get_new_inode() 新分配的 inode 将被添加到 inode_in_use 列表中。inode_unused),其中包含 i_count = 0 的有效 inode。sb->s_dirty),其中包含 i_count>0、i_nlink>0 和 i_state & I_DIRTY 的有效 inode。当 inode 被标记为脏时,如果它也被哈希,则会被添加到 sb->s_dirty 列表中。维护每个超级块的 inode 脏列表可以快速同步 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.h 中 list_entry() 宏的定义。
为了理解 inode 缓存的工作原理,让我们跟踪 ext2 文件系统上常规文件的 inode 在打开和关闭时的生命周期
fd = open("file", O_RDONLY);
close(fd);
open(2) 系统调用在 fs/open.c:sys_open 函数中实现,实际工作由 fs/open.c:filp_open() 函数完成,该函数分为两个部分
open_namei():填充包含 dentry 和 vfsmount 结构的 nameidata 结构。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),它执行以下操作
inode_lock 的保护下,在哈希表中查找具有匹配超级块和 inode 编号的 inode。如果找到 inode,则其引用计数 (i_count) 递增;如果递增之前它为 0 并且 inode 不是脏的,则将其从当前所在的任何类型列表 (inode->i_list) 中删除(当然必须是 inode_unused 列表),并插入到 inode_in_use 类型列表中;最后,inodes_stat.nr_unused 递减。iget4() 保证返回一个解锁的 inode。get_new_inode(),并将指向哈希表中应插入位置的指针传递给它。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)
sb->s_op->put_inode() 方法,则立即调用它,不持有任何自旋锁(因此它可以阻塞)。inode_lock 自旋锁,并且 i_count 递减。如果这不是对 inode 的最后一次引用,那么我们只需检查是否对它有太多引用,以至于 i_count 可以环绕分配给它的 32 位,如果是,我们打印警告并返回。请注意,我们在持有 inode_lock 自旋锁时调用 printk() - 这很好,因为 printk() 永远不会阻塞,因此可以在任何上下文中调用它(甚至从中断处理程序中!)。iput() 在最后一个 inode 引用上执行的工作相当复杂,因此我们将其分成一个单独的列表
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) 递减。i_nlink != 0,那么我们检查在同一哈希桶中是否还有其他 inode,如果没有,则如果 inode 不是脏的,我们将其从其类型列表中删除并添加到 inode_unused 列表,递增 inodes_stat.nr_unused。如果同一哈希桶中存在 inode,那么我们将其从类型列表中删除并添加到 inode_unused 列表。如果这是一个匿名 inode (NetApp .snapshot),那么我们将其从类型列表中删除并完全清除/销毁它。
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_type 在 include/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;
};
其中的字段解释如下
/proc/filesystems 文件中,并用作按名称查找文件系统的键;此名称也用于 mount(2) 中的文件系统类型,并且它应该是唯一的:给定名称的文件系统(显然)只能有一个。对于模块,名称指向模块的地址空间,而不是复制:这意味着如果模块已卸载但文件系统仍已注册,cat /proc/filesystems 可能会出错。FS_REQUIRES_DEV;对于只能有一个超级块的文件系统,FS_SINGLE;对于无法通过 mount(2) 系统调用从用户空间挂载的文件系统,FS_NOMOUNT:但是可以使用 kern_mount() 接口在内部挂载它们,例如 pipefs。FS_SINGLE 情况下,它将在 get_sb_single() 中出错,尝试在 fs_type->kern_mnt->mnt_sb 中解引用 NULL 指针 (fs_type->kern_mnt = NULL)。THIS_MODULE 会自动执行正确的操作。FS_SINGLE 文件系统。这由 kern_mount() 设置(TODO:如果未设置 FS_SINGLE,kern_mount() 应该拒绝挂载文件系统)。file_systems 引导的单链表(参见 fs/super.c)。该列表受 file_systems_lock 读写自旋锁保护,函数 register/unregister_filesystem() 通过链接和取消链接列表中的条目来修改它。read_super() 函数的工作是填充超级块的字段,分配根 inode 并初始化与此文件系统挂载实例关联的任何文件系统私有信息。因此,通常 read_super() 会执行
bread() 函数,从通过 sb->s_dev 参数指定的设备读取超级块。如果它预计立即读取更多后续元数据块,那么使用 breada() 来安排异步读取额外的块是有意义的。sb->s_op 以指向 struct super_block_operations 结构。此结构包含文件系统特定的函数,用于实现诸如“读取 inode”、“删除 inode”等操作。d_alloc_root() 分配根 inode 和根 dentry。sb->s_dirt 设置为 1,并将包含超级块的缓冲区标记为脏(TODO:我们为什么要这样做?我在 BFS 中这样做是因为 MINIX 这样做...)。
在 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_FILES 的 clone() 系统调用创建的,则父进程和子进程之间可以共享 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,作为下次此进程想要打开文件时查找第一个未使用描述符的提示。
文件结构在 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 的各种字段
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 自旋锁保护。open_namei()(或更确切地说是它调用的 path_walk())创建的,但实际的 file->f_dentry 字段由 dentry_open() 设置为包含这样找到的 dentry。vfsmount 结构的指针。这由 dentry_open() 设置,但在 nameidata 查找时由 open_namei()(或更确切地说是它调用的 path_init())找到。file_operations 的指针,其中包含可以在文件上调用的各种方法。这是从 inode->i_fop 复制的,inode->i_fop 是在 nameidata 查找期间由文件系统特定的 s_op->read_inode() 方法放置在那里的。我们将在本节稍后详细查看 file_operations 方法。get_file/put_filp/fput 操作的引用计数。O_XXX 标志(由 filp_open() 稍作修改),由 dentry_open() 设置,并在清除 O_CREAT、O_EXCL、O_NOCTTY、O_TRUNC 之后 - 永久存储这些标志没有意义,因为它们不能通过 F_SETFL(或通过 F_GETFL 查询)fcntl(2) 调用来修改。dentry_open() 设置。转换的目的是将读取和写入访问权限存储在单独的位中,以便可以轻松进行诸如 (f_mode & FMODE_WRITE) 和 (f_mode & FMODE_READ) 之类的检查。long long,即 64 位值。SIGIO 机制接收异步 I/O 通知(参见 fs/fcntl.c:kill_fasync())。get_empty_filp() 中创建文件结构时,设置为打开文件的进程的用户 ID 和组 ID。如果文件是套接字,则由 ipv4 netfilter 使用。fs/nfs/file.c 中设置,并在 mm/filemap.c:generic_file_write() 中检查。f_pos 更改时递增(使用全局 event)。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 *);
};
THIS_MODULE,文件系统可以很高兴地忽略它,因为它们的模块计数在挂载/卸载时控制,而驱动程序需要在打开/释放时控制它。fs/read_write.c:default_llseek(),它会做正确的事情(TODO:强制所有当前将其设置为 NULL 的人使用 default_llseek - 这样我们在 llseek() 中节省了一个 if())。read(2) 系统调用。文件系统可以将 mm/filemap.c:generic_file_read() 用于常规文件,并将 fs/read_write.c:generic_read_dir()(它只是返回 -EISDIR)用于此处的目录。mm/filemap.c:generic_file_write() 用于常规文件,并在此处忽略目录。FIBMAP、FIGETBSZ、FIONREAD 之类的通用文件 ioctl 由更高级别实现,因此它们永远不会读取 f_op->ioctl() 方法。dentry_open() 调用。文件系统很少使用它,例如,coda 尝试在打开时在本地缓存文件。release() 方法)。唯一使用它的是 NFS 客户端,用于刷新所有脏页。请注意,这可能会返回错误,该错误将传递回发出 close(2) 系统调用的用户空间。file->f_count 达到 0 时。虽然定义为返回 int,但 VFS 忽略返回值(参见 fs/file_table.c:__fput())。file = fget(fd)) 和 down/up inode->i_sem 信号量。Ext2 文件系统当前忽略最后一个参数,并且对于 fsync(2) 和 fdatasync(2) 执行完全相同的操作。file->f_flags & FASYNC 更改时调用此方法。posix_lock_file()) 之前被调用,如果它成功但标准 POSIX 锁定代码失败,那么它将永远不会在文件系统相关级别上解锁。
在 Linux 下,有关已挂载文件系统的信息保存在两个单独的结构中 - super_block 和 vfsmount。这样做的原因是 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 结构中的各种字段是
FS_REQUIRES_DEV 文件系统,这是块设备的 i_dev。对于其他文件系统(称为匿名文件系统),这是一个整数 MKDEV(UNNAMED_MAJOR, i),其中 i 是 unnamed_dev_in_use 数组中第一个未设置的位,介于 1 到 255 之间(含)。请参阅 fs/super.c:get_unnamed_dev()/put_unnamed_dev()。已经多次建议匿名文件系统不应使用 s_dev 字段。lock_super()/unlock_super() 锁定。struct file_system_type 的指针。文件系统的 read_super() 方法不需要设置它,因为 VFS fs/super.c:read_super() 会在文件系统特定的 read_super() 成功时为您设置它,并在失败时重置为 NULL。super_operations 结构的指针,其中包含文件系统特定的方法,用于读取/写入 inode 等。正确初始化 s_op 是文件系统的 read_super() 方法的工作。d_alloc_root() 以分配 dentry 并实例化它是 read_super() 的工作。某些文件系统将“root”拼写为 “/” 以外的其他内容,因此使用更通用的 d_alloc() 函数将 dentry 绑定到名称,例如,pipefs 将自身挂载在 “pipe:” 上作为其自己的根目录而不是 “/”。inode->i_state & I_DIRTY),那么它就在特定于超级块的脏列表中,通过 inode->i_list 链接。fs/file_table.c:fs_may_remount_ro(),它遍历 sb->s_files 列表,如果存在为写入打开的文件 (file->f_mode & FMODE_WRITE) 或具有待处理取消链接的文件 (inode->i_nlink == 0),则拒绝重新挂载。FS_REQUIRES_DEV,这指向描述文件系统挂载在其上的设备的 block_device 结构。vfsmount 结构的列表,每个挂载的此超级块实例一个。超级块操作在 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 *);
};
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_op 和 inode->i_fop,以便 VFS 级别知道可以在 inode 或相应文件上执行哪些操作。不实现 read_inode() 的文件系统是 ramfs 和 pipefs。例如,ramfs 有自己的 inode 生成函数 ramfs_get_inode(),所有 inode 操作都根据需要调用它。read_inode(),因为它需要找到磁盘上的相关块,并通过调用 mark_buffer_dirty(bh) 与缓冲区缓存交互。当需要单独同步 inode 或作为同步整个文件系统的一部分时,会在脏 inode(那些标记为脏的 inode,使用 mark_inode_dirty())上调用此方法。inode->i_count 和 inode->i_nlink 都达到 0 时调用。文件系统删除 inode 在磁盘上的副本,并在 VFS inode 上调用 clear_inode() 以“极其彻底地终止它”。brelse() 包含超级块的块,并 kfree() 为空闲块、inode 等分配的任何位图。sb-private 区域),并 mark_buffer_dirty(bh) 。它还应该清除 sb->s_dirt 标志。struct statfs 的指针是一个内核指针,而不是用户指针,因此我们不需要对用户空间进行任何 I/O 操作。如果未实现,则 statfs(2) 将失败并返回 ENOSYS。clear_inode() 调用。将私有数据附加到 inode 结构(通过 generic_ip 字段)的文件系统必须在此处释放它。那么,让我们看看当我们挂载一个磁盘上的 (FS_REQUIRES_DEV) 文件系统时会发生什么。 mount(2) 系统调用的实现在 fs/super.c:sys_mount() 中,这只是一个包装器,用于为 do_mount() 函数复制选项、文件系统类型和设备名称,而 do_mount() 函数才是真正完成工作的函数。
do_mount() 调用 get_fs_type(),另一次是 get_sb_dev() 调用 get_filesystem()(如果 read_super() 成功)。第一次递增是为了防止在我们位于 read_super() 方法内部时模块被卸载,第二次递增是为了指示模块正在被此挂载实例使用。显然,do_mount() 在返回之前会递减计数,因此总体而言,每次挂载后计数仅增加 1。fs_type->fs_flags & FS_REQUIRES_DEV 为真,因此超级块通过调用 get_sb_bdev() 进行初始化,该调用获取对块设备的引用,并与文件系统的 read_super() 方法交互以填充超级块。如果一切顺利,则 super_block 结构被初始化,并且我们对文件系统模块有一个额外的引用,以及对底层块设备的引用。vfsmount 结构被分配并链接到 sb->s_mounts 列表和全局 vfsmntlist 列表。 vfsmount 字段 mnt_instances 允许查找与此实例挂载在同一超级块上的所有实例。 mnt_list 字段允许查找系统范围内所有超级块的所有实例。 mnt_sb 字段指向此超级块,而 mnt_root 对 sb->s_root dentry 有一个新的引用。
作为一个不需要块设备即可挂载的 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() 的结果是
unnamed_dev_in_use 位图中的一个位来分配一个新的未命名(匿名)设备号;如果不再有位,则 kern_mount() 失败并返回 EMFILE。get_empty_super() 的方式分配。 get_empty_super() 函数遍历以 super_block 为首的超级块列表,并查找空条目,即 s->s_dev == 0。如果未找到此类空超级块,则使用 kmalloc() 以 GFP_USER 优先级分配一个新的超级块。在 get_empty_super() 中检查了系统范围内的最大超级块数量,因此如果它开始失败,可以调整可调参数 /proc/sys/fs/super-max。pipe_fs_type->read_super() 方法,即 pipefs_read_super(),被调用,它分配根 inode 和根 dentry sb->s_root,并将 sb->s_op 设置为 &pipefs_ops。kern_mount() 调用 add_vfsmnt(NULL, sb->s_root, "none"),它分配一个新的 vfsmount 结构,并将其链接到 vfsmntlist 和 sb->s_mounts。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_fops 和 write_pipe_fops。对读取侧的写入返回 EBADF,对写入侧的读取也是如此。
作为磁盘上的 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() 中实现。它执行以下操作
set_blocksize(s->s_dev, BFS_BSIZE):由于我们即将通过缓冲区缓存与块设备层交互,我们必须初始化一些东西,即设置块大小,并通过字段 s->s_blocksize 和 s->s_blocksize_bits 通知 VFS。bh = bread(dev, 0, BFS_BSIZE):我们读取通过 s->s_dev 传递的设备块 0。此块是文件系统的超级块。BFS_MAGIC 数字进行验证,如果有效,则存储在 sb-private 字段 s->su_sbh 中(实际上是 s->u.bfs_sb.si_sbh)。kmalloc(GFP_KERNEL) 分配 inode 位图,并将所有位清除为 0,除了前两位,我们将其设置为 1 以指示我们永远不应分配 inode 0 和 1。Inode 2 是根,并且相应的位无论如何都会在稍后的几行中设置为 1 - 文件系统在挂载时应具有有效的根 inode!s->s_op,这意味着我们可以从此时通过 iget() 调用 inode 缓存,这将导致 s_op->read_inode() 被调用。这将找到包含指定 inode (由 inode->i_ino 和 inode->i_dev 指定) 的块并将其读入。如果我们无法获取根 inode,则我们释放 inode 位图并将超级块缓冲区释放回缓冲区缓存并返回 NULL。如果根 inode 读取正常,则我们分配一个名为 / 的 dentry(作为根)并使用此 inode 实例化它。iput() 返回到 inode 缓存 - 我们不会将对其的引用保持超过需要的时间。s->s_dirt 标志(TODO:我为什么要这样做?最初,我这样做是因为 minix_read_super() 这样做,但 minix 和 BFS 似乎都没有在 read_super() 中修改超级块)。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_op 和 inode->i_fop 等内容;打开文件会将 inode->i_fop 传播到 file->f_op 中。
让我们检查 link(2) 系统调用的代码路径。系统调用的实现在 fs/namei.c:sys_link() 中
getname() 函数复制到内核空间,该函数执行错误检查。path_init()/path_walk() 转换的 nameidata。结果存储在 old_nd 和 nd 结构中。old_nd.mnt != nd.mnt,则返回 "跨设备链接" EXDEV - 无法在文件系统之间链接,在 Linux 中,这转化为 - 无法在文件系统的挂载实例之间(或特别是在文件系统之间)链接。lookup_create() 创建与 nd 对应的新 dentry 。vfs_link() 函数,该函数检查我们是否可以在目录中创建新条目,并调用 dir->i_op->link() 方法,这将我们带回到文件系统特定的 fs/bfs/dir.c:bfs_link() 函数。bfs_link() 内部,我们检查是否尝试链接目录,如果是,则拒绝并返回 EPERM 错误。这与标准 (ext2) 的行为相同。bfs_add_entry() 将新的目录条目添加到指定的目录,该函数遍历所有条目以查找未使用的槽 (de->ino == 0),并在找到时将名称/inode 对写入相应的块并将其标记为脏(以非超级块优先级)。inode->i_nlink,更新 inode->i_ctime,并将此 inode 也标记为脏,并将新的 dentry 与 inode 实例化。其他相关的 inode 操作(如 unlink()/rename() 等)以类似的方式工作,因此详细检查所有这些操作并没有太多收获。
Linux 支持从磁盘加载用户应用程序二进制文件。更有趣的是,二进制文件可以以不同的格式存储,并且操作系统对程序的响应可以通过系统调用偏离规范(规范是 Linux 行为),以便模拟在其他 UNIX 版本(COFF 等)中找到的格式,并模拟其他版本(Solaris、UnixWare 等)的系统调用行为。这就是执行域和二进制格式的用途。
每个 Linux 任务都有一个存储在其 task_struct (p->personality) 中的个性。当前存在的个性(在官方内核中或作为附加补丁)包括对 FreeBSD、Solaris、UnixWare、OpenServer 和许多其他流行的操作系统的支持。 current->personality 的值分为两部分
STICKY_TIMEOUTS、WHOLE_SECONDS 等。通过更改个性,我们可以更改操作系统处理某些系统调用的方式,例如,向 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 的值。显然,此系统调用本身的行为不取决于个性。
执行域注册的内核接口由两个函数组成
int register_exec_domain(struct exec_domain *):通过在读写自旋锁 exec_domains_lock 的写保护下将其链接到单链表 exec_domains 中来注册执行域。成功时返回 0,失败时返回非零值。int unregister_exec_domain(struct exec_domain *):通过从 exec_domains 列表中取消链接来取消注册执行域,同样使用写模式下的 exec_domains_lock 自旋锁。成功时返回 0。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.S 的 ENTRY(lcall7),因为它是在 arch/i386/kernel/traps.c:trap_init() 中准备的。在适当的堆栈布局转换之后,entry.S:lcall7 从 current 获取指向 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。