1.文件系统原理
· 装过操作系统的人都知道,在装系统之前有一个磁盘分区的操作,每个磁盘通过最开始的MBR(主引导记录)来记录分区信息,每个分区就是一个文件系统,磁盘分区的操作就是将文件系统数据分布结构写入分区。使用mount指令就是将一个带文件系统的存储设备挂载到指定目录。如下图就是磁盘内部实际分布结构:
·磁盘是以扇区为单位存储的(512字节),由于扇区太小(512字节),直接使用效率低下,因此就设计了更大的逻辑区块(Block)作为文件系统的最小单位,通常是2、4、8个扇区的大小,也就是1KB、2KB、4KB。
· SuperBlock是文件系统最开始的Block,用来存储文件系统的大小,空的块,填满的块和各自总数等文件系统关键信息,每个文件系统都会只有一个SuperBlock。但是随着硬盘容量的不断扩大且磁盘分区有限,这就出现了区块群组 (block group),每个区块群组就相当于一个独立的文件系统,都有一个SuperBlock,这就是我们重装系统时创建的逻辑分区,逻辑分区中可以再分区就是通过这种方式实现的。
· 以ext2文件系统为例,文件包括文件内容和文件属性,分别放在Block和inode中。ext2文件系统除SuperBlock外,又被分为inode table区 和 Block area区。
· 以上信息都是描述一个具体的文件系统,而VFS是一个虚拟文件系统,他是所有文件系统的抽象,用来管理所有文件系统的。因此VFS也使用了几个类似的概念,super_block、inode、 dentry、file是其中最主要的概念。
2.VFS概述
· 从上图中我们可知,虚拟文件系统是文件操作的入口。linux内核通过虚拟文件系统来管理文件系统。VFS为所有的文件系统提供了统一的接口,这使得用户访问文件系统可以使用同样的系统调用,为此,所有的文件系统也必须要按照VFS定义的方式来实现。
· linux内核如何通过虚拟文件系统来管理文件系统?
(1)具体文件系统需要填充超级块super_block,并注册到一个全局链表中,让内核知道这个文件系统的存在,这是通过函数register_filesystem来实现的;
(2)调用kern_mount函数为具体文件系统申请必备的数据结构
(3)从填充inode的操作函数,比如创建文件、目录等操作;
· 我们会后续会通过yaffs文件系统作为举例来说明这三步是如何具体实现,本文后续将解释VFS使用的几个核心的数据结构。
3.超级块super_block
· 该数据结构位于 include/linux/fs.h 中,由于我们是初学,所以只将一些重要的列出来,见下列代码:
struct super_block {
unsigned char s_blocksize_bits; // 指定文件系统的块大小的位数
unsigned long s_blocksize; // 指定文件系统的块大小
loff_t s_maxbytes; // 最大的文件大小
struct file_system_type *s_type; // 指向file_system_type结构的指针,这个是在 register_filesystem 的时候放到file_systems的。
const struct super_operations *s_op; //超级块操作结构指针 --->
unsigned long s_magic; // 魔术字,每个文件系统都有一个
struct dentry *s_root; // 指向文件系统根dentry的指针
struct list_head s_inodes; /* 指向文件系统内所有的inode,可以通过其遍历inode对象 */
struct block_device *s_bdev; // 指向文件系统存在的块设备指针 --->
}
超级块代表了整个文件系统本身,因此每个文件系统都有一个super_block结构。重要结构有几个:
· struct file_system_type *s_type; 指向file_system_type结构的指针,这个是在 register_filesystem 的时候放到file_systems的。
· struct dentry *s_root; 指向文件系统根dentry的指针。
· struct list_head s_inodes; 指向文件系统内所有的inode,可以通过其遍历inode对象。
· const struct super_operations *s_op; 超级块操作结构指针,这个是每个文件系统需要各自实现的,由于太过重要,也简单介绍一下:
struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb); // 为一个新的inode分配内存并初始化
void (*destroy_inode)(struct inode *); // 收回一个inode的资源
void (*dirty_inode) (struct inode *, int flags); // 标记一个inode为dirty状态
int (*write_inode) (struct inode *, struct writeback_control *wbc); // 写一个inode函数,也标记一个写回结构
int (*drop_inode) (struct inode *); // 清除上一次掉线而没释放的自旋锁
void (*evict_inode) (struct inode *); // 驱逐一个inode
void (*put_super) (struct super_block *); // 准备释放一个super_block
void (*write_super) (struct super_block *); // 将super_block写到磁盘中
int (*sync_fs)(struct super_block *sb, int wait); // 文件系统同步,就是将所有的dirty inode全写到磁盘中
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 *); // 卸载文件系统的开始时使用
... 省略部分函数
};
· 其中比较重要的就是与inode相关的操作函数,alloc_inode是为一个新的inode分配内存并初始化,destroy_inode是收回一个inode的资源,dirty_inode是标记一个inode为dirty状态(dirty状态表示已经修改了内存中的inode,但还没有写到磁盘中)等。还有与super_block相关的函数以及fs相关的操作见注释。
4. 目录结构dentry
· 在本文第1部分介绍文件系统原理的时候,我们没有看到目录dentry。其实具体文件系统中不存在专门的目录结构。也就是说这个结构只存在于只存在于RAM中,不会存在于磁盘中。但是文件之间关系,在文件系统中是存在的,只是没有专用的结构,使用的和普通的文件一样,都是inode,只是内容不一样。dentry的存在价值是它能提高文件搜索的效率。
· 文件系统挂载的时候,VFS会遍历所有的文件关系,并为其创建dentry结构,这个结构就是上文提到的dentry cache 或叫 dcache。
· 当然,由于计算机的RAM大小有限,不一定能存放这么多dentry,所以通常只创建了一部分,所以当在dcache中找不到一个文件的时候,就要通过文件系统的调用去找,这就是上一篇讲open实现中所做的事情。
· 下面介绍dentry结构:
struct dentry {
/* RCU 搜寻需要的结构 */
unsigned int d_flags; /* 被顺序锁保护 */
seqcount_t d_seq; /* 每个目录结构都存在一个顺序锁*/
struct hlist_bl_node d_hash; /* 搜寻哈希表 */
struct dentry *d_parent; /* 父目录指针 */
/* Ref 搜寻模式需要如下结构 */
unsigned int d_count; /* 被自旋锁保护 */
spinlock_t d_lock; /* 每个目录结构都存在一个自旋锁*/
const struct dentry_operations *d_op; /* 目录操作函数 --->*/
struct super_block *d_sb; /* 目录树的根 */
struct list_head d_subdirs; /* 子目录链表 */
struct list_head d_alias; /* inode别名链表 */
};
c· RCU 搜寻和 Ref 搜寻模式在上文的第3节讲得很细致,其中的保护机制 顺序锁d_seq 和 自旋锁d_lock都在此定义。
· d_subdirs 用来存放子目录结构。
· d_parent 指向该目录的父目录结构。
· 操作函数d_op,我们也简单介绍一下:
struct dentry_operations {
int (*d_revalidate)(struct dentry *, struct nameidata *);
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_hash函数,我们在上篇文章的分析中第6节中提到了这个函数,该函数用来将一个dentry加入hash table中。
· d_compare函数,在上文中的第9节介绍了它,用来比较两个dentry的名字是否一致。
· d_revalidate函数 用来让目录重新有效,在dcache中进行文件名检索时总会调用。
5. 节点结构inode
· inode结构代表一个文件,inode保存了文件的重要参数,包括文件的大小,创建时间,操作函数等。通过inode和dentry,才能完整的表示一个文件。
· 下面介绍inode结构,也只介绍重要的部分。
struct inode {
umode_t i_mode; // 文件权限属性,例如777
unsigned short i_opflags;
uid_t i_uid; // 文件所属用户ID
gid_t i_gid; // 文件所属用户组ID
unsigned int i_flags;
const struct inode_operations *i_op; // 操作函数
struct super_block *i_sb; // 所属文件系统的super_block --->
struct address_space *i_mapping; // 指向文件系统的缓存内容 --->
struct hlist_node i_hash; //
struct list_head i_wb_list; /* 用来链接到 dev IO list */
struct list_head i_lru; /* inode LRU list */
struct list_head i_sb_list; // 用来连接到super_block中的链表
union {
struct list_head i_dentry; //用来存放包含该inode的dentry结构,可以由多个目录包含同一文件(链接)。
struct rcu_head i_rcu;
};
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;
};
};
· i_mode,i_uid, i_gid 分别表示文件权限属性,所属用户ID 和 文件所属用户组ID,这都是用户用 ls -al 命令可以查看得到的。
· i_mapping 很重要,是指向文件的缓存内容,对于文件内容的读写首先都要在此寻找是否有缓存,没有的话就要去磁盘中提取文件到缓存中再操作。写缓存之后会在合适的时候回写到磁盘中。
· i_wb_list、i_lru、i_dentry、i_devices 这都些链表结构都是链接到与该文件相关的地方,比如i_wb_list链接到设备IO链表,i_dentry 链接到包含该inode的dentry等。
· i_bdev 是指向文件所属设备的指针,大部分文件是属于一个块设备的,所以i_bdev 用的比较多。
· i_op 是inode的操作函数,由于很重要,也展开讲一讲:
struct inode_operations {
struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);
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 *,int, struct nameidata *);
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 *,int);
int (*rmdir) (struct inode *,struct dentry *);
int (*mknod) (struct inode *,struct dentry *,int,dev_t);
int (*rename) (struct inode *, struct dentry *,
struct inode *, struct dentry *);
...skip
} ____cacheline_aligned;
· 看到以上的各个操作函数名,我们知道,很多系统调用中关于目录的部分的最终调用就是这些函数,比如create用来创建文件,link就是创建硬链接文件,mkdir就是创建文件夹,mknod就是创建一个设备节点,rename就是重命名。这是由于目录dentry都会配备一个inode来保存相关信息,这是实际磁盘中存在的。
· lookup函数是用来搜索一个inode,这个我们上文介绍open函数内核实现时分析过。
6.文件对象file
· 文件对象的作用是描述进程和文件的交互关系。这个结构只存在于内存中,进程每打开一个文件,内核都会创建一个file对象。同一个文件在不同的进程中会有不同的文件对象。
struct file {
struct path f_path; // 文件路径,包括目录和挂载点
#define f_dentry f_path.dentry
#define f_vfsmnt f_path.mnt
const struct file_operations *f_op; // 文件操作函数,用来操作一个打开了的函数
spinlock_t f_lock; // 用来保护f_ep_links, f_flags 和在 lseek SEEK_CUR中的f_pos 和 i_size
atomic_long_t f_count; // 文件操作次数统计
unsigned int f_flags; // 文件标记,包括O_NONBLOCK,O_CREAT,O_RDONLY, O_WRONLY 等所需标记
fmode_t f_mode; // 文件模式,标记了文件操作权限,比如读,写,执行等
loff_t f_pos; // 指示当前进程对文件的操作位置
struct file_ra_state f_ra; // 用于文件预读的设置
void *private_data; // 串口等设备需要携带特定数据
struct list_head f_ep_links; // 在epoll中用来链接与该文件相关的钩子函数
struct address_space *f_mapping; // 指向的结构封装了文件的读写缓存页面
...skip
};
· f_lock 文件自旋锁,用于保护f_ep_links,f_flags ,lseek 函数中使用的 f_pos 和 i_size。
· f_count 是文件操作次数统计。
· f_mode 是文件模式,标记了文件操作权限,比如读,写,执行等。
· f_ra 用于文件预读的设置,这个后续文章会讲述。
· f_pos 指示当前进程对文件的操作位置。
· f_mapping 指向的结构封装了文件的读写缓存页面。
· f_op 是文件操作函数,操作一个打开了的函数需要这些操作:
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 *);
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 (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
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 *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, struct pipe_inode_info *, size_t, unsigned int);
};
· 在上面的操作函数中,我们也能看到很多系统操作的影子。read 和 write就是读写文件的函数,open就是打开文件的函数,其他的一些函数虽然没有用过,但是都和系统调用有关,这也是因为我们文件的操作都是在file结构中。