Linux 文件系统(三)VFS

原图

通用文件系统接口

VFS 使得用户可以直接使用 open()、read() 和 write() 这样的系统调用而无须考虑具体文件系统和实际物理介质,使得这些通用的系统调用可以跨越各种文件系统和不同介质执行。

Unix 使用了四种和文件系统相关的传统抽象概念:文件、目录项、索引节点和安装点 (mount point)。

VFS 对象及其数据结构

VFS 其实采用的是面向对象的设计思路,使用一组数据结构来代表通用文件对象,有四个主要的对象类型,它们分别是:

  • 超级块对象,它代表一个具体的已安装文件系统
  • 索引节点对象,它代表一个具体文件
  • 目录项对象,它代表一个目录项,是路径的一个组成部分
  • 文件对象,它代表由进程打开的文件

注意,因为 VFS 将目录作为一个文件来处理,所以不存在目录对象。

每个主要对象中都包含一个操作对象,这些操作对象描述了内核针对主要对象可以使用的方法:

  • super_operations 对象,其中包括内核针对特定文件系统所能调用的方法,比如 write_inode() 和 sync_fs() 等方法
  • inode_operations 对象,其中包括内核针对特定文件所能调用的方法,比如 create() 和 link() 等方法
  • dentry_operations 对象,其中包括内核针对特定目录所能调用的方法,比如 d_compare() 和 d_delete() 等方法
  • file_operations 对象,其中包括进程针对已打开文件所能调用的方法,比如 read() 和 write() 等方法

操作对象作为一个结构体指针来实现,此结构体中包含指向操作其父对象的函数指针。

超级块对象

各种文件系统都必须实现超级块对象,该对象用于存储特定文件系统的信息,通常对应于存放在磁盘特定扇区中的文件系统超级块或文件系统控制块(所以称为超级块对象)。对于并非基于磁盘的文件系统(如基于内存的文件系统,比如 sysfs),它们会在使用现场创建超级块并将其保存到内存中。超级块对象由 super block 结构体表示,定义在文件《linux/fs.h》中,下面给出它的结构和各个域的描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
struct super_block {
struct list_head s_list; /* Keep this first */
dev_t s_dev; /* search index; _not_ kdev_t */
unsigned char s_blocksize_bits;
unsigned long s_blocksize;
loff_t s_maxbytes; /* Max file size */
struct file_system_type *s_type;
const struct super_operations *s_op;
const struct dquot_operations *dq_op;
const struct quotactl_ops *s_qcop;
const struct export_operations *s_export_op;
unsigned long s_flags;
unsigned long s_iflags; /* internal SB_I_* flags */
unsigned long s_magic;
struct dentry *s_root;
struct rw_semaphore s_umount;
int s_count;
atomic_t s_active;
#ifdef CONFIG_SECURITY
void *s_security;
#endif
const struct xattr_handler **s_xattr;
#ifdef CONFIG_FS_ENCRYPTION
const struct fscrypt_operations *s_cop;
struct key *s_master_keys; /* master crypto keys in use */
#endif
#ifdef CONFIG_FS_VERITY
const struct fsverity_operations *s_vop;
#endif
#ifdef CONFIG_UNICODE
struct unicode_map *s_encoding;
__u16 s_encoding_flags;
#endif
struct hlist_bl_head s_roots; /* alternate root dentries for NFS */
struct list_head s_mounts; /* list of mounts; _not_ for fs use */
struct block_device *s_bdev;
struct backing_dev_info *s_bdi;
struct mtd_info *s_mtd;
struct hlist_node s_instances;
unsigned int s_quota_types; /* Bitmask of supported quota types */
struct quota_info s_dquot; /* Diskquota specific options */

struct sb_writers s_writers;

/*
* Keep s_fs_info, s_time_gran, s_fsnotify_mask, and
* s_fsnotify_marks together for cache efficiency. They are frequently
* accessed and rarely modified.
*/
void *s_fs_info; /* Filesystem private info */

/* Granularity of c/m/atime in ns (cannot be worse than a second) */
u32 s_time_gran;
/* Time limits for c/m/atime in seconds */
time64_t s_time_min;
time64_t s_time_max;
#ifdef CONFIG_FSNOTIFY
__u32 s_fsnotify_mask;
struct fsnotify_mark_connector __rcu *s_fsnotify_marks;
#endif

char s_id[32]; /* Informational name */
uuid_t s_uuid; /* UUID */

unsigned int s_max_links;
fmode_t s_mode;

/*
* The next field is for VFS *only*. No filesystems have any business
* even looking at it. You had been warned.
*/
struct mutex s_vfs_rename_mutex; /* Kludge */

/*
* Filesystem subtype. If non-empty the filesystem type field
* in /proc/mounts will be "type.subtype"
*/
const char *s_subtype;

const struct dentry_operations *s_d_op; /* default d_op for dentries */

/*
* Saved pool identifier for cleancache (-1 means none)
*/
int cleancache_poolid;

struct shrinker s_shrink; /* per-sb shrinker handle */

/* Number of inodes with nlink == 0 but still referenced */
atomic_long_t s_remove_count;

/*
* Number of inode/mount/sb objects that are being watched, note that
* inodes objects are currently double-accounted.
*/
atomic_long_t s_fsnotify_connectors;

/* Being remounted read-only */
int s_readonly_remount;

/* per-sb errseq_t for reporting writeback errors via syncfs */
errseq_t s_wb_err;

/* AIO completions deferred from interrupt context */
struct workqueue_struct *s_dio_done_wq;
struct hlist_head s_pins;

/*
* Owning user namespace and default context in which to
* interpret filesystem uids, gids, quotas, device nodes,
* xattrs and security labels.
*/
struct user_namespace *s_user_ns;

/*
* The list_lru structure is essentially just a pointer to a table
* of per-node lru lists, each of which has its own spinlock.
* There is no need to put them into separate cachelines.
*/
struct list_lru s_dentry_lru;
struct list_lru s_inode_lru;
struct rcu_head rcu;
struct work_struct destroy_work;

struct mutex s_sync_lock; /* sync serialisation lock */

/*
* Indicates how deep in a filesystem stack this SB is
*/
int s_stack_depth;

/* s_inode_list_lock protects s_inodes */
spinlock_t s_inode_list_lock ____cacheline_aligned_in_smp;
struct list_head s_inodes; /* all inodes */

spinlock_t s_inode_wblist_lock;
struct list_head s_inodes_wb; /* writeback inodes */
} __randomize_layout;

