虚拟文件系统 (VFS)-基于linux3.10

2023-11-12

引言

虚拟文件系统(VFS, VirtualFileSystem)介于具体的文件系统和C库之间,其用提供一个统一的方法来操作文件、目录以及其它对象。其能够很好的抽象具体的文件系统,在linux上具体的文件系统主要分为三类:

l  基于非易失性的存储介质(磁盘、SSD、Flash)的文件系统,如Ext4,Resisterfs、FAT、Ubifs、yaffs2等

l  伪文件系统, 如proc和sys文件系统,它们的信息并不存储在非易失性介质上,而是动态的生成的。

l  网络文件系统,NTFS、CIFS等。

在操作系统运行时,某一时刻与虚拟文件系统相关的一个拓扑结构如下:


图0.1 虚拟文件系统

图0.1中反映的是数据结构层次的联系,对于刚接触VFS的人来说可能并不能很好的理解该图的意义,为了让上述的概念更容易理解,使用面向对象的思维对该图进行简化得到如下的对象图0.2。

该图比0.1要直观多了,分为几个部分,进程、文件对象、目录项、索引节点、超级快。结合图0.1和0.2,可以看出每个进程的files字段指向的是文件的对象,比如hello.c这样的文件等,而每个进程的fs字段则指向具体的文件系统的类型,如ext4、ubifs文件系统。由于一个进程可以打开多个文件,这就意味着必须有一个方法跟踪打开的文件,这就是fd_array数组的作用,从该数组的名称可以看出是文件描述符数组,实质上就是一个整数,一个打开的文件就会对应在该数组中存在一项,文件对象的f_dentry和f_inode字段指向了目录项和索引节点。根据索引节点和超级快信息就可以实际的存取非易失性设备上的数据了,实际上这之间通常还有一个块设备层,不过这和本章的主题没有关系,所以这里略过。

虚拟文件系统(VFS)主要的内容包括三个部分:

l  表示该文件系统的管理和组织数据结构

l  文件系统的挂载,

l  文件系统的操作,open、read等。

本章就按照上述的划分,分为三个部分来介绍VFS,次序和上面所列的是一致的。

1.1 VFS数据结构

1.1.1 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;

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

	/* 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 timespec		i_atime;
	struct timespec		i_mtime;
	struct timespec		i_ctime;
	spinlock_t		i_lock;	/* i_blocks, i_bytes, maybe i_size */
	unsigned short          i_bytes;
	unsigned int		i_blkbits;
	blkcnt_t		i_blocks;

	/* Misc */
	unsigned long		i_state;
	struct mutex		i_mutex;
	unsigned long		dirtied_when;	/* jiffies of first dirtying */
	struct hlist_node	i_hash;
	struct list_head	i_wb_list;	/* backing dev IO list */
	struct list_head	i_lru;		/* inode LRU list */
	struct list_head	i_sb_list;
	union {
		struct hlist_head	i_dentry;
		struct rcu_head		i_rcu;
	};
	u64			i_version;
	atomic_t		i_count;
	atomic_t		i_dio_count;
	atomic_t		i_writecount;
	const struct file_operations	*i_fop;	/* former ->i_op->default_file_ops */
	struct file_lock	*i_flock;
	struct address_space	i_data;
	struct list_head	i_devices;
	union {
		struct pipe_inode_info	*i_pipe;
		struct block_device	*i_bdev;
		struct cdev		*i_cdev;
	};

	__u32			i_generation;
	void			*i_private; /* fs or device private pointer */
};

这里的inode指的是内存中的inode的,其和具体的存储于非易失性介质上的inode并不完全一样。放在该结构体的开始处的字段是存取最为频繁的字段,这能够提高cache使用效率。i_mode存储文件类型(目录、普通、链接、套接字、块)和访问权限(读、写、执行)信息。i_opflags存放的标志共以下三种:

#define IOP_FASTPERM	0x0001
#define IOP_LOOKUP	0x0002
#define IOP_NOFOLLOW	0x0004

如果fastperm标志被设置,则说明该可以直接进入generic_permission()函数,而不需要执行inode->i_op的permission函数指针进行额外的检查。Loopup标志则意味着不需要调用inode->i_op->lookup成员检查是否能够查找目录项。最后的弄nofollow标志是告诉调用者不需要调用inode->i_op->follow_link查找连接了,这些操作会被频繁的调用,这会节省很多时间。

i_uid和i_gid是和该文件相关的用户ID和组ID。i_flags和超级块的标志是没有关系的,这些标志存储和文件相关的一些信息,这些信息包括写时刷新、存取时间是否更新、是否仅支持append操作等等。

i_op是索引节点的操作集,包含大量的用于操作索引节点的函数。

i_sb索引节点关联的超级块。

i_ino是索引节点的唯一编号。

i_nlink和__i_nlink是该索引节点的硬链接计数变量。

i_rdev设备文件的设备号(主、次),块设备、字符设备等

i_size是文件的长度,字节计算值。i_blocks是文件按块计算的长度。

