1、虚拟文件系统概述
虚拟文件系统作用:虚拟文件系统是Linux文件系统中的一个抽象软件层;因为它的支持,众多不同的实际文件系统才能在Linux中共存,跨文件系统操作才能实现。通过虚拟文件系统,程序可以利用标准的Unix系统调用对不同的文件系统,甚至不同介质上的文件进行读写操作。VFS使得用户可以直接使用open()、read()、write()这一的系统调用而无须考虑具体文件系统和实际物理介质。Unix/Linux中的“一切皆是文件”的口号才能够得以实现。
文件系统抽象层:内核在它的底层文件系统接口上建立了一个抽象层。该抽象层使Linux能够支持各种文件系统,即便是它们在功能和行为上存在很大差别。VFS抽象层之所以能衔接各种文件系统,是因为它定义了所有文件系统都支持的、基本的、概念上的接口和数据结构。
Unix文件系统:
- 目录:文件通过目录组织起来。文件目录好比一个文件夹,用来容纳相关文件。在Unix中,目录属于普通文件,它列出包含在其中的所有文件。由于VFS把目录当做文件对待,所以可以对目录执行和文件相同的操作。
- inode:Unix将文件的相关信息和文件本身两个概念加以区分。例如访问控制权限、大小、拥有者、创建时间等信息。文件相关信息有时被称作文件的元数据,被存储在一个单独的数据结构中,该结构被称为索引节点(inode),它其实index node的缩写。
- 超级块:超级块是一种包含文件系统信息的数据结构。文件系统的控制信息存储在超级块中。
虚拟文件系统位于内核的架构图:
2、虚拟文件系统对象及其数据结构
文件系统是特殊的数据分层存储结构,它包含文件、目录和相关的控制信息。为了描述 这个结构, VFS采用面向对象的设计思路。将其分为不同的对象,并提供了对象的操作方式:
- 超级块对象:存放系统中已安装文件系统的信息。
- 索引节点对象:文件系统处理文件所需要的所有信息都保存在索引节点inode中。inode代表的是物理意义上的文件,记录的是物理上的属性。比如 inode号、文件大小、访问权限、修改日期、数据的位置等。索引节点和文件一一对应,它跟文件内容一样,都会被持久化存储到磁盘中。
- 目录项对象:每个文件除了一个struct inode结构体外,还要一个目录项struct dentry结构。描述的是文件逻辑上的属性,没有对应的磁盘数据结构,目录项是由内核维护的一个内存数据结构,根据字符串形式的路径名现场创建。记录文件的名字、索引节点指针以及与其他目录项的关联关系。多个关联的目录项,就构成了文件系统的目录结构。
- 文件对象:存放打开文件与进程之间进行交互的有关信息。
超级块是对一个文件系统的描述;索引节点inode是对一个文件物理属性的描述;目录项dentry是对一个文件逻辑属性的描述;文件file是对当前进程打开的文件的描述。一个进程和文件系统相关的信息是由fs_struct来描述的(如当前正在执行的文件和当前工作目录),一个进程打开的文件集是由files_struct来描述的,而每个打开的文件是由file结构描述的。
文件系统对象在磁盘中的位置关系图:
3、超级块数据结构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; /* 活动引用计数 */
void *s_security; /* 安全模块 */
const struct xattr_handler **s_xattr; /* 扩展的属性操作 */
struct list_head s_inodes; /* all inodes inodes 链表 */
struct hlist_bl_head s_anon; /* anonymous dentries for(nfs)exporting匿名目录项 */
struct list_head __percpu *s_files;
struct list_head s_files; /* 被分配文件链表 */
struct list_head s_mounts; /* list of mounts; _not_ for fs use */
struct list_head s_dentry_lru; /* unused dentry lru 未被使用目录项链表 */
int s_nr_dentry_unused; /* # of dentry on lru 链表中目录项的数目 */
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; /* 存储磁盘信息 */
struct hlist_node s_instances; /* 该类型文件系统 */
struct quota_info s_dquot; /* Diskquota specific options 限额相关选项 */
struct sb_writers s_writers;
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; /* 安装权限 */
u32 s_time_gran; /* 时间戳粒度 */
struct mutex s_vfs_rename_mutex; /* Kludge */
char *s_subtype; /* 子类型名称 */
char __rcu *s_options; /* 已存安装选项 */
const struct dentry_operations *s_d_op; /* default d_op for dentries */
int cleancache_poolid;
struct shrinker s_shrink; /* per-sb shrinker handle */
atomic_long_t s_remove_count;
int s_readonly_remount;
};
超级块的操作:
struct super_operations {
/* 给定的超级块下创建和初始化一个新的索引节点对象 */
struct inode *(*alloc_inode)(struct super_block *sb);
/* 释放给定的索引节点 */
void (*destroy_inode)(struct inode *);
/* 当索引节点被修改时,执行函数更新日志文件系统 */
void (*dirty_inode) (struct inode *);
/* 将给定的索引几点写入磁盘, wait表示是否需要同步操作*/
int (*write_inode) (struct inode *, int wait);
/* 最后一个索引节点的引用被释放后,VFS会调用该函数。进行删除节点 */
void (*drop_inode) (struct inode *);
/* 删除指定节点 */
void (*delete_inode) (struct inode *);
/* 卸载文件系统时,由VFS调用,用来释放超级块。调用者必须一直持有s_lock锁 */
void (*put_super) (struct super_block *);
/* 用给定的超级块更新磁盘上的超级快,VFS通过该函数对内存中的超级快和磁盘中的超级快进行同步,调用者必须一直持有s_lock锁 */
void (*write_super) (struct super_block *);
/* 文件系统的数据元与磁盘上的文件系统同步。wait指定是否同步 */
int (*sync_fs)(struct super_block *sb, int wait);
/* */
int (*freeze_fs) (struct super_block *);
int (*unfreeze_fs) (struct super_block *);
/* 获取文件系统状态,指定文件系统相关的统计信息放在statfs中 */
int (*statfs) (struct dentry *, struct kstatfs *);
/* 指定新的安装选项重新安装文件系统时调用,调用者必须一直持有s_lock锁 */
int (*remount_fs) (struct super_block *, int *, char *);
/* 释放索引节点,并清空包含相关数据的所有页面 */
void (*clear_inode) (struct inode *);
/* 中断安装操作,该函数被网络文件系统使用,如NFS */
void (*umount_begin) (struct super_block *);
int (*show_options)(struct seq_file *, struct vfsmount *);
int (*show_stats)(struct seq_file *, struct vfsmount *);
#ifdef CONFIG_QUOTA ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
#endif int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t);
};
4、索引节点数据结构inode
索引节点对象包含了内核在操作文件或目录时需要的全部信息。索引节点必须在内存中创建,以便于文件系统使用。一个索引节点代表文件系统中的一个特殊文件,它也可以是设备或管道这样的特殊文件。
struct inode
{
/**********描述索引节点高速缓存管理的域****************/
struct list_head i_hash; /*指向哈希链表的指针*/
struct list_head i_list; /*指向索引节点链表的指针*/
struct list_head i_dentry;/*指向目录项链表的指针*/
struct list_head i_dirty_buffers;
struct list_head i_dirty_data_buffers;
/**********描述文件信息的域****************/
unsigned long i_ino; /*索引节点号*/
kdev_t i_dev; /*设备标识号 */
umode_t i_mode; /*文件的类型与访问权限 */
nlink_t i_nlink; /*与该节点建立链接的文件数 */
uid_t i_uid; /*文件拥有者标识号*/
gid_t i_gid; /*文件拥有者所在组的标识号*/
kdev_t i_rdev; /*实际设备标识号*/
off_t i_size; /*文件的大小(以字节为单位)*/
unsigned long i_blksize; /*块大小*/
unsigned long i_blocks; /*该文件所占块数*/
time_t i_atime; /*文件的最后访问时间*/
time_t i_mtime; /*文件的最后修改时间*/
time_t i_ctime; /*节点的修改时间*/
unsigned long i_version; /*版本号*/
struct semaphore i_zombie; /*僵死索引节点的信号量*/
/***********用于索引节点操作的域*****************/
struct inode_operations *i_op; /*索引节点的操作*/
struct super_block *i_sb; /*指向该文件系统超级块的指针 */
atomic_t i_count; /*当前使用该节点的进程数。计数为 0,表明该节点可丢弃或被重新使用 */
struct file_operations *i_fop; /*指向文件操作的指针 */
unsigned char i_lock; /*该节点是否被锁定,用于同步操作中*/
struct semaphore i_sem; /*指向用于同步操作的信号量结构*/
wait_queue_head_t *i_wait; /*指向索引节点等待队列的指针*/
unsigned char i_dirt; /*表明该节点是否被修改过,若已被修改,则应当将该节点写回磁盘*/
struct file_lock *i_flock; /*指向文件加锁链表的指针*/
struct dquot *i_dquot[MAXQUOTAS]; /*索引节点的磁盘限额*/
/************用于分页机制的域**********************************/
struct address_space *i_mapping; /* 把所有可交换的页面管理起来*/
struct address_space i_data;
/**********以下几个域应当是联合体****************************************/
struct list_head i_devices; /*设备文件形成的链表*/
struct pipe_inode_info i_pipe; /*指向管道文件*/
struct block_device *i_bdev; /*指向块设备文件的指针*/
struct char_device *i_cdev; /*指向字符设备文件的指针*/
/*************************其他域***************************************/
unsigned long i_dnotify_mask; /* Directory notify events */
struct dnotify_struct *i_dnotify; /* for directory notifications */
unsigned long i_state; /*索引节点的状态标志*/
unsigned int i_flags; /*文件系统的安装标志*/
unsigned char i_sock; /*如果是套接字文件则为真*/
atomic_t i_writecount; /*写进程的引用计数*/
unsigned int i_attr_flags; /*文件创建标志*/
__u32 i_generation /*为以后的开发保留*/
}
struct address_space {
struct inode *host; // 所在的inode 以便于获取文件元信息
struct xarray i_pages; // 文件对应的内存页
gfp_t gfp_mask; // 内存类型
atomic_t i_mmap_writable; // VM_SHARED映射计数
struct rb_root_cached i_mmap; // mmap私有和共享映射的树结构
struct rw_semaphore i_mmap_rwsem;
unsigned long nrpages; // 文件大小对应的内存页数量
unsigned long nrexceptional;
pgoff_t writeback_index; //回写由此开始
const struct address_space_operations *a_ops; // 地址空间操作
unsigned long flags; // 错误标识位
errseq_t wb_err; //
spinlock_t private_lock;
struct list_head private_list;
void *private_data;
} __attribute__((aligned(sizeof(long)))) __randomize_layout;
struct address_space_operations {
int (*writepage)(struct page *page, struct writeback_control *wbc); // 回写一页
int (*readpage)(struct file *, struct page *); //读取一页数据到内存中
int (*writepages)(struct address_space *, struct writeback_control *); // 回写脏页
int (*set_page_dirty)(struct page *page); // 标记脏页
int (*readpages)(struct file *filp, struct address_space *mapping,
struct list_head *pages, unsigned nr_pages);
int (*write_begin)(struct file *, struct address_space *mapping,
loff_t pos, unsigned len, unsigned flags,
struct page **pagep, void **fsdata);
int (*write_end)(struct file *, struct address_space *mapping,
loff_t pos, unsigned len, unsigned copied,
struct page *page, void *fsdata);
sector_t (*bmap)(struct address_space *, sector_t);
void (*invalidatepage) (struct page *, unsigned int, unsigned int);
int (*releasepage) (struct page *, gfp_t);
void (*freepage)(struct page *);
ssize_t (*direct_IO)(struct kiocb *, struct iov_iter *iter);
int (*migratepage) (struct address_space *,
struct page *, struct page *, enum migrate_mode);
bool (*isolate_page)(struct page *, isolate_mode_t);
void (*putback_page)(struct page *);
int (*launder_page) (struct page *);
int (*is_partially_uptodate) (struct page *, unsigned long,
unsigned long);
void (*is_dirty_writeback) (struct page *, bool *, bool *);
int (*error_remove_page)(struct address_space *, struct page *);
int (*swap_activate)(struct swap_info_struct *sis, struct file *file,
sector_t *span);
void (*swap_deactivate)(struct file *file);
};
真正的将文件与磁盘等存储设备的交互由谁来做呢?write一份数据是怎么从内存写回磁盘,而又如何从磁盘读数据到内存呢?这就是address_space主要需要处理的工作,address_space主要用于处理内存到后端设备之间的数据同步。
索引节点操作:
struct inode_operations {
/* 创建一个新的索引节点 */
int (*create) (struct inode *,struct dentry *,int, struct nameidata *);
/* 在特定目录中寻找索引节点,该索引节点要对应于denrty中给出的文件名 */
struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);
/* 创建一个硬链接,dentry参数指定硬链接名,链接对象是dir中的old_dir */
int (*link) (struct dentry *,struct inode *,struct dentry *);
/* 删除一个硬链接 */
int (*unlink) (struct inode *dir,struct dentry *dentry);
/* 创建一个软链接 */
int (*symlink) (struct inode *dir,struct dentry *dentry,const char *sysname);
/* 创建一个文件夹,最后为一个模式*/
int (*mkdir) (struct inode *,struct dentry *,int mode);
int (*rmdir) (struct inode *,struct dentry *);
/* 创建特殊文件,文件在dir目录中,其目录项为dentry,关联的设备为rdev,mode设置权限 */
int (*mknod) (struct inode *dir,struct dentry *dentry,int mode,dev_t rdev);
/* 重命名 */
int (*rename) (struct inode *, struct dentry *,
struct inode *, struct dentry *);
/* 拷贝数据到特定的缓冲buffer中。 */
int (*readlink) (struct dentry *, char *,int buflen);
/* 查找执行的索引节点存储在nameidata中 */
void * (*follow_link) (struct dentry *, struct nameidata *);
/* 清除查找后的结果 */
void (*put_link) (struct dentry *, struct nameidata *, void *);
/* 修改文件的大小 */
void (*truncate) (struct inode *);
/* 检查文件是否允许特定的访问模式 */
int (*permission) (struct inode *, int);
/* 通知发生了改变事件,一般被notify_change()调用 */
int (*setattr) (struct dentry *, struct iattr *);
/* 在通知索引节点需要从磁盘中更新时,VFS会调用该函数,扩展属性允许key/value这样的一对值与文件关联 */
int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);
/* 给dentry指定的文件设置扩展属性。属性名为name,值为value */
int (*setxattr) (struct dentry *, const char *,const void *,size_t,int);
/* 向value中拷贝给定文件的扩展属性name对应的数值 */
ssize_t (*getxattr) (struct dentry *, const char *, void * value, size_t);
/* 将指定文件的所有属性拷贝到一个缓冲列表 */
ssize_t (*listxattr) (struct dentry *, char *, size_t);
/* 删除指定属性 */
int (*removexattr) (struct dentry *, const char *);
void (*truncate_range)(struct inode *, loff_t, loff_t);
long (*fallocate)(struct inode *inode, int mode, loff_t offset,
loff_t len);
int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start,
u64 len);
};
硬链接与软连接的不同:
(1)软硬链接实现的原理不同
硬链接是建立一个目录项,包含文件名和文件的inode,但inode是原来文件的inode号,并不建立其所对应得数据。所以硬链接并不占用inode。软连接也创建一个目录项,也包含文件名和文件的inode,但它的inode指向的并不是原来文件名所指向的数据的inode,而是新建一个inode,并建立数据,数据指向的是原来文件名,所以原来文件名的字符数,即为软连接所占字节数。
(2)软硬链接所能创建的目标有区别
因为每个分区各有一套不同的inode表,所以硬链接不能跨分区创建而软连接可以,因为软连接指向的是文件名,所以软链接是可以跨文件系统的,甚至目标文件被删除了,链接文件还是在的,只不过指向的文件找不到了而已。
(3)硬链接不能指向目录
如果说目录有硬链接那么可能引入死循环,但是你可能会疑问软连接也会陷入循环啊,答案当然不是,因为软连接是存在自己的数据的,可以查看自己的文件属性,既然可以判断出来软连接,那么自然不会陷入循环,并且系统在连续遇到8个符号连接后就停止遍历。但是硬链接可就不行了,因为他的inode号一致,所以就判断不出是硬链接,所以就会陷入死循环了。
5、目录项数据结构dentry
dentry结构是实际文件系统的目录项在虚拟文件系统VFS中的对应物,实质上就是目录缓冲区,与前面的两个对象不同,目录项对象没有对应的磁盘数据结构。当系统访问一个具体文件时,会根据这个文件在磁盘上的目录在内存中创建一个dentry结构,它除了要存放文件目录信息之外,还要存放有关文件路径的一些动态信息。
struct dentry {
atomic_t d_count; /*使用标记*/
unsigned int d_flags; /* 记录目录项被引用次数的计数器 */
spinlock_t d_lock; /* 目录项的标志 */
int d_mounted;
struct inode *d_inode; /* 与文件名相对应的inode */
struct hlist_node d_hash; /* 目录项形成的散列表 */
struct dentry *d_parent; /* 指向父目录项的指针 */
struct qstr d_name; //包含文件名的结构,目录名称
struct list_head d_lru; /* 已经没有用户使用的目录项的链表 */
union {
struct list_head d_child; /* 父目录的子目录项形成的链表 */
struct rcu_head d_rcu;
} d_u;
struct list_head d_subdirs; /* i节点别名的链表 */
struct list_head d_alias; /* inode alias list */
unsigned long d_time; /* used by d_revalidate */
const struct dentry_operations *d_op; //指向dentry操作函数集的指针
struct super_block *d_sb; /* 目录树的超级块,即本目录项所在目录树的根 */
void *d_fsdata; /* 文件系统的特定数据 */
unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* 短文件名 */
};
struct qstr {
unsigned int hash; //文件名
unsigned int len; //文件名长度
const unsigned char *name; //散列值
};
dentry有两个特殊的成员变量d_lru和d_hash。这其实关联于两张目录缓存表,该表保存的是一系列已分配过的目录缓存池,用于文件操作中快速的查找和使用。结构中的域d_count记录了本dentry被访问的次数。当某个dentry的d_count的值为0时,就意味着这个dentry结构已经没有用户使用了,这时这个dentry会被d_lru链入一个由系统维护的另一个队列dentry_unused中。由于内核从不删除曾经建立的dentry,于是同一个目录被再次访问时,其相关信息就可以直接由dentry获得,而不必重复访问存储文件系统的外存。
- 引用为 0:一个在散列表中的 dentry 变成没有人引用了,就会被加到 LRU 表中去;
- 再次被引用:一个在 LRU 表中的 dentry 再次被引用了,则从 LRU 表中移除;
- 分配:当 dentry 在散列表中没有找到,则从 Slub 分配器中分配一个;
- 过期归还:当 LRU 表中最长时间没有使用的 dentry 应该释放回 Slub 分配器;
- 文件删除:文件被删除了,相应的 dentry 应该释放回 Slub 分配器;
- 结构复用:当需要分配一个 dentry,但是无法分配新的,就从 LRU 表中取出一个来复用。
目录对象有三种有效状态:
- 被使用:对应一个有效的索引节点,并且d_count大于0,基本不会被丢弃。
- 未被使用:对应一个有效的索引节点,但是d_count为0,可以丢弃。
- 负状态:没有对应的有效索引节点,因为索引节点已经被删除了。但是目录项仍然保留,以便快速解析以后的路径查询。
页目录缓存:为了避免每次操作都要遍历整个目录树,内核将目录对象缓存在目录项缓存(dcache)中
- “被使用的”目录项链表:通过索引节点的i_dentry项链接相关的索引节点。连一个索引节点可能有多个链接,可能有多个目录项对象。
- “最近被使用的”双向链表:包含未被使用的和负状态的目录项对象。插入较多。链头节点的数目比链尾节点的数据要新。
- 散列表和相应的散列函数用来快速的将给定路径解析为相关目录项对象。
目录项操作:
struct dentry_operations {
/* 判断目录项是否有效 */
int (*d_revalidate)(struct dentry *, struct nameidata *);
/* 生成一个散列值 */
int (*d_hash) (struct dentry *, struct qstr *);
/* 比较两个文件名 */
int (*d_compare) (struct dentry *, struct qstr *, struct qstr *);
/* 删除count为0的dentry */
int (*d_delete)(struct dentry *);
/* 释放一个dentry对象 */
void (*d_release)(struct dentry *);
/* 丢弃目录项对应的inode */
void (*d_iput)(struct dentry *, struct inode *);
char *(*d_dname)(struct dentry *, char *, int);
};
6、文件数据结构file
文件对象表示进程已打开的文件,文件对象是已打开的文件在内存中的表示。已经有索引节点对象和目录项对象,之所以还需要文件对象,是因为同一个文件可以由多个进程打开,所以同一个文件可能存在多个对应的文件对象,文件对象不唯一,索引节点和目录项对象是唯一的。一个文件的索引节点和目录项对象是整个文件系统所拥有的资源,所有进程都可以访问,而文件对象是单个进程所拥有的,其他进程不能访问。
struct file
{
struct list_head f_list; /*所有打开的文件形成一个链表*/
struct dentry *f_dentry; /*指向相关目录项的指针*/
struct vfsmount *f_vfsmnt; /*指向 VFS 安装点的指针*/
struct file_operations *f_op; /*指向文件操作表的指针*/
mode_t f_mode; /*文件的打开模式*/
loff_t f_pos; /*文件的当前位置*/
unsigned short f_flags; /*打开文件时所指定的标志*/
unsigned short f_count; /*使用该结构的进程数*/
unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;/*预读标志、要预读的最多页面数、上次预读后的文件指针、预读的字节数以及预读的页面数*/
int f_owner; /* 通过信号进行异步 I/O 数据的传送*/
unsigned int f_uid, f_gid; /*用户的 UID 和 GID*/
int f_error; /*网络写操作的错误码*/
unsigned long f_version; /*版本号*/
void *private_data; /* tty 驱动程序所需 */
};
文件操作:
struct file_operations {
struct module *owner;
/* 更新偏移纸指针,由系统调用lleek*(调用它) */
loff_t (*llseek) (struct file *, loff_t, int);
/* 从给定文件的offset偏移处读取conut字节的数据到buf中,同时更新文件指针,一般由read进行调用 */
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
/* 从给定的buf中取出conut字节的数据,写入给定文件的offset偏移处,同时更新文件指针 */
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
/* 返回目录列表中的下一个目录,由系统调用readdir()调用它 */
int (*readdir) (struct file *, void *, filldir_t);
/* 函数睡眠等待给定文件活动,由系统调用poll()调用它 */
unsigned int (*poll) (struct file *, struct poll_table_struct *);
/* 用来 */
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
/* 将给定的文件映射到指定的地址空间上。由系统调用mmap()调用它 */
int (*mmap) (struct file *, struct vm_area_struct *);
/* 创建一个新的文件对象,并将其和对应的索引节点对象关联起来 */
int (*open) (struct inode *, struct file *);
/* 更新一个文件相关信息 */
int (*flush) (struct file *);
/* 当文件最后一个引用被注销时,该函数会被VFS调用,具体功能由文件系统决定 */
int (*release) (struct inode *, struct file *);
/* 将给定的所有缓存数据写回磁盘,由系统调用fsync()调用它 */
int (*fsync) (struct file *, struct dentry *, int datasync);
/* 打开或关闭异步I/O的通告信号 */
int (*fasync) (int, struct file *, int);
/* 给指定文件上锁 */
int (*lock) (struct file *, int, struct file_lock *);
/* 从给定文件中读取数据,并将其写入由vector描述的count个缓冲中去,同时增加文件的偏移量。由系统调用readv()调用它 */
ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
/* 由给定vector描述的count个缓冲中的数据写入到由file指定的文件中去,同时减小文件的偏移量。由系统调用writev()调用它 */
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
/* 从一个文件向另外一个文件发送数据 */
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);
};
7、与文件系统相关的数据结构
file_system_type结构用于描述具体的文件系统的类型信息,比如ext3、ext4或UDF。
vfsmount结构用来描述一个安装文件系统的实例;当文件系统被实际安装时,将有一个vfsmount结构体在安装点被创建。
安装点、超级块和文件系统的关系:
struct file_system_type {
const char *name; /*文件系统的名字*/
struct subsystem subsys; /*sysfs子系统对象*/
int fs_flags; /*文件系统类型标志*/
/*在文件系统被安装时,从磁盘中读取超级块,在内存中组装超级块对象*/
struct super_block *(*get_sb) (struct file_system_type*,
int, const char*, void *);
void (*kill_sb) (struct super_block *); /*终止访问超级块*/
struct module *owner; /*文件系统模块*/
struct file_system_type * next; /*链表中的下一个文件系统类型*/
struct list_head fs_supers; /*具有同一种文件系统类型的超级块对象链表*/
};
struct vfsmount
{
struct list_head mnt_hash; /*散列表*/
struct vfsmount *mnt_parent; /*父文件系统*/
struct dentry *mnt_mountpoint; /*安装点的目录项对象*/
struct dentry *mnt_root; /*该文件系统的根目录项对象*/
struct super_block *mnt_sb; /*该文件系统的超级块*/
struct list_head mnt_mounts; /*子文件系统链表*/
struct list_head mnt_child; /*子文件系统链表*/
atomic_t mnt_count; /*使用计数*/
int mnt_flags; /*安装标志*/
char *mnt_devname; /*设备文件名*/
struct list_head mnt_list; /*描述符链表*/
struct list_head mnt_fslink; /*具体文件系统的到期列表*/
struct namespace *mnt_namespace; /*相关的名字空间*/
};
8、与进程相关的数据结构
系统中每个进程都有自己的一组打开的文件。如:根文件系统、当前工作目录、安装点等。其中file_struct、fs_struct、namespace将VFS层和系统的进程紧密结合在一起。
file_struct结构记录打开的文件集,即所有与该进程相关的文件信息(如打开的文件和文件描述符),进程描述符task_struct的files域指向该结构。
fs_struct结构记录进程和文件系统相关的信息,如当前正在执行的文件、进程当前工作目录和根目录,进程描述符task_struct的fs域指向该结构。
namespace结构记录每一个进程在系统中都看到唯一的安装文件系统(唯一的根目录,唯一的文件系统层次结构),进程描述符task_struct的mmt_namespace域指向该结构。
struct files_struct {//打开的文件集
atomic_t count; /*结构的使用计数*/
struct fdtable *fdt; /* 指向其它fd表的指针*/
struct fdtable fdtab; /* 基fd表 */
spinlock_t file_lock; /* 单个文件的锁*/
int next_fd; /* 缓存下一个可用的fd */
struct embedded_fd_set close_on_exec_init; /* exec()时关闭的文件描述符链接 */
struct embedded_fd_set open_fds_init; /* 打开的文件描述符链表 */
struct file *fd_aray[NR_OPEN_DEFAULT]; /* 缺省的文件数组对象 */
};
struct fs_struct {//建立进程与文件系统的关系
int users; /* 用户数目 */
rwlock_t lock; /* 保护该结构体的锁 */
int umask; /* 掩码 */
int int_exec; /* 当前正在执行的文件 */
struct path root; /* 根目录路径 */
struct path pwd; /*当前工作目录的路径 */
};
struct mmt_namespace{
atomic_t count; /* 结构的使用计数 */
struct vfsmount *root; /* 根目录的安装点对象 */
struct list_head list; /* 安装点链表 */
wait_queue_head_t poll; /* 轮询的等待队列 */
int event; /* 事件计数 */
}
9、打开创建文件操作
用户空间的open函数在内核里面的入口函数是sys_open,然后调用do_sys_open来完成具体的工作,do_sys_open具体实现:
(1)找到一个本进程没有使用的文件描述符fd
- 在当前进程打开的文件位图表中,找到第一个为0的位,返回这个位在位图表里面的下标,这个下标就是将用分配的未使用的文件描述符fd
- 把当前进程的文件表扩展一个文件(即尝试添加一个struct file到当前进程的文件列表中),进程task_struct-> files_struct-> fd_array[NR_OPEN_DEFAULT]是一个struct file 数组,而NR_OPEN_DEFAULT在64位系统中等于64(因为一般进程打开的文件数大多都用这个数组就可以直接放下了),如果扩展操作导致当前进程的这个存放struct file的数组放不下了,如要装第65个struct flie结构体,那么将重新分配一片内存区专门用来存放struct file结构体,并且这个内存区的大小为128个struct file结构体,然后将当前进程的task_struct->files_struct->fdtable->fd指针指向这片内存的首地址,然后把之前数组里面的内容复制到这片内存区里面来。下次添加如果超过了128个了,则分配256个大小直到256个也装满,超过256则分配512,依次类推,总是2的幂次方,且会把之前的复制到新分配的内存里面去。注意:这里只是更新了进程的这个file table,新的进程描述符对应的struct file还没有生成进去。
- 设置进程的文件位图中新分配的这个文件描述符位为(1)中找到的下标,并更新下一次该分配的进程描述符起点
(2)分配一个全新的struct file结构体
Struct file =kmem_cache_zalloc(filp_cachep, GFP_KERNEL);
(3)根据传人的pathname查找或建立对应的dentry并设置此dentry对应的inode。内核做这个事情借助于一个nameidata数据结构
- 如果pathname中第一个字符是“/”,那么说明使用绝对路径,设置nameidata为更目录对应的dentry和当前目录的inode,mount点等
- 如果不是“/”,则使用相对路径,设置nameidata为当前目录对应的dentry,inode,mount点等
- 一层一层往下查找,直到找到最终的那个文件或者目录分量,注意:如”/usr/bin/make”,先找“/”(这是3.1就做了的),再找“/”下面的usr,再找bin,最后找make。一层一层的往下找,依次会找到usr,bin,最后到了”make”。
这里细说一下第一层怎么在“/“下面找到”usr“的:
第一层查找先找“/”下面的usr对应的dentry,内核通过“/”对应的dentry和”usr”字符串两个参数进行hash运算获取一个dentry的链表,然后逐个看这个链表里面有没有parent dentry为“/”对应的dentry的,以及dentry对应的名字的hash值是否与“usr”对应的hash值相同。
前面条件都满足这里再看一下parent dentry是否有DCACHE_OP_COMPARE标识,如果有此标识且文件系统实现了dentry->dentry_operations->d_compare函数,那么就调用文件系统的这个函数来判断。如果条件都符合,那么说明内存中usr对应的这个dentry是存在的,如果这个dentry->d_flags中包含DCACHE_OP_REVALIDATE,那么就会调用此dentry->dentry_operatoin->d_revalidate来进行一次核对(网络文件系统此函数都实现了,以便于远程的便跟,在这里会得到更新)。
如果最终usr对应的dentry不存在,那么内核就在内存中直接分配一个dentry结构体并且把dentry的name和“usr”对应起来,并且设置这个dentry的parent为“/”对应的dentry,然后还要调用”/”对应的dentry->d_inode这个inode的inode_operation->lookup(“/”的inode,新建的dentry,flags),如果返回了新的dentry,那么就把dentry结构体指针指向新返回的dentry,否则还是返回刚刚新创建的那个dentry。(一般的文件系统都实现了inode_operation->lookup,我猜他们会在这个函数里面如果/usr存在则把dentry对应的inode给设置好。。如果/usr不存在则返回一个NULL之类的,以一个错误跳出整个路径执行)。
到这里,无论是dentry已经存在于内存中找到的,还是新创建的dentry,总之,对应于“usr”结构的dentry在内存中已经存在了。然后调用follow_managed()函数找到“usr”最新的vfsmount(这里有一点点麻烦,后续会专门讲,这里只需要指定如果”/dev/sda” mount 到了/mnt,/dev/sdb 也mount到了/mnt,那么这里返回的是最新的这条/dev/sdb mount到/mnt这个vfsmount)。
然后把这个已经找到的或创建的dentry(已经存在于内存中的dentry已经有了inode和它绑定,新建立的dentry也通过inode_operation->lookup建立起来了inode和dentry的联系(此函数会在操作真正的磁盘介质吧inode读出来))和这个最新的vfsmount存到struct path中
然后把这个含有dentry,vfsmount的path结构体存入nameidata数据结构中,到这里,“usr“对应的dentry,inode,vfsmount都准备好了,且存到了nameidata中了。
一层一层的往下找,依次会找到usr,bin,最后到了”make”,这里就不调用一层一层往下找的函数了,进入另外一个函数do_last()函数来处理。在dolast,在dolast里面如果此dentry不存在则创建它,如果有O_CREATE标识则创建这个文件的inode(这里会调用vfs_create函数,继续调用dentry->inode_operation->create来建立inode,文件系统实现的此函数会操作正在的磁盘介质去创建inode),并且建立inode和dentry的联系,并且建立”make”对应的vfsmount为最新的mount结构,至此,“/usr/bin/make”中最后一个分量“make”的dentry,inode,vfsmount都存到nameidata中去了。
接着还会把2中分配的file结构体的path(包含dentry和vfsmount)的dentry分量设置为nameidata的这个dentry(dentry结构体中已经有inode的指针),vfsmount也设置为nameidata的vfsmount,并且设置file结构体的file->f_mapppin为nameidata中dentry的inode的i_mapping.并且设置file->f_pos指针为0。
至此,make对应的新分配的这个struct file结构体中的dentry,inode,vfsmount都为nameidata中的了,并且struct file映射到内存的address_space也设置为了inode对应的address_space,struct file的当前位置指针设置为了0,“make”分量的这个struct file结构体准备好了。
接着还会把这个struct file结构体加入其inode对应的super_block超级块的s_files链表中,即struct file结构体会加入其自身inode所在超级块的所有文件链表中。并且如果自身inode的file_operations不为空则还会设置这个struct file的file_operation等于inode的这个file_operations,即公用inode的file的操作方法。如果inode的file_operations没有实现,则设置为空。设置此文件标识符为FILE_OPENED。
(4)建立fd到这个struct file结构体的联系
调用fd_install(fd,f)来把1中分配的文件描述符和3中的struct file建立联系。过程简单描述一下,先获取当前进程的fdtable(简单可理解为进程的关联的所有文件数组)的所有文件数组fdtable=current->files->fdt,(current为当前进程task_struct),设置fdtable->fd[fd]=file,(下标fd即新分配的文件描述符,file即为3中创建的struct file结构体)。这样,进程和文件描述符,struct file,dentry,inode,vfsmount就全部关联起来了。
10、读写文件操作
Read系统调用在内核里面的入口函数为sys_read。
(1)根据用户空间传入的文件描述符fd取出对应的struct file结构体
Struct file file=fget_light(fd, &fput_needed);此函数主要返回当前进程的所有文件表中下标为fd(即文件描述符)的struct file结构体(current为当前进程task_struct),即返回current->files_struct->fdtable->file[fd]这个struct file结构体;(这里我用结构体名字表示成员变量是为了方便理解)。
这里之所以可以这么直接取出来,是因为在调用sys_read系统调用之前,用户一般都已经通过调用open函数已经调用了sys_open系统调用(前面文章Linux 中open系统调用),把进程的这个fd对应的文件从硬盘上读取或创建好了,所以这里可以之前从数组里面取。
所以用户编程一般会先调用open函数在调用read函数,当然,如果有些库函数同时封装了这两个函数在一个函数里面的情况,这里不考虑了,最终结果一样。
(2)获取struct file 结构体的当前偏移量指针
对文件的内容读写都通过这个当前偏移量指针来操作,如对文件的随机位置访问就通过这个指针的随机偏移值来完成的。
loff_t pos = file_pos_read(file);
此函数返回file->f_pos,即返回当前struct file结构体的当前操作指针位置
(3)调用vfs_read从文件读取内容,存放到用户空间内存区
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
即把内核读取的文件内容存入char __user* buf指向的内存地址。如果文件系统没有实现file_operation或者既没有实现file_operation->read,也没有实现file_operation->aio_read,则报错。(即文件系统即没有实现同步读,也没有实现异步读,那就报错返回错误了)
如果文件系统实现了file->file_operation->read(还记得我吗在open系统调用中讲到的吗,在open系统调用中file->file_operation设置为了inode->file_operation)函数,则调用它来完成。
否则(说明文件系统没有实现read,但是实现了file_operation->aio_read)调用内核的默认函数do_sync_read(file, buf, count, pos);来做同步读取操作;而内核的do_sync_read函数内部实现是
ssize_t do_sync_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)函数
struct iovec iov = { .iov_base = buf, .iov_len = len };//用户空间的内存地址作为iov_base,到时候好存放从磁盘读取上来的数据,最终调用的是file_operation->aio_read,可是iov数组的长度为1.最终由文件系统的file_operation->aio_read函数去把数据从磁盘上读上来。可是由于aio_read读取可能由于磁盘忙没有完成读取数据的任务,那就需要不停的去读取直到不需要retry为止。
总之,do_sync_read最后调用的是file_operation->aio_read方法,但是iov数组长度为1,并且读取过程中如果读取操作没有完成则显示调用进程调度函数,本进程可能被挂起来且进程状态为TASK_UNINTERRUPTIBLE。直到最终读取成功,读取成功后进程状态会变为TASK_RUNNING,且数据直接存放在了用户空间传进来的那个buf内存去中。
(4)读取成功,唤醒相关等待进程
fsnotify_access(file);这里暂不做进一步分析,不是我们的重点
(5)更新文件的当前指针
读取操作完了之后,文件的当前指针的位置很可能变了,故此设置file->f_pos = pos;来根系文件当前指针
(6)如果需要则释放对file结构的引用
这里如果当前进程在读取的时候,struct file结构体的引用次数在自己读取之前加1正好等于1,说明本次读取可能是唯一的struct file的引用者,那么read函数结束的时候再次检查struct file的引用是否还是1(继续再检查一次是可能多进程竞争引起的,所以必须再次检查),如果还是,那么就把这个struct file结构体从超级块的文件链表中也删除掉(但是struct file结构体此时还没有从内存中释放)。
如果上述第一次检查就不等于1,那本进程不用释放这个struct file即相关释放工作
如果上述第一次检查等于1而第二次不等于1,本进程也不会释放struct file即相关释放工作
(我说的等于1是我自己方便描述,实际内核调用的是atomic_long_dec_and_test之类的函数来先减1,再和0比较之类的)
释放操作实际只是注册了一个回调函数,通过下面两行
init_task_work(&file->f_u.fu_rcuhead, ____fput); //____fput是一个实际释放操作的回调函数
task_work_add(task, &file->f_u.fu_rcuhead, true);
其中,____fput函数会释放struct file结构体,以及尝试释放起对应的dentry,mnt(之所以叫尝试是因为调用dput(dentry),dput(mnt),而dput(denty),dput(mnt)会继续检查dentry,mnt是否还在被使用,如果没有任何引用则真正释放所占内存,否则仅减少其引用计数)。 init_task_work中,file->f_u.fu_rcuhead是一个rcu_head节点。总之,init_task_work把____fput函数进行file->f_u.fu_rcuhead->func=___fput设置。而task_work_add(task, &file->f_u.fu_rcuhead, true);会吧这个rcu_head节点加入task->task_works, 并且会调用set_notify_resume(task)把进程的thread_info的标识里设置上TIF_NOTIFY_RESUME。这样,在进程从内核态返回用户态的时候会调用tracehook_notify_resume把task->task_works链表中的所有注册好的函数都会执行一遍(此时___fput函数就会被调用到了),并且清除TIF_NOTIFY_RESUME标识位
所以,struct file结构体要释放也是在内核返回用户态的时候才执行的,在内核态的时候一直还保留着。 注意,这里__fput中执行的释放操作并没有把进程所拥有的这个文件描述符及其在位图中的占位清空,如果执行了__fput只是这个文件描述符对应的的struct file=NULL了而已,文件描述符还站着呢。这需要后面用户空间再发个sys_close调用才能完成后续清除文件描述符等任务。注意,这些释放都是内存操作,磁盘上面的文件,inode等并没有释放。