【Linux】内核学习笔记(十二)——虚拟文件系统

本篇是《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标志进行克隆即可共享该结构体。

发表评论

您的电子邮箱地址不会被公开。