欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

VFS内核源码学习(一)

程序员文章站 2022-05-09 23:40:52
...

VFS:为各种文件系统提供了一个通用的接口,它使得上层进程在进行与文件系统相关的操作时可以使用同一组系统调用,但是系统调用在内核中可以根据不同的文件系统执行不同的操作。在一个Linux操作系统中,存在多种的文件系统,例如ext2,ext3,ext4等,每种文件系统都有自己的组织方式,和操作方法,对于用户来说,不可能所有的文件系统都了解,所以在Linux中在应用程序和各种文件系统之间添加了一层称为虚拟文件系统的机制,在系统不同分区存在各种不同的文件系统,用户在操作这些文件系统的时候,会直接跟vfs打交道。vfs根据你操作实际文件系统,然后来进行适合该文件系统的相应的操作。

VFS内核源码学习(一)
文件系统的四对象:

超级块:
存放已经安装文件系统的有关信息,对基于磁盘的文件系统,这类对象通常对应于存放在磁盘上的文件系统控制块。

inode索引节点对象:
存放关于文件的一般信息,对基于磁盘的文件系统,这类对象通常对应于存放在磁盘上的文件控制块,每个索引节点对象都有一个索引节点号,这个节点号唯一标示了文件系统中的文件。

文件对象:
存放打开文件与进程之间进行交互的有关信息,这类信息仅当进程访问文件期存在于内核内存中。

目录项对象:
存放目录项与对应文件进行链接的有关信息,每个磁盘文件都以自己特有的方式将该类信息存在磁盘上。

VFS内核源码学习(一)
进程和VFS之间的交互

vfs数据结构:
内核超级块源代码:

struct super_block {
	struct list_head	s_list;		/* Keep this first *///指向超级块链表的指针
	//这样的结构来将super_block中的s_list链接起来,那么遍历到s_list之后,直接读取super_block这么长的一个内存块,就可以将这个
///super_block直接读进来!这样就很快捷方便!这也是为什么s_list必须放在第一个字段的原因。
	dev_t			s_dev;		/* search index; _not_ kdev_t *///设备标识符号
	//包含该具体文件系统的块设备标识符。例如,对于 /dev/hda1,其设备标识符为 0x301
	unsigned long		s_blocksize; // 以字节为单位块大小
	//
	unsigned long		s_old_blocksize; // 基于驱动程序中提到的以字节为单位的块大小
	unsigned char		s_blocksize_bits; //以位为单位的块大小
	unsigned char		s_dirt; //修改脏标志
	unsigned long long	s_maxbytes;	/* Max file size *///文件的最长长度
	struct file_system_type	*s_type; //文件系统类型 
	//要区分“文件系统”和“文件系统类型”不一样!一个文件系统类型下可以包括很多文件系统即很多的super_block
	struct super_operations	*s_op; //超级块的方法
	struct dquot_operations	*dq_op; .//磁盘限额处理方法
 	struct quotactl_ops	*s_qcop; // 磁盘限额管理方法
	struct export_operations *s_export_op; //网络文件系统使用的输出操作
	unsigned long		s_flags; //安装标志
	unsigned long		s_magic; //文件系统的魔数
	struct dentry		*s_root; //文件系统根目录的目录项对象
	struct rw_semaphore	s_umount; .//卸载所用的信号量
	struct semaphore	s_lock; // 超级快的信号量
	int			s_count; //引用计数器
	int			s_syncing; //对超级块索引节点进行同步的标志
	int			s_need_sync_fs; //对超级快已经安装的文件系统进行同步的标志
	atomic_t		s_active; //次级引用计数器
	void                    *s_security; //指向超级快安全数据结构的指针
	struct xattr_handler	**s_xattr; //指向超级块扩展属性结构的指针

	struct list_head	s_inodes;	/* all inodes */ //所用索引节点的链表
	struct list_head	s_dirty;	/* dirty inodes */ //改进型索引节点的链表
	struct list_head	s_io;		/* parked for writeback */ //等待被写入磁盘的索引节点的链表
	struct hlist_head	s_anon;		/* anonymous dentries for (nfs) exporting */ //
	struct list_head	s_files;  //文件对象的链表

	struct block_device	*s_bdev; //指向块设备驱动程序描述符的指针
	struct list_head	s_instances; //用于给定文件系统类型的超级快对象链表的指针
	struct quota_info	s_dquot;	/* Diskquota specific options */ //磁盘限额描述符

	int			s_frozen; //冻结文件系统时使用的标志
	wait_queue_head_t	s_wait_unfrozen; //进程挂起的等待队列,知道文件系统被冻结

	char s_id[32];				/* Informational name */ //包括超级快的块设备名称

	void 			*s_fs_info;	/* Filesystem private info *///指向特定系统的块设备名称