创建、管理和撤销超级块对象的代码位于文件 fs/super.c 中。超级块对象通过 alloc_super() 函数创建并初始化。在文件系统安装时,文件系统会调用该函数以便从磁盘读取文件系统超级块,并且将其信息填充到内存中的超级块对象中。

超级块操作

超级块对象中最重要的一个域是 s_op,它指向超级块的操作函数表。超级块操作函数表由 super_operations 结构体表示,定义在文件《linux/fs.h》中,其形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb);
void (*destroy_inode)(struct inode *);
void (*free_inode)(struct inode *);

void (*dirty_inode) (struct inode *, int flags);
int (*write_inode) (struct inode *, struct writeback_control *wbc);
int (*drop_inode) (struct inode *);
void (*evict_inode) (struct inode *);
void (*put_super) (struct super_block *);
int (*sync_fs)(struct super_block *sb, int wait);
int (*freeze_super) (struct super_block *);
int (*freeze_fs) (struct super_block *);
int (*thaw_super) (struct super_block *);
int (*unfreeze_fs) (struct super_block *);
int (*statfs) (struct dentry *, struct kstatfs *);
int (*remount_fs) (struct super_block *, int *, char *);
void (*umount_begin) (struct super_block *);

int (*show_options)(struct seq_file *, struct dentry *);
int (*show_devname)(struct seq_file *, struct dentry *);
int (*show_path)(struct seq_file *, struct dentry *);
int (*show_stats)(struct seq_file *, struct dentry *);
#ifdef CONFIG_QUOTA
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);
struct dquot **(*get_dquots)(struct inode *);
#endif
long (*nr_cached_objects)(struct super_block *,
struct shrink_control *);
long (*free_cached_objects)(struct super_block *,
struct shrink_control *);
};

该结构体中的每一项都是一个指向超级块操作函数的指针,超级块操作函数执行文件系统和索引节点的底层操作。当文件系统需要对其超级块执行操作时,首先要在超级块对象中寻找需要的操作方法。