i_atime、i_mtime、i_ctime分别是最后访问时间、最后修改时间、最后修改索引节点时间。

         i_blkbits文件块的位数。

         i_state、i_mutex、dirtied_when是索引节点的状态、互斥锁以及第一次使该索引节点脏的jiffies值。

         i_hash、i_wb_list、i_lru、i_sb_list是用来管理inode的链表。i_hash管理溢出的哈希链表,i_wb_list是writeback链表,i_lru是最近最少使用链表,i_sb_list指向的超级块链表。

union {
		struct hlist_head	i_dentry;
		struct rcu_head		i_rcu;
	};

i_dentry是目录项链表,如果不用作目录项,则用作rcu链表。  

i_version64位版本号,

i_count、i_dio_count、i_writecount分别是索引节点的引用计数、io计数、写计数。

i_fop缺省的文件操作。

i_flock文件锁。

i_data设备地址映射。

i_devices设备链表,

一个设备只能属于一种类型,所以下面的联合里表示的设备类型中,管道、块设备、字符设备只能属于其中的一个。

union {
		struct pipe_inode_info	*i_pipe;
		struct block_device	*i_bdev;
		struct cdev		*i_cdev;
	};

i_generation索引节点的版本号。

1.1.2 inode操作

在上面看到索引的节点的操作封装在了const struct inode_operations   *i_op;该结构体,

struct inode_operations {
	struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int);
	void * (*follow_link) (struct dentry *, struct nameidata *);
	int (*permission) (struct inode *, int);
	struct posix_acl * (*get_acl)(struct inode *, int);

	int (*readlink) (struct dentry *, char __user *,int);
	void (*put_link) (struct dentry *, struct nameidata *, void *);

	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 *);
	int (*setattr) (struct dentry *, struct iattr *);
	int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);
	int (*setxattr) (struct dentry *, const char *,const void *,size_t,int);
	ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);
	ssize_t (*listxattr) (struct dentry *, char *, size_t);
	int (*removexattr) (struct dentry *, const char *);
	int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start,
		      u64 len);
	int (*update_time)(struct inode *, struct timespec *, int);
	int (*atomic_open)(struct inode *, struct dentry *,
			   struct file *, unsigned open_flag,
			   umode_t create_mode, int *opened);
} ____cacheline_aligned;

该函数操作集中各个函数的意义还是很明显的。

lookup根据文件描述符查找对应的索引节点。

link用于删除文件。

1.1.3 文件操作

inode的如下部分定义的是文件的操作。

