为了支持多种文件系统,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
。