下面给出 super_operation 中,超级块操作函数的用法。

  • alloc_inode: 在给定的超级块下创建和初始化一个新的索引节点对象
  • destroy_inode: 用于释放给定的索引节点
  • dirty_inode: VFS 在索引节点脏(被修改)时会调用此函数。日志文件系统(如 ext3 和 ext4)执行该函数进行日志更新
  • write_inode: 用于将给定的索引节点写入磁盘。wait 参数指明写操作是否需要同步
  • ...

索引节点对象

索引节点对象包含了内核在操作文件或目录时需要的全部信息。对于 Unix 风格的文件系统来说,这些信息可以从磁盘索引节点直接读入。 索引节点对象由 inode 结构体表示,它定义在文件《linux/fs.h》中,下面给出它的结构体和各项的描述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/*
* Keep mostly read-only and often accessed (especially for
* the RCU path lookup and 'stat' data) fields at the beginning
* of the 'struct inode'
*/
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;

#ifdef CONFIG_FS_POSIX_ACL
struct posix_acl *i_acl;
struct posix_acl *i_default_acl;
#endif

const struct inode_operations *i_op;
struct super_block *i_sb;
struct address_space *i_mapping;

#ifdef CONFIG_SECURITY
void *i_security;
#endif

/* Stat data, not accessed from path walking */
unsigned long i_ino;
/*
* Filesystems may only read i_nlink directly. They shall use the
* following functions for modification:
*
* (set|clear|inc|drop)_nlink
* inode_(inc|dec)_link_count
*/
union {
const unsigned int i_nlink;
unsigned int __i_nlink;
};
dev_t i_rdev;
loff_t i_size;
struct timespec64 i_atime;
struct timespec64 i_mtime;
struct timespec64 i_ctime;
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
unsigned short i_bytes;
u8 i_blkbits;
u8 i_write_hint;
blkcnt_t i_blocks;

#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
#endif

/* Misc */
unsigned long i_state;
struct rw_semaphore i_rwsem;

unsigned long dirtied_when; /* jiffies of first dirtying */
unsigned long dirtied_time_when;

struct hlist_node i_hash;
struct list_head i_io_list; /* backing dev IO list */
#ifdef CONFIG_CGROUP_WRITEBACK
struct bdi_writeback *i_wb; /* the associated cgroup wb */

/* foreign inode detection, see wbc_detach_inode() */
int i_wb_frn_winner;
u16 i_wb_frn_avg_time;
u16 i_wb_frn_history;
#endif
struct list_head i_lru; /* inode LRU list */
struct list_head i_sb_list;
struct list_head i_wb_list; /* backing dev writeback list */
union {
struct hlist_head i_dentry;
struct rcu_head i_rcu;
};
atomic64_t i_version;
atomic64_t i_sequence; /* see futex */
atomic_t i_count;
atomic_t i_dio_count;
atomic_t i_writecount;
#if defined(CONFIG_IMA) || defined(CONFIG_FILE_LOCKING)
atomic_t i_readcount; /* struct files open RO */
#endif
union {
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
void (*free_inode)(struct inode *);
};
struct file_lock_context *i_flctx;
struct address_space i_data;
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct cdev *i_cdev;
char *i_link;
unsigned i_dir_seq;
};

__u32 i_generation;

#ifdef CONFIG_FSNOTIFY
__u32 i_fsnotify_mask; /* all events this inode cares about */
struct fsnotify_mark_connector __rcu *i_fsnotify_marks;
#endif

#ifdef CONFIG_FS_ENCRYPTION
struct fscrypt_info *i_crypt_info;
#endif

#ifdef CONFIG_FS_VERITY
struct fsverity_info *i_verity_info;
#endif

void *i_private; /* fs or device private pointer */
} __randomize_layout;

一个索引节点代表文件系统中(但是索引节点仅当文件被访问时,才在内存中创建)的一个文件,它也可以是设备或管道这样的特殊文件。因此索引节点结构体中有一些和特殊文件相关的项,比如 i_pipe 项就指向一个代表有名管道的数据结构,i_bdev 指向块设备结构体,i_cdev 指向字符设备结构体。这三个指针被存放在一个公用体中,因为一个给定的索引节点每次只能表示三者之一(或三者均不)。

索引节点操作