const struct file_operations	*i_fop;
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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*readdir) (struct file *, void *, filldir_t);
	unsigned int (*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 (*aio_fsync) (struct kiocb *, 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 **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	int (*show_fdinfo)(struct seq_file *m, struct file *f);
};

这个函数集的很多操作在应用程序编程都调用过类似的接口,它们的意义还是很容易明白的。

fs/ubifs/file.c
const struct file_operations ubifs_file_operations = {
	.llseek         = generic_file_llseek,
	.read           = do_sync_read,
	.write          = do_sync_write,
	.aio_read       = generic_file_aio_read,
	.aio_write      = ubifs_aio_write,
	.mmap           = ubifs_file_mmap,
	.fsync          = ubifs_fsync,
	.unlocked_ioctl = ubifs_ioctl,
	.splice_read	= generic_file_splice_read,
	.splice_write	= generic_file_splice_write,
#ifdef CONFIG_COMPAT
	.compat_ioctl   = ubifs_compat_ioctl,
#endif
};

1.1.4 文件系统信息

图0.1中的文件系统信息结构体如下:

struct fs_struct {
	int users;
	spinlock_t lock;
	seqcount_t seq;
	int umask;
	int in_exec;
	struct path root, pwd;
};

Users反映的是用户数,umask是设置文件权限的掩码,path是路径名,放映的是文件系统的挂载路径和目录项。早先这两个成员直接存在于fs_strcut中。

struct path {
	struct vfsmount *mnt;
	struct dentry *dentry;
};

在mount一个文件系统时,再回到这里查看该文件系统。

1.1.5 目录项

在真实的文件系统(ext2、ext3)中并没有目录项的概念,其目的是加速访问先前查找操作的结果,在读取一个目录或者文件数据之后,就会创建一个dentry实例,以缓存查找到的实例。

include/linux/dcache.h
struct dentry {
	/* RCU lookup touched fields */
	unsigned int d_flags;		/* protected by d_lock */
	seqcount_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 */
	unsigned int d_count;		/* protected by d_lock */
	spinlock_t d_lock;		/* per dentry lock */
	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 */

	struct list_head d_lru;		/* LRU list */
	/*
	 * d_child and d_rcu can share memory
	 */
	union {
		struct list_head d_child;	/* child of parent list */
	 	struct rcu_head d_rcu;
	} d_u;
	struct list_head d_subdirs;	/* our children */
	struct hlist_node d_alias;	/* inode alias list */
};

d_flags定义了目录项的相关的一些标志,这些标志和dentry定义于同一个文件中,这些标志用于表明目录项的状态。

d_seq的序列锁,保护之用。

d_hash用于散列溢出。

d_parent是一个指针,指向当前节点父目录的目录项实例,当前的dentry实例位于父目录的d_subdirs链表中。

d_name是文件/目录的名称,qstr封装了文件名字符串的长度和散列值。

d_inode指向该目录项管关联的索引节点。

d_iname用于存储短文件名。

d_count是该目录项的引用计数。

d_sb指向目录项所属的超级块实例。

d_alias连接表示相同文件的各个目录项。

d_lru,最近最少使用链表,其中不再使用的对象将授予一个最后宽限期,过了宽限期后才从内存移除。

d_op是目录项的操作函数集。

struct dentry_operations {
	int (*d_revalidate)(struct dentry *, unsigned int);
	int (*d_weak_revalidate)(struct dentry *, unsigned int);
	int (*d_hash)(const struct dentry *, const struct inode *,
			struct qstr *);
	int (*d_compare)(const struct dentry *, const struct inode *,
			const struct dentry *, const struct inode *,
			unsigned int, const char *, const struct qstr *);
	int (*d_delete)(const 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)(struct dentry *, bool);
} ____cacheline_aligned;

d_revalidate、d_weak_revalidate网络文件系统使用,检查目录项是否能够反映文件系统中的情况。

d_hash计算目录项的哈希值,该值用于散列目录项

d_compare用于比较两个目录对象的文件名。

d_release用于递减目录项的引用计数值(d_count),当计数值达到零时,调用d_delete函数删除目录项。

d_iput从不在使用的目录项中释放inode。

1.1.6 超级块

超级块定义于include/linux/fs.h,在装载一个文件系统时,很重要的一步是调用read_super读取存储介质上的超级块信息,并重构到如下的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;
	unsigned long		s_flags;
	unsigned long		s_magic;
	struct dentry		*s_root; //关联超级块的根目录项
	struct rw_semaphore	s_umount;
	int			s_count;
	atomic_t		s_active;
	const struct xattr_handler **s_xattr;//扩展属性处理函数指针。

	struct list_head	s_inodes;	/* all inodes */
	struct hlist_bl_head	s_anon;		/* anonymous dentries for (nfs) exporting */
	struct list_head	s_files;
	struct list_head	s_mounts;	/* list of mounts; _not_ for fs use */
	/* s_dentry_lru, s_nr_dentry_unused protected by dcache.c lru locks */
	struct list_head	s_dentry_lru;	/* unused dentry lru */
	int			s_nr_dentry_unused;	/* # of dentry on lru */

	/* s_inode_lru_lock protects s_inode_lru and s_nr_inodes_unused */
	spinlock_t		s_inode_lru_lock ____cacheline_aligned_in_smp;
	struct list_head	s_inode_lru;		/* unused inode lru */
	int			s_nr_inodes_unused;	/* # of inodes on lru */

	struct block_device	*s_bdev;//真实的文件系统所在的块设备,该结构体详细定义了设备的操作和功能。
	struct backing_dev_info *s_bdi;//拥有所有者数据的块设备指针。
	struct mtd_info		*s_mtd; //分区信息,MTD(Memory Technology Device)系统使用。
	struct hlist_node	s_instances;
	struct quota_info	s_dquot;	/* Diskquota specific options */

	struct sb_writers	s_writers;//对超级块操作的管理结构,会使用wait queue的方式管理超级块写者。

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

	void 			*s_fs_info;	/* Filesystem private info */
	unsigned int		s_max_links;
	fmode_t			s_mode;

	/* Granularity of c/m/atime in ns.
	   Cannot be worse than a second */
	u32		   s_time_gran;

	/*
	 * 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"
	 */
	char *s_subtype;

	/*
	 * Saved mount options for lazy filesystems using
	 * generic_show_options()
	 */
	char __rcu *s_options;
	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;

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

s_blocksize和s_blocksize_bits分别指定了文件系统的块长度,前一个是按字节计算的长度,而后一个是前一个取以2为底的对数。

s_op是超级块操作函数,这些函数由特定的文件系统实现。

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 *, 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_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 (*umount_begin) (struct super_block *);//仅用于NFS、CIFS和9fs网络文件系统和用户空间文件系统(FUSE),它允许在卸载操作系统之前,于远程的文件系统提供者进行通信。

	int (*show_options)(struct seq_file *, struct dentry *);//用于proc文件系统,用以显示文件系统装载的选项。
	int (*show_devname)(struct seq_file *, struct dentry *);
	int (*show_path)(struct seq_file *, struct dentry *);
	int (*show_stats)(struct seq_file *, struct dentry *);//统计信息,也是用于proc文件系统。
	int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t);
	int (*nr_cached_objects)(struct super_block *);
	void (*free_cached_objects)(struct super_block *, int);
};

