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

引言

虚拟文件系统(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系统调用流程

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shichaog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值