和超级块操作一样,索引节点对象中的 inode_operations 项也非常重要,因为它描述了 VFS 用以操作索引节点对象的所有方法,这些方法由文件系统实现。与超级块类似,对索引节点的操作调用方式如下:

1
i->i_op->truncate(i)

i 指向给定的索引节点,truncate() 函数是由索引节点 i 所在的文件系统定义的。inode_operations 结构体定义在文件《linux/fs.h》中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct inode_operations {
struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned 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 (*readlink) (struct dentry *, char __user *,int);

int (*create) (struct user_namespace *, struct inode *,struct dentry *,
umode_t, bool);
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 (*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);
int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start,
u64 len);
int (*update_time)(struct inode *, struct timespec64 *, 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);
} ____cacheline_aligned;

下面这些接口由各种函数组成,在给定的节点上,可能由 VF 执行这些函数,也可能由具体的文件系统执行:

  • create: VFS 通过系统调用 create() 和 open() 来调用该函数,从而为 dentry 对象创建一个新的索引节点。在创建时使用 mode 指定的初始模式
  • lookup: 该函数在特定目录中寻找索引节点,该索引节点要对应于 denrty 中给出的文件名
  • link: 该函数被系统调用 link() 调用,用来创建硬连接。硬连接名称由 dentry 参数指定,连接对象是 dir 目录中 old_dentry 目录项所代表的文件
  • unlink: 该函数被系统调用 unlinkO() 调用,从目录 dir 中删除由目录项 dentry 指定的索引节点对象
  • symlink: 该函数被系统调用 symlik() 调用,创建符号连接。该符号连接名称由 symname 指定,连接对象是 dir 目录中的 dentry 目录项
  • mkdir: 该函数被系统调用 mkdir() 调用,创建一个新目录。创建时使用 mode 指定的初始模式
  • rmdir: 该函数被系统调用 rmdir() 调用,删除 dir 目录中的 dentry 目录项代表的文件
  • ...

目录项对象

VFS 把目录当作文件对待,虽然它们可以统一由索引节点表示,但是 VFS 经常需要执行目录相关的操作,比如路径名查找等,为了方便查找操作,VFS 引入了目录项的概念。每个 dentry 代表路径中的一个特定部分。解析一个路径并遍历其分量绝非简单,它是耗时的、常规的字符串比较过程,执行耗时、代码繁琐。目录项对象的引入使得这个过程更加简单。目录项也可包括安装点。VFS 在执行目录操作时会现场创建目录项对象。 目录项对象由 dentry 结构体表示,定义在文件《linux/dcache.h》中。下面给出该结构体和其中各项的描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct dentry {
/* RCU lookup touched fields */
unsigned int d_flags; /* protected by d_lock */
seqcount_spinlock_t d_seq; /* per dentry seqlock */
struct hlist_bl_node d_hash; /* lookup hash list */
struct dentry *d_parent; /* parent directory */
struct qstr d_name;
struct inode *d_inode; /* Where the name belongs to - NULL is
* negative */
unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */

/* Ref lookup also touches following */
struct lockref d_lockref; /* per-dentry lock and refcount */
const struct dentry_operations *d_op;
struct super_block *d_sb; /* The root of the dentry tree */
unsigned long d_time; /* used by d_revalidate */
void *d_fsdata; /* fs-specific data */

union {
struct list_head d_lru; /* LRU list */
wait_queue_head_t *d_wait; /* in-lookup ones only */
};
struct list_head d_child; /* child of parent list */
struct list_head d_subdirs; /* our children */
/*
* d_alias and d_rcu can share memory
*/
union {
struct hlist_node d_alias; /* inode alias list */
struct hlist_bl_node d_in_lookup_hash; /* only for in-lookup ones */
struct rcu_head d_rcu;
} d_u;
} __randomize_layout;

与前面的两个对象不同,目录项对象没有对应的磁盘数据结构,VFS 根据字符串形式的路径名现场创建它。而且由于目录项对象并非真正保存在磁盘上,所以目录项结构体没有是否被修改的标志(也就是是否为脏、是否需要写回磁盘的标志)。

目录项状态