alloc_inode和destroy_inode分别是创建和销毁一个索引节点,dirty_inode和write_inode分别是将索引节点标记为“脏”和将更新存储介质上的索引节点。drop_inode删除索引节点。sysfs这类比较特殊的文件系统会实现evict_inode方法,这是因为对于sysfs而言,sysfs_dirent既做为索引节点又做为目录项,为了防止sysfs的索引节点号被永久的删除,采用了在索引节点里有一项指向sysfs_dirent,而这里的evict_inode()正是用于在索引节点销毁时解除上述索引节点对的sysfs_dirent引用。对于pur_super释放超级块,在卸载文件系统时会调用该方法。

freeze_fs和unfreeze_fs是在linux3.9才引入的,用于文件的快照功能。

1.2 文件系统挂载

1.2.1 注册文件系统

文件系统的注册工作由fs/super.c中的register_filesystem函数来完成。该函数的结构非常简单。
int register_filesystem(struct file_system_type * fs)
{
	int res = 0;
	struct file_system_type ** p;

	if (fs->next)
		return -EBUSY;
	write_lock(&file_systems_lock);
	p = find_filesystem(fs->name, strlen(fs->name));
	if (*p)
		res = -EBUSY;
	else
		*p = fs;
	write_unlock(&file_systems_lock);
	return res;
}

该函数的参数定义如下:

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_USERNS_DEV_MOUNT	16 /* A userns mount does not imply MNT_NODEV */
#define FS_RENAME_DOES_D_MOVE	32768	/* FS will handle d_move() during rename() internally. */
	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;
};

该函数稍微有些长,其name参数是该文件系统的名称,fs_flags是装载标志,如只读、禁止设置setuid/setgid操作等,其字段的下面由#define定义的哪些宏就是用于fs_flags的。mount函数指针是在挂载该文件系统时会调用的方法。Kill_sb 用于删除一个超级块对象,owner在该文件系统使用模块形式加载到内核时会用到。Next用于串接各个文件系统以便与管理。Fs_super是将同一个文件系统多个超级块聚集在一起的链表头。最后是一些用于保护的锁。

添加一个文件系统的逻辑还是很简单的,由于所有的文件系统都连接在file_systems这个链表上,将内核链表上的文件系统的名称和要注册的文件系统的名称(name)进行比较,如果找到了,则if (*p)就会成立,即res = -EBUSY;否则简单的将新添加的文件系统放在链表的末尾*p = fs。

1.2.2 文件系统装载和卸载

文件系统的装载和卸载比仅仅注册一个文件系统要复杂许多。文件系统的装载由mount系统调用发起,对于根文件系统则是由mount_root(init/do_mounts.c)完成。文件系统的注销则由umount完成。查看当前挂载的文件系统可以使用mount命令。

/dev/sda1 on / type ext4 (rw,errors=remount-ro)
proc on /proc type proc (rw,noexec,nosuid,nodev)
sysfs on /sys type sysfs (rw,noexec,nosuid,nodev)
none on /sys/fs/fuse/connections type fusectl (rw)
none on /sys/kernel/debug type debugfs (rw)
none on /sys/kernel/security type securityfs (rw)
udev on /dev type devtmpfs (rw,mode=0755)
devpts on /dev/pts type devpts (rw,noexec,nosuid,gid=5,mode=0620)
tmpfs on /run type tmpfs (rw,noexec,nosuid,size=10%,mode=0755)
none on /run/lock type tmpfs (rw,noexec,nosuid,nodev,size=5242880)
none on /run/shm type tmpfs (rw,nosuid,nodev)
/dev/sdb1 on /data1 type ext4 (rw,usrquota,grpquota)
rpc_pipefs on /run/rpc_pipefs type rpc_pipefs (rw)
/dev/sdc1 on /data2 type ext4 (rw,usrquota,grpquota)
nfsd on /proc/fs/nfsd type nfsd (rw)
每个被装载的文件系统都对应一个vfsmount结构体,其各字段的意义通过1.1节已经非常明晰。
<include/linux/mount.h>
struct vfsmount {
	struct dentry *mnt_root;	/* root of the mounted tree */
	struct super_block *mnt_sb;	/* pointer to superblock */
	int mnt_flags;
};

挂载一个文件系统的flag有如下可选标志:

#define MNT_NOSUID	0x01 //禁止setuid
#define MNT_NODEV	0x02//挂载虚拟文件系统,无物理设备
#define MNT_NOEXEC	0x04//不允许程序执行
#define MNT_NOATIME	0x08//不更新access时间
#define MNT_NODIRATIME	0x10//不更新目录access时间
#define MNT_RELATIME	0x20//
#define MNT_READONLY	0x40	/* does the user want this to be r/o? */

#define MNT_SHRINKABLE	0x100
#define MNT_WRITE_HOLD	0x200

#define MNT_SHARED	0x1000	/* if the vfsmount is a shared mount */
#define MNT_UNBINDABLE	0x2000	/* if the vfsmount is a unbindable mount */
/*
 * MNT_SHARED_MASK is the set of flags that should be cleared when a
 * mount becomes shared.  Currently, this is only the flag that says a
 * mount cannot be bind mounted, since this is how we create a mount
 * that shares events with another mount.  If you add a new MNT_*
 * flag, consider how it interacts with shared mounts.
 */