	/*
	 * The next field is for VFS *only*. No filesystems have any business
	 * even looking at it. You had been warned.
	 */
	struct semaphore s_vfs_rename_sem;	/* Kludge */当vfs通过目录重命名文件时使用的信号量

	/* Granuality of c/m/atime in ns.
	   Cannot be worse than a second */
	u32		   s_time_gran; //时间戳的粒度
};

inode节点保存实际数据的信息,称为元数据,例如:文件大小,设备标识符号,用户组标识符,文件模式,扩展属性,文件读取或者修改的时间戳,连接数量,指向存储该内容的磁盘区块的指针,文件分类等。
数据分成:元数据和数据本身。
inode 有两种:每个i_node节点的大小,一般是128字节或者256字节,inode节点的总数,在格式化时就给定,一般每2KB就设置一个inode。inode号是唯一的,表示不同的文件。当创建一个文件的时候就会分配一个inode。

struct inode数据结构:

struct inode {
	struct hlist_node	i_hash; //指向hash链表指针,用于inode的hash表
	struct list_head	i_list; //指向索引节点链表指针
	struct list_head	i_sb_list; 
	struct list_head	i_dentry; //指向目录项链表指针
	unsigned long		i_ino;  //索引节点号,每个inode都是唯一的
	atomic_t		i_count; //引用计数
	umode_t			i_mode; //文件权限标识
	unsigned int		i_nlink; //与该节点建立连接的文件数
	uid_t			i_uid; //文件拥有者标识
	gid_t			i_gid; //文件所在组标识
	dev_t			i_rdev; //实际的设备标识
	loff_t			i_size;  //inode所代表的文件大小
	struct timespec		i_atime; //最后一次访问时间
	struct timespec		i_mtime; //文件最后一次修改时间
	struct timespec		i_ctime; //inode最后一次修改时间
	unsigned int		i_blkbits; //快大小 位为单位
	unsigned long		i_blksize; //块大小字节为单位
	unsigned long		i_version;//版本号
	unsigned long		i_blocks; //文件所占块数
	unsigned short          i_bytes; //文件最后一个快的字节数
	unsigned char		i_sock; //
	spinlock_t		i_lock;	/* i_blocks, i_bytes, maybe i_size */
	struct semaphore	i_sem;
	struct rw_semaphore	i_alloc_sem;
	struct inode_operations	*i_op;
	struct file_operations	*i_fop;	/* former ->i_op->default_file_ops */
	struct super_block	*i_sb; //inode索引节点指向超级快的指针
	struct file_lock	*i_flock; //文件锁链表
	struct address_space	*i_mapping; //表示向谁请求页面
	struct address_space	i_data; //表示inode读写页面
#ifdef CONFIG_QUOTA
	struct dquot		*i_dquot[MAXQUOTAS]; //inode的读写限额
#endif
	/* These three should probably be a union */
	struct list_head	i_devices; //设备链表
	struct pipe_inode_info	*i_pipe; //指向管道文件
	struct block_device	*i_bdev; //指向块设备文件
	struct cdev		*i_cdev; //指向字符设备文件
	int			i_cindex; 

	__u32			i_generation;

#ifdef CONFIG_DNOTIFY
	unsigned long		i_dnotify_mask; /* Directory notify events */
	struct dnotify_struct	*i_dnotify; /* for directory notifications */
#endif

	unsigned long		i_state; //索引节点的状态标识 I_NEW,I_LOCK,I_FREEING
	unsigned long		dirtied_when;	/* jiffies of first dirtying */

	unsigned int		i_flags; //索引节点的安装标识

	atomic_t		i_writecount; //记录进程以刻写模式打开此文件
	void			*i_security;
	union {
		void		*generic_ip;
	} u;
#ifdef __NEED_I_SIZE_ORDERED
	seqcount_t		i_size_seqcount;
#endif
};

管理inode的链表:

inode_unused:将目前还没有使用的inode节点连接
inode_in_use :将目前正在使用的inode节点连接起来
super_block中的s_dirty:将所有修改过的inode连接起来,这个字段在super_block中
inode_hashtable:注意为了加快inode的查找速率,将正在使用的inode和脏inode也会放在inode_hashtable这样一个hash结构中,但是不同的inode的hash值可能相等,所以将hash值相等的这些inode通过这个i_hash字段连接。

目录项:目录也是一种文件,打开目录就是打开目录文件:

struct dentry {
	atomic_t d_count;  //引用计数
	unsigned int d_flags;		/* protected by d_lock *///目录项缓存标识
	spinlock_t d_lock;		/* per dentry lock */自旋锁
	struct inode *d_inode;		/* Where the name belongs to - NULL is与该目录项相关联 的inode
					 * negative */
	/*
	 * The next three fields are touched by __d_lookup.  Place them here
	 * so they all fit in a 16-byte range, with 16-byte alignment.
	 */
	struct dentry *d_parent;	/* parent directory */ 父目录的目录项
	struct qstr d_name; ///目录项名称