目录项对象有三种有效状态:被使用、未被使用和负状态。

  • 一个被使用的目录项对应一个有效的索引节点(即 d_inode 指向相应的索引节点)并且表明该对象存在一个或多个使用者(即 d_count 为正值)
  • 一个未被使用的目录项对应一个有效的索引节点(d_inode 指向一个索引节点),但是应指明 VFS 当前并未使用它(d_count 为 O)。该目录项对象仍然指向一个有效对象,而且被保留在缓存中以便需要时再使用它
  • 一个负状态的目录项没有对应的有效索引节点(d_inode 为 NULL),因为索引节点已被删除了,或路径不再正确了,但是目录项仍然保留,以便快速解析以后的路径查询。比如,一个守护进程不断地去试图打开并读取一个不存在的配置文件。open() 系统调用不断地返回 ENOENT,直到内核构建了这个路径、遍历磁盘上的目录结构体并检查这个文件的确不存在为止。即便这个失败的查找很浪费资源,但是将负状态缓存起来还是非常值得的

目录项缓存

如果 VFS 层遍历路径名中所有的元素并将它们逐个地解析成目录项对象,还要到达最深层目录,将是一件非常费力的工作,会浪费大量的时间。所以内核将目录项对象缓存在目录项缓存(简称 dcache)中。 目录项缓存包括三个主要部分:

  • “被使用的”目录项链表。该链表通过索引节点对象中的 i_dentry 项连接相关的索引节点,因为一个给定的索引节点可能有多个链接,所以就可能有多个目录项对象,因此用一个链表来连接它们
  • “最近被使用的”双向链表。该链表含有未被使用的和负状态的目录项对象。由于该链总是在头部插入目录项,所以链头节点的数据总比链尾的数据要新
  • 散列表和相应的散列函数用来快速地将给定路径解析为相关目录项对象

举例说明,假设你需要在自己目录中编译一个源文件,/home/dracula/src/the_sun _sucks.c,每一次对文件进行访问(比如说,首先要打开它,然后要存储它,还要进行编译等),VFS 都必须沿着嵌套的目录依次解析全部路径:/、home、dracula、src 和最终的 the_sun_sucks.c。为了避免每次访问该路径名都进行这种耗时的操作,VFS 会先在目录项缓存中搜索路径名,如果找到了,就无须花费那么大的力气了。相反,如果该目录项在目录项缓存中并不存在,VFS 就必须自己通过遍历文件系统为每个路径分量解析路径,解析完毕后,再将目录项对象加入 dcache 中,以便以后可以快速查找到它。

而 dcache 在一定意义上也提供对索引节点的缓存,也就是 icache。和目录项对象相关的索引节点对象不会被释放,因为目录项会让相关索引节点的使用计数为正,这样就可以确保索引节点留在内存中。只要目录项被缓存,其相应的索引节点也就被缓存了。

因为文件访问呈现空间和时间的局部性,所以对目录项和索引节点进行缓存非常有益。

目录项操作

dentry_operation 结构体指明了 VFS 操作目录项的所有方法。该结构定义在文件《linux/dcache.h》中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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_prune)(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 *);
} ____cacheline_aligned;

下面给出函数的具体用法:

  • d_revalidate: 该函数判断目录对象是否有效。VFS 准备从 dcache 中使用一个目录项时,会调用该函数。大部分文件系统将该方法置 NULL,因为它们认为 dcache 中的目录项对象总是有效的
  • d_hash: 该函数为目录项生成散列值,当目录项需要加入到散列表中时,VF 调用该函数
  • d_compare: VFS 调用该函数来比较 namel 和 name2 这两个文件名。多数文件系统使用 VFS 默认的操作,仅仅作字符串比较。对有些文件系统,比如 FAT,简单的字符串比较不能满足其需要。因为 FAT 文件系统不区分大小写,所以需要实现一种不区分大小写的字符串比较函数。注意使用该函数时需要加 dcache_lock 锁
  • d_delete: 当目录项对象的 d_count 计数值等于 O 时,VFS 调用该函数。注意使用该函数需要加 dcache_lock 锁和目录项的 d_lock
  • d_release: 当目录项对象将要被释放时,VFS 调用该函数,默认情况下,它什么也不做
  • d_iput: 当一个目录项对象丢失了其相关的索引节点时(也就是说磁盘索引节点被删除了),VFS 调用该函数。默认情况下 VFS 会调用 iput() 函数释放索引节点。如果文件系统重载了该函数,那么除了执行此文件系统特殊的工作外,还必须调用 iput() 函数