#define MNT_SHARED_MASK	(MNT_UNBINDABLE)
#define MNT_PROPAGATION_MASK	(MNT_SHARED | MNT_UNBINDABLE)

#define MNT_INTERNAL	0x4000
#define MNT_LOCK_READONLY	0x400000

1.2.3 mount系统调用

要想对文件系统有所了解,就必须知道mount系统调用流程,并且在有了前面的相关数据结构以及数据结构体之间的关系,来看mount系统调用目标也会变的明确,也就是那些数据结构如何实例化。通常能够看到如下mount的常用用法:

mount      [OPTIONS]  [-o OPTS]    DEVICE                            NODE
mount   -t     proc                proc                               /proc
mount   -t     sysfs                sysfs                               /sys
mount   -t     ramfs               ramfs                              /home
mount   –t     ubifs               /dev/ubi4_0                         /config
mount   -t     nfs   -o   nolock    10.1.14.104:/workteam/shichaog/nfs     /mnt

这里有必要对上面进行适当的说明,前面提到的三种类型的文件系统,伪文件系统、存储介质文件系统、网络文件系统在上面均有示例,第一行是mount的常用用法,-t指定了挂载的文件系统类型,DEVICE是挂载的设备,这是必须存在的,而最后的NODE那一列是设备挂载点。

         mount系统调用的入口函数定义于fs/namespace.c文件中,其定义原型如下:

SYSCALL_DEFINE5(mount, char __user *, dev_name, char__user *, dir_name,
char __user *,type, unsigned long, flags, void __user *, data)

上述mount命令的常用用法的type字段对应系统调用的字段

         上面的type参数是挂载时指定的文件系统类型,如proc、sysfs、ubifs、ramfs、tmpfs、ubifs、yaffs2等,dev_name是要挂载的设备对应mount用法的DEVICE那一列。Flag是挂载挂载的标志,还是有必要来看一看,其调用流程。

图1.2.1 mount代码流程图

在上述流程图中,关键的工作由do_mount来完成,首先调用kern_path找到挂载点,然后根据挂载的标志,调用相应的挂载函数执行挂载,比如如果设置了MS_REMOUNT标志,则会调用do_remount来重新挂载文件系统,如果没有设置特殊的标志,则调用do_new_mount完成挂载工作,上面的挂载实例执行的都是这个函数。

查找挂载点的函数调用如下:

retval =kern_path(dir_name, LOOKUP_FOLLOW, &path);

其第一个参数是挂载点,第二个参数指定了如何查找,正确找到的情况下,会将查找的路径放在最后一个参数path里。

int kern_path(const char *name, unsigned int flags, struct path *path)
{
	struct nameidata nd;
	int res = do_path_lookup(AT_FDCWD, name, flags, &nd);
	if (!res)
		*path = nd.path;
	return res;
}

nameidata临时用于查找路径之用,

struct nameidata {
	struct path	path; //
	struct qstr	last; 
	struct path	root;
	struct inode	*inode; /* path.dentry.d_inode */
	unsigned int	flags; //搜索标志
	unsigned	seq;
	int		last_type;
	unsigned	depth;
	char *saved_names[MAX_NESTED_LINKS + 1];
};

其path和last成员的定义原型如下:

struct path {
	struct vfsmount *mnt;
	struct dentry *dentry;
};

qstr是“quickstring”缩写,存储了字符串长度值,而name存储了字符串名。

struct qstr {
	union {
		struct {
			HASH_LEN_DECLARE;
		};
		u64 hash_len;
	};
	const unsigned char *name;
};

路径查找的过程很繁琐,这一过程也不是最感兴趣的部分,在1.3.1有所涉及文件路径的相关操作,这里只需要知道查找到的路径信息将会存放在struct nameidata结构体里供后续使用。

图1.2.1do_new_mount流程

get_fs_type用于调用find_filesystem在全局链表file_systems里查找当前要挂载的设备的文件系统是否已经注册了,如果注册了则返回表示该类型的文件系统的struct file_system_type的对应实例,如果没有注册,这说明需要加载对应的文件系统模块,这通过request_module来完成,成功加载返回文件系统实例,如果都失败了,则返回-ENODEV错误号,一终止挂载工作。

vfs_kern_mount则调用特定的文件系统struct file_system_type的mount方法读取超级块。

do_add_mount将一个挂载添加到挂载的命名空间中。该函数首先对父挂载点进行检查以防止在同一个挂载点进行了两次的挂载工作。接下来调用attach_recursive_mnt完成核心的添加工作。

1.3 文件操作

1.3.1 文件打开

应用程序在读写文件或者设备时需要先使用open打开一个设备,对应调用的是fs/open.c文件中的open系统调用,其原型如下:

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
	if (force_o_largefile())
		flags |= O_LARGEFILE;

	return do_sys_open(AT_FDCWD, filename, flags, mode);
}

如果是64位系统而不是32位系统则会执行flags |= O_LARGEFILE;语句,这是因为64位系统默认选择了大文件配置选项。