	struct list_head d_lru;		/* LRU list */最近未使用的目录项的链表
	struct list_head d_child;	/* child of parent list */目录项通过这个加入到父目录的d_subdirs中
	struct list_head d_subdirs;	/* our children *本目录的所有孩子目录链表头*/
	struct list_head d_alias;	/* inode alias list */一个有效的dentry必然与一个inode进行关联,但是一个inode可以对应多个dentry,因为一个文件可以被连接到其他文件,所以这个字段连接就属于自己的inode结构中的i_dentry链表中的
	unsigned long d_time;		/* used by d_revalidate *///重新变为有效的时间
	struct dentry_operations *d_op;//目录项操作
	struct super_block *d_sb;	/* The root of the dentry tree *///目录所属的超级快
	void *d_fsdata;			/* fs-specific data *///文件系统的私有数据
 	struct rcu_head d_rcu; //
	struct dcookie_struct *d_cookie; /* cookie, if any */ 
	struct hlist_node d_hash;	/* lookup hash list */	内核使用hashtable对dentry进行管理,dentey_hashtable是由list_head组成的链表,一个dentry创建之后。就通过d_hash连接进入对应的hash值的链表中
	int d_mounted; //安装在该目录的文件系统的数量,注意文件目录下可以有不同的文件系统
	unsigned char d_iname[DNAME_INLINE_LEN_MIN];	/* small names *///存放短文件的文件名称
};

一个有效的dentry结构必定有一个inode结构,这是因为一个目录项要么代表着一个文件,要么代表着一个目录,而目录实际上也是文件。所以,只要dentry结构是有效的,则其指针d_inode必定指向一个inode结构。但是inode却可以对应多个。

进程通过文件描述符操作文件,注意每个文件都有一个32位数字来代表下一个读写的字节位置,一般打开文件后,从0开始,Linux中的file结构体来保存打开的文件的位置,所以file称为打开的文件描述。file结构形成一个链表,称为系统打开文件表。这个结构体是针对一个文件设置的。
下面是内核中的数据结构描述:


struct file {
	struct list_head	f_list; //本进程所有打开的文件形成的链表
	struct dentry		*f_dentry; //与该文件相关的dentry
	struct vfsmount         *f_vfsmnt; //该文件在文件系统的中的安装点
	struct file_operations	*f_op; //文件操作,当进程打开文件的时候,这个文件的关联inode中的i_fop文件操作就会初始化这个f_op字段
	atomic_t		f_count; //引用计数 
	unsigned int 		f_flags; //f_flags打开文件时候指定的标识
	mode_t			f_mode; //文件的访问模式
	int			f_error; //写错误码
	loff_t			f_pos; //f_ops  目前文件的相对开头的偏移
	struct fown_struct	f_owner; //记录一个进程ID,以及当某些事发送的时候发送给该ID进程的信号
	unsigned int		f_uid, f_gid; //用户ID
	struct file_ra_state	f_ra; //组ID

	size_t			f_maxcount; //
	unsigned long		f_version; //版本号
	void			*f_security;

	/* 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 list_head	f_ep_links;
	spinlock_t		f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */
	struct address_space	*f_mapping;
};

f_flags,f_mode和f_pos代表的是当前操作这个文件的控制信息,因为对于一个文件,可以被多个进程同时打开,那么对于每个进程来说操作这个文件是异步的。

f_count:引用计数,当一个进程关闭已经打开的文件时,只是将对应的f_count减1,当f_count=0放入时候,才会真的区关闭它,对于dup和fork操作,都会使得f_count增加。

f_op:设计所有的文件的操作的结构体。例如:用户使用read,最终会调用file_operation中的读操作,而file_operations结构体是对于不同的文件系统不一定相同,里面一个重要的操作函数,release函数,当用户执行close时,其实在内核中执行release函数,这个函数仅仅将f_count减一,直到减为0,才真正关闭相应的文件。

对于正在使用和未使用的文件对象分别使用一个双向链表进行管理。

用户打开文件表:

struct files_struct {
        atomic_t count; //引用计数
        spinlock_t file_lock;     /* Protects all the below members.  Nests inside tsk->alloc_lock */自旋锁
        int max_fds; //当前文件对象的最大数量
        int max_fdset; //文件描述符的最大数
        int next_fd; //已分配的最大的文件描述符
        struct file ** fd;      /* current fd array */ 指向文件指针数组的指针,一般指向最后一个字段fd_dentry,当文件数超过NR_OPEN_DEFAULT时,就会重新分配一个数组,然后指向这个新的数组指针
        fd_set *close_on_exec; //执行exec时,需要关闭的文件描述符
        fd_set *open_fds; //指向打开的文件描述符的指针
        fd_set close_on_exec_init; //执行exec时,需要关闭的文件描述符初始化值
        fd_set open_fds_init; //文件描述符初值的集合
        struct file * fd_array[NR_OPEN_DEFAULT]; //文件对象指针的初始化数组
};