文件对象

VFS 的最后一个主要对象是文件对象。文件对象表示进程已打开的文件。文件对象包含我们非常熟悉的信息(如访问模式,当前偏移等),同样道理,文件操作和我们非常熟悉的系统调用 read() 和 write() 等也很类似。文件对象是已打开的文件在内存中的表示。该对象(不是物理文件)由相应的 open() 系统调用创建,由 close() 系统调用撤销,所有这些文件相关的调用实际上都是文件操作表中定义的方法。因为多个进程可以同时打开和操作同一个文件,所以同一个文件也可能存在多个对应的文件对象。文件对象仅仅在进程观点上代表已打开文件,它反过来指向目录项对象(反过来指向索引节点),其实只有目录项对象才表示已打开的实际文件。虽然一个文件对应的文件对象不是唯一的,但对应的索引节点和目录项对象无疑是唯一的。 文件对象由 file 结构体表示,定义在文件《linux/fs.h》中,下面给出该结构体和各项的描述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;

/*
* Protects f_ep, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;

u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;

#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct hlist_head *f_ep;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
errseq_t f_wb_err;
errseq_t f_sb_err; /* for syncfs */
} __randomize_layout
__attribute__((aligned(4))); /* lest something weird decides that 2 is OK */

类似于目录项对象,文件对象实际上没有对应的磁盘数据。所以在结构体中没有代表其对象是否为脏、是否需要写回磁盘的标志。文件对象通过 f_dentry 指针指向相关的目录项对象。目录项会指向相关的索引节点,索引节点会记录文件是否是脏的。

文件操作

和 VFS 的其他对象一样,文件操作表在文件对象中也非常重要。跟 file 结构体相关的操作与系统调用很类似,这些操作是标准 Unix 系统调用的基础。 文件对象的操作由 file_operations 结构体表示,定义在文件<linux/fs.h>中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 *);
unsigned long mmap_supported_flags;
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);
} __randomize_layout;

具体的文件系统可以为每一种操作做专门的实现,或者如果存在通用操作,也可以使用通用操作。一般在基于 Unix 的文件系统上,这些通用操作效果都不错。并不要求实际文件系统实现文件操作函数表中的所有方法—一虽然不实现最基础的那些操作显然是很不明智的,对不感兴趣的操作完全可以简单地将该函数指针置为 NULL。 下面给出操作的用法说明:

  • lleek: 该函数用于更新偏移量指针,由系统调用 lleek() 调用它
  • read: 该函数从给定文件的 offset 偏移处读取 conut 字节的数据到 buf 中,同时更新文件指针。由系统调用 read() 调用它
  • aio_read: 该函数从 iocb 描述的文件里,以同步方式读取 count 字节的数据到 buf 中。由系统调用 aio_read() 调用它
  • write: 该函数从给定的 buf 中取出 conut 字节的数据,写入给定文件的 offset 偏移处,同时更新文件指针。由系统调用 write() 调用它
  • aio_write: 该函数以同步方式从给定的 buf 中取出 conut 字节的数据,写入由 iocb 描述的文件中。由系统调用 aio_write() 调用它
  • readdir: 该函数返回目录列表中的下一个目录。由系统调用 readdir() 调用它
  • poll: 该函数睡眠等待给定文件活动。由系统调用 poll() 调用它
  • ioctl: 该函数用来给设备发送命令参数对。当文件是一个被打开的设备节点时,可以通过它进行设置操作。由系统调用 ioctl() 调用它。调用者必须持有 BKL
  • unlocked_ioctl: 其实现与 ioctl() 有类似的功能,只不过不需要调用者持有 BKL。如果用户空间调用 ioctl() 系统调用,VFS 便可以调用 unlocked_ioctl()(凡是 ioctl() 出现的场所)。因此文件系统只需要实现其中的一个,一般优先实现 unlocked_ioctl()
  • ...

和文件系统相关的数据结构

除了以上几种 VFS 基础对象外,内核还使用了另外一些标准数据结构来管理文件系统的其他相关数据。第一个对象是 file_system_type,用来描述各种特定文件系统类型,比如 ext3、ext4 或 UDF。第二个结构体是 vfsmount,用来描述一个安装文件系统的实例。