图1.3.1打开文件系统调用代码流程

open调用正常的话将得到文件描述符,实际上就是一个整数,这个整数用于在task_struct->files->fd_array数组中索引对应的表示文件的file结构(fd_array数组元素就是表示file的结构),图0.1将这一关系表述的很清楚,所以首先调用get_unused_fd_flags为将要打开的文件找到一个空闲的文件描述符,通常进程打开的文件数量不超过资源设定值将获得一个空闲的文件描述符。

         上述的工作完了,就要查找文件的索引节点了,这正是do_filp_open的工作。do_file_open函数的意义如下:

dfd指示了文件打开的位置,如果在当前工作目录下打开,则会被设置为AT_FDCWD,pathname是文件的路径名称,op指示的是文件打开标志,最后的一个flags参数指示的是路径的查找策略。

fs/manei.c
struct file *do_filp_open(int dfd, struct filename *pathname,
		const struct open_flags *op, int flags)
{
	struct nameidata nd;
	struct file *filp;

	filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU);
	return filp;
}

path_openat的主要目的就是返回对应文件的struct file结构体,这一任务由path_openat完成,该函数首先调用path_init函数将当前目录或者根目录(这依赖于传递的是绝对路径/还是相对路径.)的信息存储在struct nameidata 类型的变量nd中,由该结构体可以知道文件路径(vfsmount和dentry)索引节点信息。

然后调用文件名解析函数link_path_walk在path_init找到的查找起始点nd中开始查找需要打开文件的structnameidata信息,如果成功,该函数返回值是零并且nd存储的就是要打开的文件的struct nameidata信息。其查找的过程是通过计算各路径分量的哈希值之和转换成一个最终的哈希值,并将值存储在nd->last(qstr)字段里。

         do_last函数的命名很形象,该函数用于处理打开一个文件的最后一步,核心工作就是将get_unused_fd_flags申请到的空闲的文件描述符的内容进行填充,即struct file 字段进行填充。