描述进程的一些信息:每个进程都有自己的根目录和当前的工作目录,内核使用struct fs_struct来记录这些信息,进程描述符中的fs字段便是指向该进程的fs_struct结构。

struct fs_struct {
 atomic_t count;
 rwlock_t lock;
 int umask;
 struct dentry * root, * pwd, * altroot;
 struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;
};

其中:
count:共享这个表的进程个数
lock:用于表中字段的读/写自旋锁
umask:当打开文件设置文件权限时所使用的位掩码
root:根目录的目录项
pwd:当前工作目录的目录项
altroot:模拟根目录的目录项(在80x86结构上始终为NULL)
rootmnt:根目录所安装的文件系统对象
pwdmnt:当前工作目录所安装的文件系统对象
altrootmnt:模拟根目录所安装的文件系统对象(在80x86结构上始终为NULL)

open的实现:


asmlinkage long sys_open(const char __user * filename, int flags, int mode)
{
	char * tmp;
	int fd, error;

#if BITS_PER_LONG != 32
	flags |= O_LARGEFILE;
#endif
//将文件名传送给内核
	tmp = getname(filename);
	//宏函数,获得错误码
	fd = PTR_ERR(tmp);
	//错误码是无效的
	if (!IS_ERR(tmp)) {
	//在当前进程的fd_array数据中找到一个合适的位置,并返回其索引
		fd = get_unused_fd();
		if (fd >= 0) {
		//执行打开文件的核心操作函数
			struct file *f = filp_open(tmp, flags, mode);
			error = PTR_ERR(f);
			if (IS_ERR(f))
				goto out_error;
				//fd_install函数将该文件对象赋值到fd_array数组的第fd个元素
			fd_install(fd, f);
		}
out:
		putname(tmp);
	}
	return fd;

out_error:
	put_unused_fd(fd);
	fd = error;
	goto out;
}

VFS内核源码学习(一)

read函数的实现:

asmlinkage ssize_t sys_read(unsigned int fd, char __user * buf, size_t count)
{
	struct file *file;
	ssize_t ret = -EBADF;
	int fput_needed;

	file = fget_light(fd, &fput_needed);
	if (file) {
		loff_t pos = file_pos_read(file);
		ret = vfs_read(file, buf, count, &pos);
		file_pos_write(file, pos);
		fput_light(file, fput_needed);
	}

	return ret;
}

read函数作用是根据文件描述符读取指定长度的数据到缓冲区buf中,该系统调用的实现涉及了内核对IO进行处理的各个层次,但是对于VFS层来说实现方法比较清晰。
在read系统调用对应的服务例程中,首先使用fget_light函数根据fd找到fd对应的file对象,再通过file_pos_read函数获取文件的其实偏移量,即文件对象的f_pos字段的值,接着通过vfs_read函数进行读操作,通过file_pos_write函数更新文件当前偏移量,通过fput_light函数释释放文件对象,最终返回vfs_read函数的返回值,该值则为实际读取数据的长度。
read服务例程中,最核心的函数即为vfs_read,他的主要工作是选择一个具体的读操作函数,如果当前文件对象操作函数集中的read钩子函数被实现(通常在驱动程序中实现),则调用它,否则使用内核默认的读函数do_sys_read。

ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
	ssize_t ret;

	if (!(file->f_mode & FMODE_READ))
		return -EBADF;
	if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read))
		return -EINVAL;
	if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
		return -EFAULT;

	ret = rw_verify_area(READ, file, pos, count);
	if (!ret) {
		ret = security_file_permission (file, MAY_READ);
		if (!ret) {
			if (file->f_op->read)
				ret = file->f_op->read(file, buf, count, pos);
			else
				ret = do_sync_read(file, buf, count, pos);
			if (ret > 0) {
				dnotify_parent(file->f_dentry, DN_ACCESS);
				current->rchar += ret;
			}
			current->syscr++;
		}
	}

	return ret;
}

事实上,do_sys_read函数在内部调用钩子函数aiio_read,该钩子函数一般指向内核实现的通用读函数generic_file_aio_read。这个通用函数已经不属于我们本文所述的VFS层实现范畴。

write函数的实现,在vfs文件系统中和read实现类似。

close函数:通过调用flush钩子函数将页缓存中的数据写回磁盘,释放该文件上的所有锁,通过fput函数释放该文件,最后返回0或者一个错误码。

参考文档:
http://edsionte.com/techblog/archives/tag/vfs

相关标签: vfs学习