因为 Linux 支持众多不同的文件系统,所以内核必须由一个特殊的结构来描述每种文件系统的功能和行为。file_system_type 结构体被定义在《linux/fs.h》中,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct file_system_type {
const char *name;
int fs_flags;
#define FS_REQUIRES_DEV 1
#define FS_BINARY_MOUNTDATA 2
#define FS_HAS_SUBTYPE 4
#define FS_USERNS_MOUNT 8 /* Can be mounted by userns root */
#define FS_DISALLOW_NOTIFY_PERM 16 /* Disable fanotify permission events */
#define FS_ALLOW_IDMAP 32 /* FS has been updated to handle vfs idmappings. */
#define FS_THP_SUPPORT 8192 /* Remove once all fs converted */
#define FS_RENAME_DOES_D_MOVE 32768 /* FS will handle d_move() during rename() internally. */
int (*init_fs_context)(struct fs_context *);
const struct fs_parameter_spec *parameters;
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 hlist_head fs_supers;

struct lock_class_key s_lock_key;
struct lock_class_key s_umount_key;
struct lock_class_key s_vfs_rename_key;
struct lock_class_key s_writers_key[SB_FREEZE_LEVELS];

struct lock_class_key i_lock_key;
struct lock_class_key i_mutex_key;
struct lock_class_key invalidate_lock_key;
struct lock_class_key i_mutex_dir_key;
};

每种文件系统,不管有多少个实例安装到系统中,还是根本就没有安装到系统中,都只有一个 file_system_type 结构。 更有趣的事情是,当文件系统被实际安装时,将有一个 vfsmount 结构体在安装点被创建。该结构体用来代表文件系统的实例—一换句话说,代表一个安装点。 vfsmount 结构被定义在《linux/mount.h》中,下面是具体结构:

1
2
3
4
5
6
struct vfsmount {
struct dentry *mnt_root; /* root of the mounted tree */
struct super_block *mnt_sb; /* pointer to superblock */
int mnt_flags;
struct user_namespace *mnt_userns;
} __randomize_layout;

和进程相关的数据结构

系统中的每一个进程都有自己的一组打开的文件,像根文件系统、当前工作目录、安装点等。有三个数据结构将 VFS 层和系统的进程紧密联系在一起,它们分别是:file_struct、fs_struct 结构体。 file_struct 结构体定义在文件《linux/fdtable.h》中。该结构体由进程描述符中的 files 目录项指向。所有与单个进程(per-process)相关的信息、(如打开的文件及文件描述符)都包含在其中,其结构和描述如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct files_struct {
/*
* read mostly part
*/
atomic_t count;
bool resize_in_progress;
wait_queue_head_t resize_wait;

struct fdtable __rcu *fdt;
struct fdtable fdtab;
/*
* written part on a separate cache line in SMP
*/
spinlock_t file_lock ____cacheline_aligned_in_smp;
unsigned int next_fd;
unsigned long close_on_exec_init[1];
unsigned long open_fds_init[1];
unsigned long full_fds_bits_init[1];
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};

fd_array 数组指针指向已打开的文件对象。因为 NR_OPEN_DEFAULT 等于 BITS_PER_LONG,在 64 位机器体系结构中这个宏的值为 64,所以该数组可以容纳 64 个文件对象。如果一个进程所打开的文件对象超过 6 个,内核将分配一个新数组,并且将 fdt 指针指向它。所以对适当数量的文件对象的访问会执行得很快,因为它是对静态数组进行的操作;如果一个进程打开的文件数量过多,那么内核就需要建立新数组。所以如果系统中有大量的进程都要打开超过 6 个文件,为了优化性能,管理员可以适当增大 NR_OPEN_DEFAULT 的预定义值。 和进程相关的第二个结构体是 fs_struct. 该结构由进程描述符的 fs 域指向。它包含文件系统和进程相关的信息,定义在文件《linux/fs_struct.h》中,下面是它的具体结构体和各项描述:

1
2
3
4
5
6
7
8
struct fs_struct {
int users;
spinlock_t lock;
seqcount_spinlock_t seq;
int umask;
int in_exec;
struct path root, pwd;
} __randomize_layout;

该结构包含了当前进程的当前工作目录(pwd)和根目录。

参考文献

《Linux 内核设计与实现》