static int do_last(struct nameidata *nd, struct path *path,
		   struct file *file, const struct open_flags *op,
		   int *opened, struct filename *name)
{
//此处是找文件的信息而非找到文件查找其实目录的信息,所以该标志需要清除掉
	nd->flags &= ~LOOKUP_PARENT;
	nd->flags |= op->intent;
…
//判断是否是创建文件,如果创建文件则没有必要查找file内容,否则查找文件的struct file
if (!(open_flag & O_CREAT)) {
		…
//0,成功找到, 1:内存中没有,需要调用lookup_real,<0则容rcu-walk方式转到ref-walk方式。
		error = lookup_fast(nd, path, &inode);
		if (likely(!error))
			goto finish_lookup; 

如果返回值显示找到,则直接跳到finish_lookup查找结束。否则会跳转到retry_lookup标号,再一次尝试查找的工作由lookup_open实现,该函数先调用lookup_dcache在cache中查找,如果找不到则调用lookup_real在文件系统里查找,如果成功则将将路径结果存放到path参数里。

retry_lookup:
	error = lookup_open(nd, path, file, op, got_write, opened);
error = follow_managed(path, nd->flags);
inode = path->dentry->d_inode;

follow_managed根据传递进来的flag和前面查找到的路径名分三种情况进行处理:

l  Autofs,即flag设置了DCACHE_MANAGE_TRANSIT标志,用于处理文件传输时,不需要i_mutx锁。调用目录项的d_manage方法。

l  挂载点,即flag设置为DCACHE_MOUNTED,调用lookup_mnt查找到挂载点,如果找到将path信息存储在第一个参数path中。

l  处理自动挂载点,flag标志是DCACHE_NEED_AUTOMOUNT,调用follow_automount执行自动挂载。

接下来就到finish_lookup标号了,这是lookup_fast返回0和返回1的交汇点,如果处在rcu-walk情景下或者nd之前遇到挂载点过则执行path_to_nameidata,该函数的名称的意思很明显就是将参数path的相关值设置到nd(nameidata)里去。

if ((nd->flags & LOOKUP_RCU) || nd->path.mnt != path->mnt) {
		path_to_nameidata(path, nd);
	} else {
		save_parent.dentry = nd->path.dentry;
		save_parent.mnt = mntget(path->mnt);
		nd->path.dentry = path->dentry;
	}
	nd->inode = inode;

跟新完nd后,会调用complete_walk结束路径查找,如果进入complete_walk时是rcu-walk方式,则放弃rcu锁并使能nd->path,如果在路径查找时验证过最终的结果或者文件系统不支持,否则这里需要重新验证最终结果的有效性,此外,complete_walk还要处理跨文件系统情况,比如说在遇到挂载点、符号链接是绝对路径时,就可能跨文件系统。跨文件系统可能要重新验证目录项,即DCACHE_OP_WEAK_REVALIDATE标志如果被设置则调用目录项的d_weak_revalidate方法进行验证。

error= complete_walk(nd);

如果返回值是0,则说明成功了,到这里要根据查找的结果填充struct file结构体了,may_open首先是进行了参数和安全方面的检查,这包括索引节点是否真正的有意义,是否是链接文件(链接文件返回-ELOOP),如果是目录文件则必须有写权限,对于块设备或者字符设备,其设备节点标志是否挂载,fifo和sock文件则取消truncate标志,此外还有索引节点权限的检查等,这些检查正确才会对file结构体赋值,才会执行finish_open函数。

finish_open_created:
	error = may_open(&nd->path, acc_mode, open_flag);

如果权限检查通过,则将前面查找到的nd信息结果存储在 struct file这个表示打开文件的结构体中。finish_open完成一个文件的打开工作,其函数原型如下:

int finish_open(struct file *file, struct dentry *dentry,
		int (*open)(struct inode *, struct file *),
		int *opened)

其第三个参数open函数指针是打开一个文件的回调函数,如果没有指定,这里正是这种情况,该字段是null,则会调用文件系统的f_op->open()函数来填充file结构。

file->f_path.mnt = nd->path.mnt;
	error = finish_open(file, nd->path.dentry, NULL, opened);
opened:
	error = open_check_o_direct(file);
	error = ima_file_check(file, op->acc_mode);
out:
	if (got_write)
		mnt_drop_write(nd->path.mnt);
	path_put(&save_parent);
	terminate_walk(nd);
	return error;

接着是一个递归的返回struct file的过程,path_openat函数将该结构返回给do_filp_open,而do_filp_open则将找到的do_sys_open函数,该函数将调用fsnotify_open函数发送FSNOTIFY_EVENT_PATH消息,此外还要向表示进程的task_struct的文件数组项添加新打开的文件描述和表示文件的struct file指针,在图0.1中有该结构的拓扑。

1.3.2 文件读和写操作

图1.3.1 read系统调用流程

read的系统调用函数定义于fs/read_write.c文件里,该函数首先根据传递进来的文件描述符fd,找到对应的struct file对象,这个对象就是在open中千辛万苦才找到的表示文件的结构体。该函数是一个轻量级的文件查找函数,如果文件描述符表没有被共享,那么就不需要增加引用计数。该函数就是到图0.1中进程表示结构体task_struct中的fd数组中获取文件的表示对象struct file。fget是一个比这个开销更大的接口。file_pos_read获取文件指针的位置,该位置由struct file的f_pos指针指示,而该file的表示结构体在fget_light中已经获得了。

vfs_write是对写函数的一个封装,该函数首先会判断获得的file结构体表示的文件系统是否提供了read方法,这一方法和具体的文件系统是息息相关的,如果有则调用该方法,如果没有则调用do_sync_read完成实际读取文件过程。这一过程的代码如下:

if (file->f_op->read)
			ret = file->f_op->read(file, buf, count, pos);
		else
			ret = do_sync_read(file, buf, count, pos);

do_sync_read实际上是对filp->f_op->aio_read的封装,从该函数的命名来看是异步都方式,不过会进一步判断该值,如果io控制块被排队,则会调用wait_on_sync_kiocb等待io控制块操作结束。

VFS向下传递依次将经历特定文件系统(NFS、EXT4…)、页缓存(page cache)、通用块层(Generic Block Layer)、IO调度层(IO scheduler Layer)、块设备驱动层(Block Device Driver Layer)、块设备层(Block DeviceLayer);这些只有在后续文章分析了。

图1.3.1 write系统调用流程

可以看到文件写系统调用和读系统调用非常类似。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

虚拟文件系统 (VFS)-基于linux3.10 的相关文章

随机推荐

  • Go timer 是如何被调度的?

    hi 大家好 我是 haohongfan 本篇文章剖析下 Go 定时器的相关内容 定时器不管是业务开发 还是基础架构开发 都是绕不过去的存在 由此可见定时器的重要程度 我们不管用 NewTimer timer After 还是 timer
  • Qt实现 自定义矩阵布局

    前言 当界面需要同时展示多个项的时候 可能需要一个矩阵来填充数据 因为通常不知道数据项的多少 所以支持自定义行列就显得尤为重要 比如可能需要在一台电脑同时显示多个报表的数据 如果一直切换 因为无法比较各个报表的数据 难免不够直观 这种时候
  • Android代码通过包名调用系统卸载程序

    卸载指定包名的应用 param packageName public boolean uninstall String packageName boolean b checkApplication packageName Logger d
  • Mongodb 锁

    MMAP1存储引擎 collection锁 collection level concurrency control 在MMAP版本中 只提供了database的锁 既当一个用户对一个collection进行操作时 其他的collectio
  • 《Kubernetes故障篇:had untolerated taint node.kubernetes.io/not-ready》

    一 环境信息 操作系统 K8S版本 containerd版本 Centos7 6 v1 24 17 v1 6 12 二 背景信息 部署完K8S集群后 使用如下命令测试集群 进入pod中发现无法解析k8s默认域名和无法ping通外网 宿主机主
  • SWUST OJ#1103(数据结构之删除顺序表中指定区间的数据)

    目录 题目 思路 代码 题目 题目描述 建立顺序表L 将指定区间的数据从顺序表中删除 假设指定区间是合法数据 无序做合法性判断 测试数据为整型 输入 第一行是表长n 第二行是表中数据元素 第三行是闭区间 输出 删除以后的顺序表中的数据元素
  • Unable to find a single main class from the following candidates

    项目启动编译时报错 错误原因 当前项目下无法找到单个main方法 因为当前项目有两个main方法 有时可能有多个 此些main方法会列在 括号中 解决方式 把非启动类中的那些main方法注释掉 保证只有一个main方法
  • cocos自定义类 绑定到lua

    1 按照 frameworks cocos2d x tools tolua下的README mdown的指示一步一步做下去 务必要和文件中描述的一样 我已经被这个坑爹的东西坑的很惨了 检查这个有没有配置好的方法是 运行下那个genbindi
  • 循环依赖 三级缓存解决

    循环依赖的产生 A对象里面依赖着B对象 B对象依赖着A对象 流程 当A开始实例化A 然后初始化A 再初始化的A的时候 去实例化B 然后再初始化B 初始化B的时候在去实例化A 产生一个循环依赖 spring设置三级缓存来解决这个问题 Obje
  • 校招面试重点汇总之多线程(不多但都是高频面试题)

    一 进程和线程有什么区别 进程和线程都是操作系统中用来实现多任务的概念 但是它们之间有一些重要的区别 如下所述 1 定义方面 进程 进程是操作系统中分配资源的基本单位 是正在运行中的一个程序 一个进程可以包含多个线程 每个进程有自己独立的地
  • vscode检测到#include错误,请更新includePath。解决方法

    vscode检测到 include错误 请更新includePath 解决方法 最近电脑重做 重新安装了一边vscode 但是写代码的时候发现头文件会标错 显示无法找到 下面是我的解决办法 Linux 子系统 执行命令查看g 包含路径 g
  • Android Studio开发环境搭建及本地Gradle设置方法

    Android Studio开发环境搭建及本地Gradle设置方法 在进行Android应用程序开发时 Android Studio是一款非常强大的集成开发环境 IDE 它提供了丰富的工具和功能 帮助开发者轻松创建 测试和调试Android
  • java token生成规则_token的生成原理 使用方法!

    什么是token Token是服务端生成的一串字符串 以作客户端进行请求的一个令牌 当第一次登录后 服务器生成一个Token便将此Token返回给客户端 以后客户端只需带上这个Token前来请求数据即可 无需再次带上用户名和密码 基于 To
  • [解决办法]已经安装了数字证书,但是谷歌浏览器登录https协议的web系统时仍然提示证书不受信任...

    已经安装了数字证书 但是谷歌浏览器登录https协议的web系统时仍然提示证书不受信任 如下图 解决办法 1 单击Chrome浏览器右侧设置菜单 选择 设置 2 拖至页面下方 单击 显示高级设置 3 单击高级设置中的 HTTPS SSL 处
  • IDEA 热部署项目

    使用Idea 开发SpringBoot项目 修改完代码以后 要重新启动 Application 才可以看到效果 这样做开发效率肯定是大受影响的 可以通过热部署 热更新来实时加载更改 提高效率 1 引入热部署插件
  • 电脑蓝屏终止代码irql_一分钟教你看懂蓝屏代码,轻松解决电脑蓝屏问题

    电脑突然蓝屏的情况 想必很多朋友都遇到过吧 不知道你们是怎么解决的呢 重装系统 还是找专业人员维修呢 其实只要够看懂蓝屏代码 就能 对症下药 有些问题自己就能解决 今天小源就分享一些常见的蓝屏代码给大家 下面我们一起来看看吧 一 0X000
  • Python中的字典索引

    Python中的符合数据类型 字符串 列表和序列 它们用整数作为索引 如果你试图用其他的类型做索引 就会产生错误 gt gt gt list 1 2 3 gt gt gt list 0 1 gt gt gt list one Traceba
  • 【Linux】序列化和反序列化

    文章目录 定义 利用 Json 实现序列化反序列化 Json 的认识 Jsoncpp 库的下载与认识 实现序列化 实现反序列化 在网络编程中 直接使用 结构体 进行数据传输会出错 因为本质上socket无法传输结构体 我们只有将结构体装换为
  • 接口测试用例怎么写?一文1600字教你写一个优秀的接口测试的测试用例

    一 用例设计1 1 接口测试概念 接口测试 测试系统间接口的一种测试 测试的对象主要是接口 主要是测试外部系统与所测系统之间以及内部系统之间的交互点 2 接口测试方法 a 可以通过开发脚本代码进行测试 b 可以通过开源免费的接口调用调试工具
  • 虚拟文件系统 (VFS)-基于linux3.10

    引言 虚拟文件系统 VFS VirtualFileSystem 介于具体的文件系统和C库之间 其用提供一个统一的方法来操作文件 目录以及其它对象 其能够很好的抽象具体的文件系统 在linux上具体的文件系统主要分为三类 l 基于非易失性的存