本篇是《Linux内核学习笔记》系列的第十二篇,讲解另一个重要的子系统——虚拟文件系统VFS。
文件系统多种多样,甚至跨越各种各样的介质。Linux在其上建立了一套通用的接口,让一切都变得简单。
1. 概念
1.1. VFS分层结构
老曹在课上讲过分层结构的思想。课件:http://sa.nemoworks.info/03-layered.html 视频:https://www.bilibili.com/video/BV1iZ4y1r7dt?spm_id_from=333.999.0.0
简单来说,分层结构包括用户界面层、业务逻辑层、数据持久化层。VFS的设计有着出奇相似的结构,如理解欠妥还请指出:
- 用户界面层:即系统调用封装函数;
- 业务逻辑层:系统调用例程和虚拟文件系统接口;
- 数据持久化层:具体文件系统的“CRUD”操作和底层硬件访问。
1.2. Unix文件系统
Unix文件系统带来了四个抽象概念:文件、目录项、索引节点和挂载点,它们提供了清晰、一致的业务逻辑接口。接下来八股文预警(
首先,文件系统承载着文件、目录等相关信息。Unix系统中,任何文件系统都安装在一个特定的挂载点上,每一挂载点是根文件系统树上的枝叶,如/、/home等等。文件系统本身的信息存放于一个超级块之中。
文件本身是一个有序、有限长的字节串,Unix以流式存储的方式进行保存。换言之,众字节完全平等。即便是目录文件,也是如此,没有什么特殊的。相比之下,另外一些文件系统会进行结构化的文件存储,例如以行作为记录,等等。
文件通过目录组织起来,但目录也是普通的文件,里面存放着目录项,指向对应文件的inode。尽管目录里面有很多目录项,还是要注意,这只是因为我们把目录文件的内容解释成目录项,它的存放方式与普通文件并没有任何区别。
大名鼎鼎的inode就是索引节点,它存放文件的元数据,并指向文件实际使用的磁盘块。
这是Unix本家的文件系统设计,VFS也按照这样的方式进行接口的统一化。但微软有一些不听话的文件系统如FAT、NTFS等,并不采用相同的设计方式,因此还需要进行封装,才能与VFS协同工作。一般只要进行on-the-fly地处理即可,但仍会影响效率。
1.3. VFS对象及其数据结构
VFS采用面向对象的设计方式,不过你可能会问C语言怎么面向对象?那当然是可以的了,不过是this指针和继承的一些小trick。C++翻译成的汇编代码是什么样的,C就可以写成什么样的。
VFS的四个主要对象类型是:
- 超级块super_block,代表一个已安装的文件系统;
- 索引节点inode,代表具体文件;
- 目录项dentry,是路径的一个组成部分;
- 文件file,代表由进程打开的文件。
注意,没有目录对象,因为目录就是文件。
每个主要对象都包含一个操作对象,用于存放主要对象的各种“成员函数”,真是美妙极了。
- super_operations,包括能够对特定文件系统调用的方法,例如write_inode、sync_fs等;
- inode_operations,包括对特定文件调用的方法,例如create、link等;
- dentry_operations,包括对特定目录调用的方法,例如d_compare、d_delete等;
- file_operations,包括对进程打开文件调用的方法,例如read、write等。
接下来我们将看到Linux如何优雅地实现OOP。
2. 超级块
2.1. 主要对象super_block
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;
结构体比较大,只放出一点点。首先,由s_list域可以看出,超级块存放在一个链表中;其次s_op域存放了超级块的操作对象。
2.2. 操作对象super_operations
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);
很多函数指针。将超级块操作剥离出来成为一个操作对象是有好处的。C中没有真正的类,如果不将函数指针剥离出来,那么每个结构体都需要维护一张“虚函数表”,增大内存开销。
通过操作对象,内核可以创建、释放、写入、删除索引节点等,修改磁盘上的超级块,或者锁定、解锁文件系统等,它们都是在进程上下文中调用的,除了dirty_inode以外,都可以被阻塞。
文件系统可以将超级块操作对象中不需要的函数指针设为NULL,这使得VFS为其调用通用操作函数。在此,读者想必已经嗅到了override的味道。
3. 索引节点
3.1. 主要对象inode
Unix本家的文件系统当然有inode。有的文件系统可能没有inode,而是将文件描述信息作为文件的一部分,或者将它们存储于数据库中。不管如何,VFS需要为它们在内存中创建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;
inode代表文件系统的一个文件,但仅在文件被访问时,才会在内存中创建。管道、设备等等都是文件,都可以用inode表示,它们在inode中都有对应的域。
对于没有inode的文件系统,写入inode到磁盘时,当然会使用特定的(override过的)方式。inode只是信息的载体而已。
3.2. 操作对象inode_operations
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 inode *, int);
struct posix_acl * (*get_acl)(struct inode *, int);
int (*readlink) (struct dentry *, char __user *,int);
int (*create) (struct inode *,struct dentry *, umode_t, bool);
int (*link) (struct dentry *,struct inode *,struct dentry *);
int (*unlink) (struct inode *,struct dentry *);
int (*symlink) (struct inode *,struct dentry *,const char *);
int (*mkdir) (struct inode *,struct dentry *,umode_t);
int (*rmdir) (struct inode *,struct dentry *);
int (*mknod) (struct inode *,struct dentry *,umode_t,dev_t);
int (*rename) (struct inode *, struct dentry *,
struct inode *, struct dentry *, unsigned int);
这些函数名应该可以说是比较亲切了,比如创建、查找、删除、链接inode等等。
4. 目录项
4.1. 主要对象dentry
每一个目录都是文件。某特定文件的路径名是由诸多目录的名字拼接而成的,因此每向下查找一级,都要打开一个目录。为了方便查找,dentry对象应运而生。
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 */
目录项对象是现场创建的,不与磁盘中的数据结构相对应。
目录项本身有三种使用状态:被使用、未被使用和负状态。被使用即目录项对应有效inode,且目录项正被使用。未被使用即目录项未被使用,但指向的仍是有效inode,因此可以加速查找。负状态即指向的inode无效,可以保留着以便在一些极端情况下加速查找,也可以释放回slab里,回炉重造。
第一次查找某个文件名时,内核需要从根目录开始逐个解析目录名并产生目录项对象,这是比较耗时的事情,因此内核有必要将目录项对象缓存在目录项缓存(dcache)中。首先很容易想象,使用哈希建立路径名到目录项的映射是非常自然的事情;其次,使用LRU链表淘汰最近最少使用的目录项。最后,一个inode可能因为链接而产生多个目录项,这些目录项由一个链表组织起来。
我们说cache是在时间和空间局部性下产生的。目录项正是如此,由于被访问的文件很可能在短时间内再次被访问(例如打开文件后很可能会读取或写入),所以有时间局部性;被访问的文件同一目录下的其他文件也可能被访问(例如读取数据或写入日志),所以有空间局部性。对目录项进行缓存能极大地提升文件存取效率。
4.2. 操作对象dentry_operation
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;
目录项操作包括计算哈希值、比较路径名、删除、释放等等。
5. 文件对象
5.1. 主要对象file
站在用户的角度考虑,最先能想到的文件系统的组成部分应该是进程已打开文件。我们所熟悉的FILE结构体中有偏移量、缓冲区等等概念,在file对象中其实都有映射。
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_links, 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;
file并不映射到磁盘的某个数据结构,因此没有脏位等标志。不过,它对应的inode会记录该文件本身是否需要写回。
5.2. 操作对象 file_operations
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 *);
文件操作在形式上很像系统调用,它也正是标准Unix系统调用的基础。不同的文件系统可以专门设计不同的文件操作,但如果不需要特别实现的话,使用通用操作就可以,即简单地将指针置为NULL。
文件操作包括:打开关闭文件、读写文件、更新偏移量指针等各种广为人知的操作。还有一个神奇的设备驱动系统调用ioctl,可以看这篇文章了解它的作用。
6. 其他的数据结构
6.1. 文件系统file_system_type
Linux支持很多文件系统,因此有必要用一个特殊的结构来描述相应文件系统的行为。
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_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 i_mutex_dir_key;
};
以NTFS系统为例,这是其文件系统描述结构。要注意,一种文件系统只有一个这样的结构,它是全局的。
static struct file_system_type ntfs_fs_type = {
.owner = THIS_MODULE,
.name = "ntfs",
.mount = ntfs_mount,
.kill_sb = kill_block_super,
.fs_flags = FS_REQUIRES_DEV,
};
MODULE_ALIAS_FS("ntfs");
6.2. 文件系统实例vfsmount
安装一个文件系统时,就会产生一个vfsmount结构体,它代表文件系统的实例——即一个挂载点。
struct vfsmount {
struct dentry *mnt_root; /* root of the mounted tree */
struct super_block *mnt_sb; /* pointer to superblock */
int mnt_flags;
} __randomize_layout;
mnt_flags域可以用来指定一些标志,例如禁止访问设备、禁止执行可执行文件,等等。这里有一点点相关的定义。
#define MNT_NOSUID 0x01
#define MNT_NODEV 0x02
#define MNT_NOEXEC 0x04
6.3. 进程相关的数据结构
很多进程相关的文件系统概念在操作系统课上会作为重点去讲,接下来我们会了解它们在真实的Linux系统中到底是怎么实现的。
6.3.1. file_struct
该结构体管理一个特定进程已打开的各个文件,在进程描述符中有一个指向它的指针。
/*
* Open file table structure
*/
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];
};
里面count字段表示当前有多少个进程在使用该结构体。一般的进程是独占它的,不过以克隆标志CLONE_FILES创建的进程会和父进程共享这些已打开文件。引用计数可以防止file_struct在被使用时撤销。
fd_array则是静态的文件描述符表,存放了各种已打开的文件对象。看起来只放得下64个file对象指针,所以如果需要打开更多文件的话,file_struct中的fdtable结构会把指针指向一个新的文件描述符数组,而不是结构体中原有的静态数组。
struct fdtable {
unsigned int max_fds;
struct file __rcu **fd; /* current fd array */
unsigned long *close_on_exec;
unsigned long *open_fds;
unsigned long *full_fds_bits;
struct rcu_head rcu;
};
6.3.2. fs_struct
它包含文件系统和进程相关的信息,也是进程描述符中的一员:
struct fs_struct {
int users;
spinlock_t lock;
seqcount_spinlock_t seq;
int umask;
int in_exec;
struct path root, pwd;
} __randomize_layout;
包含了当前进程的当前工作目录和根目录,用CLONE_FS标志进行克隆即可共享该结构体。