核心VFS¶
概述¶
虚拟文件系统是内核中的软件层,它向上层为用户空间程序提供文件系统接口,它向下层还在内核中提供了一个抽象,允许不同的文件系统实现共存。
VFS系统调用open(2)、stat(2)、read(2)、write(2)、chmod(2)等等都是由用户层进程发起调用的。
目录缓存(dcache)¶
VFS实现了open(2)、stat(2)、chmod(2)和类似的系统调用,从这些系统调用传到VFS的参数pathname于搜索目录缓存(也称为dentry缓存或dcache)。
dcache 实现了一种快速的查找机制,可以将路径名(filename)转换为特定的dentry。
dcache 只存储在RAM中,并不不保存到磁盘:它们的存在就是为了提高性能。
dcache 描绘了整个文件系统,但是大多数计算机无法同时在RAM中保存所有的dcache条目,因此在解析路径名时候当缺少dentry会创建。
inode 对象¶
单个dentry通常有一个指向inode的指针。inode是文件系统对象,比如普通文件、目录、fifo 和其他东西。它们要么存在于磁盘(对于块设备文件系统),要么存在于内存(对于伪文件系统)。当需要时,将磁盘上的inode复制到内存中,并将对inode的更改写回磁盘。一个inode可以被多个dentry指向(例如,硬链接就是这样做的)。
要查找一个inode, VFS需要调用父目录inode的lookup()方法。该方法由inode所在的特定文件系统实现。一旦VFS拥有了所需的dentry(以及inode),我们就可以执行所有操作,比如open(2)打开文件或stat(2)文件以查看inode数据。stat(2)操作相当简单:一旦VFS有了dentry,它就会查看inode数据,并将其中一些数据传递回用户空间。
File 对象¶
打开文件需要另一个操作:分配文件结构(这是文件描述符的内核端实现)。新分配的文件结构使用指向 dentry 的指针和一组文件操作成员函数进行初始化,这些都是取自 inode 数据。然后调用 open() 文件方法,以便特定的文件系统实现可以完成其工作。其中文件结构被放置在进程的文件描述符表中。
读取、写入和关闭文件(以及其他各种 VFS 操作)是通过使用用户空间文件描述符获取适当的文件结构,然后调用所需的文件结构方法来执行所需的任何操作来完成的。只要文件处于打开状态,它就会使 dentry 保持使用状态,这反过来意味着 VFS inode 仍在使用中。
文件系统的注册和挂载¶
注册和注销文件系统,使用以下API函数:
#include <linux/fs.h>
extern int register_filesystem(struct file_system_type *);
extern int unregister_filesystem(struct file_system_type *);
传递的struct file_system_type描述了文件系统。当需要将文件系统挂载到名称空间中的目录时,VFS将为特定的文件系统调用适当的mount()方法。新vfmount将把 ->mount() 返回的树形结构附加到挂载点,这样当路径名解析到达挂载点时,它将跳转到该vfmount的根目录。
您可以在文件/proc/filesystems中看到注册到内核的所有文件系统。
struct file_system_type¶
这个结构体描述了文件系统。从内核 2.6.39 开始,定义了以下成员:
struct file_system_type
{
const char *name;
int fs_flags;
struct dentry *(*mount) (struct file_system_type*, int, const char *, void *);
void (*kill_sb) (struct super_block *);
struct module *owner;
struct file_system_type * next;
struct list_head fs_supers;
struct lock_class_key s_lock_key;
struct lock_class_key s_umount_key;
};
name:文件系统类型的名称,例如“ext2”、“iso9660”、“msdos”等。fs_flags:各种标志(例如:FS_REQUIRES_DEV,FS_NO_DCACHE等)。mount:当应该安装该文件系统的新实例时调用的方法。mount()方法必须返回调用者请求的树的根dentry。对其超级块的激活引用必须被抓取,并且超级块必须被锁定。失败时,它应该返回ERR_PTR(错误)。参数1(
struct file_system_type *):描述由特定文件系统代码部分初始化的文件系统参数2(
int):挂载flag;参数3(
const char *): 要挂载的设备名;参数4(
void*):任意的挂载选项,通常是ASCII字符串(参见“Mount Options”一节)
kill_sb:当该文件系统的实例应该关闭时要调用的方法owner:对于内部VFS使用:你应该在大多数情况下初始化THIS_MODULE。next:对于内部VFS使用:你应该将其初始化为NULLs_lock_key,s_umount_key:
superblock 对象¶
超级块对象表示一个挂载的文件系统。
struct super_operations¶
这描述了VFS如何操作文件系统的超级块。在2.6.22内核中,定义了以下成员:
struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb);
void (*destroy_inode)(struct inode *);
void (*dirty_inode) (struct inode *, int flags);
int (*write_inode) (struct inode *, int);
void (*drop_inode) (struct inode *);
void (*delete_inode) (struct inode *);
void (*put_super) (struct super_block *);
int (*sync_fs)(struct super_block *sb, int wait);
int (*freeze_fs) (struct super_block *);
int (*unfreeze_fs) (struct super_block *);
int (*statfs) (struct dentry *, struct kstatfs *);
int (*remount_fs) (struct super_block *, int *, char *);
void (*clear_inode) (struct inode *);
void (*umount_begin) (struct super_block *);
int (*show_options)(struct seq_file *, struct dentry *);
ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
int (*nr_cached_objects)(struct super_block *);
void (*free_cached_objects)(struct super_block *, int);
};
除非另有说明,否则所有方法都是在不持有任何锁的情况下调用的。这意味着大多数方法都可以安全地阻塞。所有方法只能从进程上下文中调用(即不能从中断处理程序或下半部调用)。
alloc_inode:该方法由alloc_inode()调用,为结构inode分配内存并对其进行初始化。如果未定义此函数,则分配一个简单的struct inode。通常,alloc_inode会被用来分配一个更大的结构,其中包含一个struct inode嵌入其中。destroy_inode:destroy_inode()调用这个方法来释放分配给结构inode的资源。只有在定义了->alloc_inode时才需要它,它只是简单地撤销->alloc_inode所做的任何事情。dirty_inode:当一个inode被标记为dirty时,VFS会调用这个方法。这是专门针对被标记为dirty的inode本身,而不是它的数据。如果fdatasync()需要持久化更新,则I_DIRTY_DATASYNC将在flags参数中设置。write_inode:当VFS需要将一个inode写入磁盘时,调用此方法。第二个参数表示写是否应该是同步的,并不是所有的文件系统都检查这个标志。drop_inode:在删除对inode的最后一次访问时调用,并保持inode->i_lock自旋锁。此方法应该是NULL(普通UNIX文件系统语义)或generic_delete_inode(对于不希望缓存inode的文件系统-导致无论i_nlink的值如何,总是调用delete_inode)generic_delete_inode()行为相当于在put_inode()情况下使用force_delete的旧实践,但不具有force_delete()方法所具有的竞争。delete_inode:当VFS想要删除一个inode时调用put_super:当VFS希望释放超级块(即卸载)时调用。在超级块锁被持有时调用sync_fs:当VFS写入与超级块关联的所有脏数据时调用。第二个参数指示方法是否应该等待直到写操作完成。可选的。freeze_fs:当VFS锁定文件系统并强制其进入一致状态时调用。该方法目前由逻辑卷管理器(LVM)使用。unfreeze_fs:当VFS解锁文件系统并使其重新可写时调用。statfs:当VFS需要获取文件系统统计信息时调用。remount_fs:当重新装载文件系统时调用。这是在内核锁被持有时调用的clear_inode:调用,则VFS清除该inode。可选umount_begin:当VFS卸载文件系统时调用。show_options:由VFS调用,以显示/proc/mounts。quota_read:调用VFS来读取文件系统配额文件。quota_write:调用VFS来写入文件系统的配额文件。nr_cached_objects:由sb缓存收缩函数为文件系统调用,以返回它所包含的可释放缓存对象的数量。可选的。free_cache_objects:由sb缓存收缩函数为文件系统调用,以扫描指定的尝试释放对象的数量。可选,但任何实现此方法的文件系统还需要实现->nr_cached_objects才能正确调用它。我们无法处理文件系统可能遇到的任何错误,因此返回类型为void。如果VM尝试在GFP_NOFS条件下回收,则永远不会调用该方法,该方法本身不需要处理这种情况。实现必须包括已完成的任何扫描循环内的条件重调度调用。这使得VFS可以确定适当的扫描批处理大小,而不必担心实现是否会由于大的扫描批处理大小而导致延迟问题。设置inode的人负责填充i_op字段。这是一个指向struct inode_operations的指针,该指针描述了可以在单个inode上执行的方法。
struct xattr_handlers¶
在支持扩展属性(xattrs)的文件系统上,s_xattr超级块字段指向一个以null结尾的xattr处理程序数组。扩展属性是 key:value 对。
name:指示处理程序匹配具有指定名称的属性(例如system.posix_acl_access);prefix字段必须为NULL。prefix:指示处理程序匹配所有具有指定名称前缀的属性(如“user.”);名称字段必须为NULL。list:确定是否应该为特定dentry列出与这个xattr处理程序匹配的属性。用于一些listxattr实现,如generic_listxattr。get:由VFS调用以获取特定扩展属性的值。该方法由getxattr(2)系统调用调用。set:由VFS调用,以设置特定扩展属性的值。当新值为NULL时,调用该函数以删除特定的扩展属性。该方法由setxattr(2)和removexattr(2)系统调 用调用。
当文件系统的xattr处理程序与指定的属性名不匹配或者文件系统不支持扩展属性时,所有*xattr(2)系统调用将返回-EOPNOTSUPP。
inode 对象¶
inode对象表示文件系统中的一个对象
struct inode_operations¶
这描述了VFS如何操作文件系统中的inode。在2.6.22内核中,定义了以下成员:
struct inode_operations
{
int (*create) (struct user_namespace *, struct inode *,struct dentry *, umode_t, bool);
struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int);
int (*link) (struct dentry *,struct inode *,struct dentry *);
int (*unlink) (struct inode *,struct dentry *);
int (*symlink) (struct user_namespace *, struct inode *,struct dentry *,const char *);
int (*mkdir) (struct user_namespace *, struct inode *,struct dentry *,umode_t);
int (*rmdir) (struct inode *,struct dentry *);
int (*mknod) (struct user_namespace *, struct inode *,struct dentry *,umode_t,dev_t);
int (*rename) (struct user_namespace *, struct inode *, struct dentry *,
struct inode *, struct dentry *, unsigned int);
int (*readlink) (struct dentry *, char __user *,int);
const char *(*get_link) (struct dentry *, struct inode *,
struct delayed_call *);
int (*permission) (struct user_namespace *, struct inode *, int);
struct posix_acl * (*get_acl)(struct inode *, int, bool);
int (*setattr) (struct user_namespace *, struct dentry *, struct iattr *);
int (*getattr) (struct user_namespace *, const struct path *, struct kstat *, u32, unsigned int);
ssize_t (*listxattr) (struct dentry *, char *, size_t);
void (*update_time)(struct inode *, struct timespec *, int);
int (*atomic_open)(struct inode *, struct dentry *, struct file *,
unsigned open_flag, umode_t create_mode);
int (*tmpfile) (struct user_namespace *, struct inode *, struct dentry *, umode_t);
int (*set_acl)(struct user_namespace *, struct inode *, struct posix_acl *, int);
int (*fileattr_set)(struct user_namespace *mnt_userns,
struct dentry *dentry, struct fileattr *fa);
int (*fileattr_get)(struct dentry *dentry, struct fileattr *fa);
};
同样,调用所有方法时不会持有任何锁,除非另有说明。
create由open(2)和create(2)系统调用调用。只有在支持常规文件时才需要。你得到的dentry不应该有一个inode(即,它应该是一个负dentry)。在这里,您可能会使用dentry和新创建的inode调用d_instantiate()lookup当VFS需要在父目录中查找一个inode时调用。要查找的名字在dentry中找到。这个方法必须调用d_add()来将找到的inode插入dentry。inode结构中的“i_count”字段应该递增。如果指定的inode不存在,则应该在dentry中插入一个NULL inode(这称为负dentry)。从这个例 程返回错误代码必须只在真正发生错误时才可以,否则使用create(2)、mknod(2)、mkdir(2)等系统调用创建索引节点将会失败。如果你希望重载dentry方法,那么你应该初始化dentry中的“d_dop”字段;这是一个指向结构体“dentry_operations”的指针。在保存目录inode信号 量的情况下调用此方法link由link(2)系统调用调用。只有在支持硬链接时才需要。您可能需要调用d_instantiate(),就像在create()方法中那样unlinksymlink由symlink(2)系统调用调用。只有在希望支持符号链接时才需要。您可能需要调用d_instantiate(),就像在create()方法中那样mkdirrmdirmknod由mknod(2)系统调用来创建一个设备(char, block) inode或一个命名管道(FIFO)或套接字。只有在希望支持创建这些类型的索引节点时才需要。您可能需要调用d_instantiate(),就像在create()方法中那样rename由rename(2)系统调用调用,重命名对象,使其具有由第二个inode和dentry提供的父节点和名称。对于任何不支持或未知的标志,文件系统必须 返回-EINVAL。目前实现了以下标志:(1)
RENAME_NOREPLACE:该标志表示如果重命名目标存在,重命名将失败,使用-EEXIST代替目标。VFS已经检查是否存在,因此对于本地文件系统,RENAME_NOREPLACE实现等价于普通重命名。(2)
RENAME_EXCHANGE:exchange源和目标。必须存在;这由VFS检查。与普通重命名不同,源和目标可能具有不同的类型。
get_link由VFS调用,以跟随到它所指向的inode的符号链接。只有在希望支持符号链接时才需要。这个方法返回要遍历的符号链接体(并可能使用nd_jump_link()重置当前位置)。如果在inode消失之前,主体不会消失,那么其他东西就不需要了;如果需要以其他方式固定它,通过让get_link(…,…,done)执行set_delayed_call(done, destructor, argument)来安排它的释放。在这种情况下,一旦VFS处理完您返回的函数体,就会调用estructor(argument)。可以在RCU模式下调用;由NULL dentry参数表示。如果请求不离开RCU模式就不能被处理,让它返回ERR_PTR(-ECHILD)。如果文件系统将符号链接目标存储在->i_link中,VFS可以直接使用它而不调用->get_link();但是,->get_link()仍然必须提供。—>i_link必须在RCU宽限期后才能释放。readlink这现在只是readlink(2)在->get_link使用nd_jump_link()或object实际上不是符号链接的情况下使用的重载。通常文件系统应该只实现->get_link用于符号链接,readlink(2)将自动使用它。permission由VFS调用,以检查类posix文件系统上的访问权限。可以在rcu-walk模式下调用(mask & MAY_NOT_BLOCK)。如果在rcu-walk模式下,文件 系统必须检查权限,而不能阻塞或存储到inode。如果遇到rcu-walk不能处理的情况,返回-ECHILD,它将在refwalk模式中再次被调用。setattrgetattrlistxattrupdate_timeatomic_open对一个open的最后一个组件调用。使用这个可选的方法,文件系统可以在一个原子操作中查找、创建和打开文件。如果它想把实际打开留给调用 者(例如,如果文件是一个符号链接、设备,或者只是文件系统不会原子打开的东西),它可能通过返回finish_no_open(file, dentry)来发出信 号。仅当最后一个组件为负数或需要查找时,才调用此方法。缓存的正向dentry仍然由f_op->open()处理。如果文件已经创建,FMODE_CREATED标志应该设置在file->f_mode中。在O_EXCL的情况下,只有在文件不存在的情况下,该方法才会成功,因此FMODE_CREATED总是在成功时设置。tmpfile在O_TMPFILEopen()的末尾调用。可选,相当于在给定目录中自动创建、打开和解链接文件。fileattr_get调用ioctl(FS_IOC_GETFLAGS)和ioctl(FS_IOC_FSGETXATTR)来检索杂项文件标志和属性。也可以在相关的SET操作之前调用,以检查发生了什么更改(在本例中,i_rwsem是锁独占的)。如果未设置,则退回到f_op->ioctl()。fileattr_set
地址空间对象(The Address Space Object)¶
地址空间对象用于对页缓存中的页进行分组和管理。它可以用来跟踪文件(或其他文件)中的页面,还可以跟踪文件各部分到进程地址空间的映射。
地址空间可以提供许多不同但相关的服务。这些方法包括通信内存压力、按地址查找页面以及跟踪标记为Dirty或Writeback的页面。
第一个可以单独用于其他的。VM可以尝试写入脏页以清除它们,也可以释放干净页以重用它们。为此,它可以在脏页面上调用->writepage方法,在干净页面上调用->releasepage方法,并设置pagprivate。没有PagePrivate和没有外部引用的干净页面将在不通知address_space的情况下被释放。
为了实现此功能,需要将页面放置在LRU上,并在使用页面时调用lru_cache_add和mark_page_active。
页面通常通过->index保存在基数树索引中。该树维护每个页面的PG_Dirty和PG_Writeback状态信息,以便快速找到带有这两个标志的页面。
Dirty标记主要由mpage_writpages(默认的->writpages方法)使用。它使用标记查找脏页并调用->writepage。如果没有使用mpage_writepages(即地址提供了自己的->writepages),那么PAGECACHE_TAG_DIRTY标签几乎没有被使用。Write_inode_now和sync_inode使用它(通过__sync_single_inode)来检查->writepages是否成功地写出了整个地址空间。
Writeback标记由filemap* wait*和sync_page*函数使用,通过filemap_fdatawait_range函数等待所有的Writeback完成。
address_space处理程序可以附加额外的信息到一个页面,通常使用’struct page’中的’private’字段。如果附加了这些信息,则应该设置PG_Private标志。这将导致各种VM例程对address_space处理程序进行额外调用来处理该数据。
地址空间充当存储和应用程序之间的中介。每次将整个页的数据读入地址空间,并通过复制页或对页进行内存映射的方式提供给应用程序。应用程
序将数据写入地址空间,然后通常以整个页面的形式将数据写回存储,但是address_space可以更好地控制写大小。
read进程实际上只需要’readpage’。写过程更为复杂,使用write_begin/write_end或dirty_folio将数据写入address_space,使用writepage和writepage将数据回写到存储。
在地址空间中添加和删除页面由inode的i_mutex保护。
当数据写入一个页面时,应该设置PG_Dirty标志。它通常保持设置状态,直到writepage要求写入它。这应该清除PG_Dirty并设置PG_Writeb ack。它实际上可以在PG_Dirty被清除后的任何时候被写入。一旦知道它是安全的,就清除PG_Writeback。
Writeback利用writeback_control结构来指导操作。这为writepage和writpages操作提供了一些关于回写请求的性质和原因,以及在哪些
约束条件下执行的信息。它还用于向调用者返回关于写页面或写页面请求结果的信息。
处理回写期间的错误¶
大多数进行缓冲I/O的应用程序将定期调用文件同步调用(fsync、fdatasync、msync或sync_file_range),以确保写入的数据已经到达后备存储。当回写过程中出现错误时,他们希望在文件同步请求时报告错误。在报告了一个请求的错误之后,对同一文件描述符的后续请求应该返回0,除非在上一次文件同步之后发生了进一步的回写错误。
理想情况下,内核只会在文件描述中报告错误,而这些文件描述的写操作后来又没有被写回。但是,通用的页面缓存基础设施不会跟踪已经污染每 个单独页面的文件描述,因此不可能确定哪个文件描述符应该返回错误。
相反,内核中的通用回写错误跟踪基础设施解决了在错误发生时向fsync报告所有打开的文件描述的错误。在有多个写入器的情况下,所有写入器都会在后续的fsync中返回一个错误,即使通过特定文件描述符完成的所有写入都成功了(或者即使对该文件描述符根本没有写入)。
希望使用此基础结构的文件系统应该调用mapping_set_error,以便在发生错误时在address_space中记录错误。然后,在他们的->fsync操作中从页面缓存写回数据后,他们应该调用file_check_and_advance_wb_err来确保struct file的错误游标已经前进到由备份设备发出的错误流
中的正确点。
struct address_space_operations¶
这描述了VFS如何操作文件到文件系统中页面缓存的映射。定义了以下成员:
struct address_space_operations {
int (*writepage)(struct page *page, struct writeback_control *wbc);
int (*readpage)(struct file *, struct page *);
int (*writepages)(struct address_space *, struct writeback_control *);
bool (*dirty_folio)(struct address_space *, struct folio *);
void (*readahead)(struct readahead_control *);
int (*write_begin)(struct file *, struct address_space *mapping,
loff_t pos, unsigned len, unsigned flags, struct page **pagep, void **fsdata);
int (*write_end)(struct file *, struct address_space *mapping,
loff_t pos, unsigned len, unsigned copied, struct page *page, void *fsdata);
sector_t (*bmap)(struct address_space *, sector_t);
void (*invalidate_folio) (struct folio *, size_t start, size_t len);
int (*releasepage) (struct page *, int);
void (*freepage)(struct page *);
ssize_t (*direct_IO)(struct kiocb *, struct iov_iter *iter);
/* isolate a page for migration */
bool (*isolate_page) (struct page *, isolate_mode_t);
/* migrate the contents of a page to the specified target */
int (*migratepage) (struct page *, struct page *);
/* put migration-failed page back to right list */
void (*putback_page) (struct page *);
int (*launder_folio) (struct folio *);
bool (*is_partially_uptodate) (struct folio *, size_t from, size_t count);
void (*is_dirty_writeback) (struct page *, bool *, bool *);
int (*error_remove_page) (struct mapping *mapping, struct page *page);
int (*swap_activate)(struct file *);
int (*swap_deactivate)(struct file *);
};
writepage由VM调用,将脏页写入后备存储。这可能是由于数据完整性原因(即“同步”),或为了释放内存(刷新)。这种差异可以从wbc->sync_mode中看出 。PG_Dirty标志已经清除,pagellocked为true。writepage应该启动writeout,应该设置PG_Writeback,并且应该确保在写操作完成 时,页面是同步或异步解锁的。如果wbc->sync_mode为WB_SYNC_NONE,如果出现问题,->writepage处理起来不会太费劲;如果更容易的话,可以选择从映射中写出其他页面(例如由于内部依赖关系)。如果它选择不启动writeout,它应该返回AOP_WRITEPAGE_ACTIVATE,这样VM就不会 一直调用该页上的->writepage。
有关更多细节,请参阅文件“Locking”。
readpage调用VM从后台存储读取页面。当readpage被调用时,该页面将被锁定,并且应该在读取完成后被解锁并标记为最新。如果->readpage发现由 于某些原因需要解锁页面,它可以这样做,然后返回AOP_TRUNCATED_PAGE。在这种情况下,页面将被重新定位并重新锁定,如果所有操作都成功 ,则将再次调用->readpage。writepages由VM调用,以写出与address_space对象关联的页面。如果wbc->sync_mode为WB_SYNC_ALL,则writeback_control将指定必须写入的页面范围。如果它是WB_SYNC_NONE,则给出一个nr_to_write,并且如果可能的话应该写入许多页。如果没有给出->writpages,则使用mpage_writpages。这将从标记为DIRTY的地址空间中选择页面,并将它们传递给->writepage。dirty_folioVM调用,将对开本标记为dirty。如果地址空间将私有数据附加到一个fifo,并且当fifo被污染时需要更新该数据,则特别需要这样做 。例如,当内存映射页被修改时,就会调用这个函数。如果定义了,它应该在i_pages中设置folio dirty标志和PAGECACHE_TAG_DIRTY搜索标记。readahead由VM调用,以读取与address_space对象关联的页。在页缓存中,页是连续的,并且被锁定。实现应该在启动每个页面的I/O后减少页面引用 计数。通常,该页面将由I/O完成处理程序解锁。页面集被分为一些同步页面和一些异步页面,rac->ra->async_size给出了异步页面的数量。 文件系统应该尝试读取所有同步页面,但可能在到达异步页面时决定停止。如果它决定停止尝试I/O,它可以简单地返回。调用者将从地址空间中删除剩余的页,解锁它们并减小页引用计数。如果I/O成功完成,设置PageUptodate。在任何页面上设置PageError都将被忽略;如果发生I/O错误,只需解锁页面。write_beginwrite_endbmapinvalidate_folioreleasepagefreepagedirect_IOisolate_pagemigrate_pageputback_pagelaunder_foliois_partially_uptodateis_dirty_writebackerror_remove_pageswap_activateswap_deactivate
File 对象¶
文件对象表示进程打开的文件。在POSIX术语中,这也称为“打开文件描述”。
struct file_operations¶
这描述了VFS如何操作打开的文件。在4.18内核中,定义了以下成员:
struct file_operations
{
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long,
unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out, loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
};
同样,调用所有方法时不会持有任何锁,除非另有说明。
llseekreadread_iterwritewrite_iteriopolliterateiterate_sharedpollunlocked_ioctlcompat_ioctlmmapopenflushreleasefsyncfasynclockget_unmapped_areacheck_flagsflocksplice_writesplice_readsetleasefallocatecopy_file_rangeremap_file_rangefadvise
注意,文件操作是由inode所在的特定文件系统实现的。当打开一个设备节点(特殊字符或块)时,大多数文件系统将调用VFS中的特殊支持例程,它 将定位所需的设备驱动程序信息。这些支持例程将文件系统文件操作替换为设备驱动程序的操作,然后继续为文件调用新的open()方法。这就是在 文件系统中打开设备文件最终如何调用设备驱动程序open()方法的。
目录缓存(dcache)¶
struct dentry_operations¶
这描述了文件系统如何重载标准dentry操作。dentry和dcache是VFS和各个文件系统实现的域。设备驱动程序在这里没有什么用。这些方法可以设置为NULL,因为它们要么是可选的,要么VFS使用默认值。在2.6.22内核中,定义了以下成员:
struct dentry_operations {
int (*d_revalidate)(struct dentry *, unsigned int);
int (*d_weak_revalidate)(struct dentry *, unsigned int);
int (*d_hash)(const struct dentry *, struct qstr *);
int (*d_compare)(const struct dentry *, unsigned int, const char *, const struct qstr *);
int (*d_delete)(const struct dentry *);
int (*d_init)(struct dentry *);
void (*d_release)(struct dentry *);
void (*d_iput)(struct dentry *, struct inode *);
char *(*d_dname)(struct dentry *, char *, int);
struct vfsmount *(*d_automount)(struct path *);
int (*d_manage)(const struct path *, bool);
struct dentry *(*d_real)(struct dentry *, const struct inode *);
};
d_revalidate_weak_revalidated_hashd_compared_deleted_initd_released_iputd_dnamed_automountd_managed_real
每个dentry都有一个指向其父dentry的指针,以及子dentry的散列列表。子dentry基本上就像目录中的文件。
目录条目缓存API(Directory Entry Cache API)¶
定义了许多允许文件系统操作dentry的函数:
dget为现有的dentry打开一个新的句柄(这只是增加了使用计数)dput关闭dentry的句柄(减少使用计数)。如果使用计数下降到0,dentry仍然在其父散列中,则调用”d_delete”方法来检查是否应该缓存它。如果它不应该被缓存,或者dentry没有被散列,它就会被删除。否则,缓存的dentry将被放入LRU列表中,在内存不足时回收。d_drop这将从其父哈希列表中解算dentry。如果dentry的使用计数下降到0,则对dput()的后续调用将释放该dentryd_delete删除一个dentry。如果没有对dentry的其他开放引用,则dentry被转换为负dentry(调用d_iput()方法)。如果有其他引用,则调用d_drop()d_add添加一个dentry到它的父哈希列表,然后调用d_instantiate()d_instantiate将dentry添加到inode的别名散列列表中,并更新“d_inode”成员。inode结构中的“i_count”成员应该设置/增加。如果inode指针为NULL,dentry称为“负dentry”。当为现有的负dentry创建一个inode时,通常会调用这个函数d_lookup它从dcache哈希表中查找指定名称的子元素。如果找到,引用计数将增加并返回dentry。调用者在使用完dentry后必须使用dput()来释放它。
挂载操作(Mount Options)¶
解析挂载配置(Parsing options)¶
在挂载和重新挂载时,将向文件系统传递一个字符串,其中包含以逗号分隔的挂载选项列表。选项可以有以下两种形式:option 和 option=value
<linux/parser.h>头文件定义了一个API来帮助解析这些选项。有很多关于如何在现有文件系统中使用它的示例。
显示挂载配置(Showing options)¶
如果文件系统接受挂载选项,它必须定义show_options()来显示所有当前活动的选项。规则是:
选项必须显示哪些不是默认值,或者它们的值与默认值不同
选项可以显示为默认启用或具有默认值
仅在装入帮助程序和内核之间内部使用的选项(如文件描述符),或者仅在装入过程中起作用的选项(如控制日志创建的选项)不受上述规则的约束。
使用上述规则的根本原因是确保可以根据在/proc/mounts.conf. 中找到的信息准确地复制一个挂载(例如,卸载和重新挂载)
5.x 内核没找到
/proc/mounts.conf
总结¶
超级块(Superblock):用于存储文件系统的元数据信息,它在文件系统初始化时候创建,包含文件系统重要信息,如:文件系统类型、块大小、inode表的位置、可用空间、挂载状态等。主要作用总结:1. 文件系统识别和类型;2. 文件系统一致性检查(如:最后一次文件系统检查的时间戳,文件系统是否被标记为“脏”等);3. 文件系统元数据定位(inode表、块位图、数据块等);4. 文件系统挂载和卸载(保存了文件系统是否挂载,以及挂载点);5. 文件系统恢复(通过超级块中的元信息,进行文件系统修复、恢复丢失的数据结构,重建文件系统的元数据信息)。
索引节点(inode):用于存储文件和目录的元数据信息。每个文件和目录在文件系统中都有一个唯一的inode,它包含了有关文件或目录的重要信息。inode主要用途总结:1. 文件和目录的标识(通过inode号,文件系统可以精确定位和访问特定的文件或目录);2. 文件和目录的元数据(元数据包括:文件类型、权限、所有者、大小、修改时间等);3. 文件数据块的索引(保存文件数据存储在文件系统的位置);4. 硬链接实现(inode允许文件系统中的多个目录项指向同一个inode);5. 文件系统性能优化(通过inode编号,文件系统可以快速定位和访问文件或目录的元数据和数据块,而不需要遍历整个目录结构)
目录缓存(dcache):是一种缓存机制,用于存储最近访问的目录项的信息,他被内核维护,并提供了一种快速查找和访问文件和目录的机制。主要作用总结:1. 提高文件系统性能(dcach存储了最近访问的目录项信息,包括:目录名、文件名、inode号,通过这些信息可以避免频繁访问磁盘);2. 减少目录遍历开销(dcache避免了每次访问遍历目录的开销);3. 加速路径解析(当用户进程给出一个文件路径时候,内核需要将路径解析为相应的inode号码,dcache可以加速路径解析的过程,通过缓存已经解析过的路径组件,避免重复的路径解析操作);4. 优化内存使用(dcache缓存的信息通常存在内核内存中,它使用一部分系统内存来存储最近访问的项目,通过调整dcache的大小和策略可以在内存使用和性能之间进行平衡,以满足系统需求)
文件对象(File):文件对象用于表示文件的抽象概念或实体。它提供了一组文件方法和属性,用于对文件进行读、写、定位、删除等操作。
另外:
地址空间对象(Address Space Object):用于管理文件数据在内存中的表示和访问的数据结构。主要作用:1. 文件数据的缓存和管理;2. 内存页面的分配和管理;3. 文件数据的映射和解除映射;4. 文件数据的共享和复制;内存页面的回写和同步。