Linux文件编程之虚拟文件系统(VFS)
当用户层程序员在编写文件函数时,常常会用到open(),read()和write()这类系统调用,而且用的也很爽,因为我们只需要知道这些函数如何调用就OK了,而不用深究具体文件系统和实际物理介质是如何实现的。而我们内核编程人员就要了解这些底层实现,给上层人员提供更多的方便。因此我们的任务就更复杂,佛家有一句名言:“我不入地狱,谁如地狱”因此我们就要有奉献精神编写出具有统一简单效率高的接口为用户层程序员提供方便。
如果你有这种奉献精神和喜爱Linux内核以及有着很高的好奇心的话,那我们就来深入学习内核看看Linux内核到底是如何实现这些功能的。
一.VFS的概念
Linux 之所以能支持除了Ext2文件系统之外的各种文件系统,是因为Linux提供了一种统一的框架,就是所谓的虚拟文件系统转换(Virtual FilesystemSwitch),简称虚拟文件系统(VFS)。这样, 用户程序可以通过同一个文件系统界面,也就是同一组系统调用,能够对各种不同的文件系统以及文件进行操作。
1.在上面提到Linux中支持几十种文件系统,它是怎么管理的呢,它都管理什么呢?
首先,VFS 只对挂载到文件系统种的文件系统进行管理,即它时按需管理的。
其次,因为它继承了Unix的设计思想,所以它也是对文件,目录项,索引节点和超级块进行管理。
2.VFS中四个主要对象:
1>超级块对象:描述已安装文件系统。
每个文件系统 都对应一个超级对象。文件系统的控制信息存储在超级块中。
2>索引节点对象:描述一个文件。
每个文件 都有一个索引节点对象。每个索引节点对象都有一索引节点号---->正是用这个号来唯一的标识某个文件系统中的指定文件。
3>目录项对象:描述一个目录项,是路径的组成部分。
VFS把每个目录看作一个由若干子目录和文件组成的常规文件。
例如:我们在查找路径名:/tmp/test时,内核为根目录“/”创建第一个目录项对象,为根目录下tmp项创建第二级目录项对象,为/tmp目录下的test项创建第三级目录项对象。
4>文件对象:描述由进程打开的文件。
Tiger-John说明:
1.因为 VFS 将目录作为一个文件来处理,所以不存在目录对象。虽然目录项不同于目录,但目录却和文件相同。
2.Linux中将文件的相关信息和文件本身区分开了。
在Linux 中文件的相关信息,被存储在一个单独的数据结构中,该结构被称为索引节点。文件(目录)信息按照索引节点形式存储在单独的块中;控制信息被集中存储在磁盘的超级块中
说了这么多,VFS到底是如何实现的呢?----现在我们就来深入内核代码来看看吧
二.VFS四个主要对象的实现
VFS采用的是面向对象的设计思想,使用一簇数据结构来代表通用文件对象。所以内核中的数据结构都使用C结构体实现。
1.superblock(超级块)对象:
1>超级块用来描述特定文件系统的信息。它存放在磁盘特定的扇区中 ,它在使用的时候将信息存在于内存中。
2> 当内核对一个文件系统进行初始化和注册时在内存为其分配一个超级块,这就是VFS超级块。
即,VFS超级块是各种具体文件系统在安装时建立的,并在这些文件系统卸载时被自动删除 。
3>超级块对象由结构体 super_block来体现。
VFS超级块的数据结构为 super_block在include/linux/fs.h中可以查看
1318struct super_block {
1319 struct list_head s_list; // 超级快链表指针
1320 dev_t s_dev; // 设备表示符
1321 unsigned char s_dirt; //脏标志
1322 unsigned char s_blocksize_bits; //以位为单位的块的大小
1323 unsigned long s_blocksize; //以字节为单位的块大小
1324 loff_t s_maxbytes; //文件大小上限
1325 struct file_system_type *s_type; //指向文件系统的file_system_type 数据结构的指针
1326 const struct super_operations *s_op; //超级块方法
1327 const struct dquot_operations *dq_op; //磁盘限额方法
1328 const struct quotactl_ops *s_qcop; //限额控制方法
1329 const struct export_operations *s_export_op; //导出方法
1330 unsigned long s_flags; //登录标志
1331 unsigned long s_magic; //文件系统的魔数
1332 struct dentry *s_root; //目录登录点
1333 struct rw_semaphore s_umount; //卸载信号量
1334 struct mutex s_lock; //超级块信号量
1335 int s_count; //超级块引用计数
1336 atomic_t s_active; //活动引用记数
1337#ifdef CONFIG_SECURITY
1338 void *s_security; //安全模块
1339#endif
1340 const struct xattr_handler **s_xattr;
1341
1342 struct list_head s_inodes; //把所有索引对象链接在一起,存放的是头结点
1343 struct hlist_head s_anon; //匿名目录项
1344#ifdef CONFIG_SMP
1345 struct list_head __percpu *s_files;
1346#else
1347 struct list_head s_files; //链接所有打开的文件。
1348#endif
1349 /* s_dentry_lru and s_nr_dentry_unused are protected by dcache_lock */
1350 struct list_head s_dentry_lru; /* unused dentry lru */
1351 int s_nr_dentry_unused; /* # of dentry on lru */
1352
1353 struct block_device *s_bdev; //相关的块设备
1354 struct backing_dev_info *s_bdi;
1355 struct mtd_info *s_mtd;
1356 struct list_head s_instances; //该类型文件系统
1357 struct quota_info s_dquot; //限额相关选项
1358
1359 int s_frozen;
1360 wait_queue_head_t s_wait_unfrozen;
1361
1362 char s_id[32]; //文本名字
1363
1364 void *s_fs_info; //文件系统特设信息
1365 fmode_t s_mode;
1366
1367 /* Granularity of c/m/atime in ns.
1368 Cannot be worse than a second */
1369 u32 s_time_gran;
1370
1371 /*
1372 * The next field is for VFS *only*. No filesystems have any business
1373 * even looking at it. You had been warned.
1374 */
1375 struct mutex s_vfs_rename_mutex; /* Kludge */
1376
1377 /*
1378 * Filesystem subtype. If non-empty the filesystem type field
1379 * in /proc/mounts will be "type.subtype"
1380 */
1381 char *s_subtype;
1382
1383 /*
1384 * Saved mount options for lazy filesystems using
1385 * generic_show_options()
1386 */
1387 char *s_options;
1388};
现在我们来对其中主要的数据结构进行分析
比较重要的数据结构以在上面有注释。
我们先来看一个图,再来具体解释:
1.s_list :所有的超级块形成一个双联表,s_list.prev和s_list.next分别指向与当前超级块相邻的前一个元素和后一个元素。通常我们通过list_entry宏来获取s_list所在超级块结构体的地址(超级块对像是以双向链表的形式链接在一起的)。
2. s_lock :保护链表免受多处理器系统上的同时访问。
3.s_fs_info: 字段指向具体文件系统的超级块。
例如:超级块对象指的是Ext2文件系统,该字段就指向ext2_sb_info数据结构。
4.s_dirt :来表示该超级块是否是脏的,也就是说,磁盘上的数据是否必须要更新。
5.超级块对象是通过函数alloc_super()创建并初始化的。在文件系统安装时,内核会调用该函数以便从磁盘读取文件系统超级块,并且将其信息填充到内存中的超级块对象中 。
6.
超级对象中最重要的就是s_op,每一种文件系统都应该有自己的super_operations操作实例。它指向超级块的操作函数表,
它由struct super_operations结构体来表示。
现在来看一下它的定义:它的定义在 include/linux/fs.h头文件中可以看到
1560struct super_operations {
1561 struct inode *(*alloc_inode)(struct super_block *sb);
1562 void (*destroy_inode)(struct inode *);
1563
1564 void (*dirty_inode) (struct inode *);
1565 int (*write_inode) (struct inode *, struct writeback_control *wbc);
1566 int (*drop_inode) (struct inode *);
1567 void (*evict_inode) (struct inode *);
1568 void (*put_super) (struct super_block *);
1569 void (*write_super) (struct super_block *);
1570 int (*sync_fs)(struct super_block *sb, int wait);
1571 int (*freeze_fs) (struct super_block *);
1572 int (*unfreeze_fs) (struct super_block *);
1573 int (*statfs) (struct dentry *, struct kstatfs *);
1574 int (*remount_fs) (struct super_block *, int *, char *);
1575 void (*umount_begin) (struct super_block *);
1576
1577 int (*show_options)(struct seq_file *, struct vfsmount *);
1578 int (*show_stats)(struct seq_file *, struct vfsmount *);
1579#ifdef CONFIG_QUOTA
1580 ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
1581 ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
1582#endif
1583 int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t);
1584};
1>可以看到该结构体中的每一项都是一个指向超级块操作函数的指针,超级块操作函数执行文件系统和索引节点的低层操作。
2>当文件系统需要对超级块执行操作时,要在超级块对象中寻找需要的操作方法
例如:一个文件系统要写自己的超级块,需要调用:
sturct super_block * sb;
sb->s_op->write_super)(sb);
sb是指向文件系统超级块的指针,沿着该指针进入超级块操作函数表,并从表中取得writ_super()函数,该函数执行写入超级块的实际操作。
Tiger-John说明:
尽管writ_super()方法来自超级块,但是在调用时,还是要把超级块作为参数传递给它。
3>.分析其中比较重要的一些数据结构。
a.struct inode * alloc_inode(struct super_block * sb) :创建和初始化一个新的索引结点。
b.void destroy_inode(struct super_block *sb) :释放指定的索引结点 。
c.void dirty_inode(struct inode *inode) :VFS在索引节点被修改时会调用此函数。
d.void write_inode(struct inode *inode, struct writeback_control *wbc) 将指定的inode写回磁盘。
e.void drop_inode( struct inode * inode):删除索引节点。
f.void put_super(struct super_block *sb) :用来释放超级块。
g.void write_super(struct super_block *sb):更新磁盘上的超级块。
h.void sync_fs(struct super_block *sb,in wait):使文件系统的数据元素与磁盘上的文件系统同步,wait参数指定操作是否同步。
i.int statfs(struct super_block *sb,struct statfs *statfs):获取文件系统状态。把文件系统相关的统计信息放在statfs中。
2.VFS的索引节点
1>文件系统处理文件或目录时的所有信息都存放在称为索引节点的数据结构中。
文件名可以随时该,但是索引节点对文件是唯一的(它是随文件的存在而存在)。
2>具体文件系统的索引节点是存放在磁盘上的,是一种静态结构,要使用它,必须将其调入内存,填写 VFS的索引节点。VFS索引节点也称为动态节点。(即索引节点仅当文件被方位时才在内存中创建)
3>我们来深入下来看一下它的内核
它的定义在 /include/linux/fs.h中有这个结构体的定义
725struct inode {
726 struct hlist_node i_hash; //散列表
727 struct list_head i_list; //索引节点链表
728 struct list_head i_sb_list; //链接一个文件系统中所有inode的链表
729 struct list_head i_dentry; //目录项链表
730 unsigned long i_ino; //索引节点号
731 atomic_t i_count; //引用计数
732 unsigned int i_nlink; //硬连接数
733 uid_t i_uid; //使用者的id
734 gid_t i_gid; //使用组id
735 dev_t i_rdev; //实际设备标识符号
736 unsigned int i_blkbits;
737 u64 i_version; //版本号
738 loff_t i_size; //以字节为单位
739#ifdef __NEED_I_SIZE_ORDERED
740 seqcount_t i_size_seqcount;
741#endif
742 struct timespec i_atime; //最后访问时间
743 struct timespec i_mtime; //最后修改时间
744 struct timespec i_ctime; //最后改变时间
745 blkcnt_t i_blocks; //文件的块数
746 unsigned short i_bytes; //使用的字节数
747 umode_t i_mode; //访问权限控制
748 spinlock_t i_lock; //自旋锁
749 struct mutex i_mutex;
750 struct rw_semaphore i_alloc_sem;
751 const struct inode_operations *i_op; //索引节点操作表
752 const struct file_operations *i_fop; //默认的索引节点链表
753 struct super_block *i_sb; //相关的超级块
754 struct file_lock *i_flock; //文件锁链表
755 struct address_space *i_mapping; //相关的地址映射
756 struct address_space i_data; //设备地址映射
757#ifdef CONFIG_QUOTA
758 struct dquot *i_dquot[MAXQUOTAS]; //节点的磁盘限额
759#endif
760 struct list_head i_devices; //块设备链表
761 union {
762 struct pipe_inode_info *i_pipe; //管道信息
763 struct block_device *i_bdev; //块设备驱动
764 struct cdev *i_cdev;
765 };
766
767 __u32 i_generation; //索引节点版本号
768
769#ifdef CONFIG_FSNOTIFY
770 __u32 i_fsnotify_mask; /* all events this inode cares about */
771 struct hlist_head i_fsnotify_marks;
772#endif
773
774 unsigned long i_state; //状态标志
775 unsigned long dirtied_when; //首次修改时间
776
777 unsigned int i_flags; //文件系统标志
778
779 atomic_t i_writecount; //写者计数
780#ifdef CONFIG_SECURITY
781 void *i_security; //安全模块
782#endif
783#ifdef CONFIG_FS_POSIX_ACL
784 struct posix_acl *i_acl;
785 struct posix_acl *i_default_acl;
786#endif
787 void *i_private; /* fs or device private pointer */
788};
在我们看完这段代码后,是不是发现在上面被紫色标记的一些信息我们都很熟悉,还记的我们在终端下输入命令:ls 命令后可以看到文件的信息,这些信息就是记录在这里的。
这是为什么呢?
在原理课中我们知道,文件是由FCB(文件控制块控制的),而具体到Linux下,文件是有索引节点结构控制的。所以在struct inode 里存放了文件的基本信息。
大家有没有发现在怎么在索引节点里面会包含超级块的对象呢(上面被红色标记的),有些人可能不明白了,先看看下面的图,再来解释把。
从上面对的图我们可以看出索引节点 对象靠i_sb指回到了超级块对象。
i_hash :为了提高查找inode的效率,每一个inode都会有一个hash值。该字段指向hash值相同的inode所形成的双链表该字段包含prev和next两个指针,分别指向上述链表的前一个元素和后一个元素;
i_list :所有索引结点形成的双联表,(从图上可以看出,索引节点对象是靠它来链接的)
i_dentry :所有引用该inode的目录项将形成一个双联表,该字段即为这个双联表的头结点
i_ino :索引结点号。通过ls -i命令可以查看文件的索引节点号;
i_count :引用计数;
i_nlink :硬链接数。当该inode描述一个目录时,这个值至少为2,代表.和..的数目;
i_uid :inode所属文件的拥有者的id,通过ls -n可查看拥有者id;
i_gid :inode所属文件所在组的id,通过ls -n可查看组id;
i_rdev :如果该inode描述的是一个设备文件,此值为设备号;
i_blkbits :以位为单位的块大小;
i_atime :文件最近一次被访问的时间。通过ls -lu可查看该时间;
i_mtime :文件最近一次被修改的时间,这里的修改只文件内容被修改。通过ls -l可查看该时间;
i_ctime :文件最近一次被修改的时间,这里的修改除了指文件内容被修改外,更强调的是文件的属性被修改。通过ls -lc可查看该时间;
i_blocks :文件使用块的个数,通过ls -s可以查看该某个文件的块使用数目;
i_mode :文件的访问权限;
i_op : 指向索引结点操作结构体的指针;
i_fop : 指向文件操作街头体的指针;
i_sb : 指向inode所属文件系统的超级块的指针;
i_pipe :如果inode所代表的文件是一个管道,则使用该字段;
i_bdev :如果inode所代表的文件是一个块设备,则使用该字段;
i_cdev :如果inode所代表的文件是一个字符设备,则使用该字段;
i_state : 索引节点的状态信息。
Tiger-John说明:
1.在同一个文件系统中,每个索引节点号都是唯一的,内核可以根据索引节点号的散列值来查找其inode结构。
2.inode中有两个设备号i_dev和i_rdev。
a.特设文件外,每个节点都存储在某个设备上,这就是i_dev。
b. 如果索引节点所代表的并不是常规文件,而是某个设备,则需要另一个设备号,这就是i_rdev。
3.对i_state的说明:
每个VFS索引节点都会复制磁盘索引节点包含的一些数据,比如文件占有的磁盘数。如果i_state 的值等于I_DIR,该索引节点就是“脏“的。也就是说,对应的磁盘索引节点必须被更新。
4.三个重要的双向链表:
a.未用索引节点链表,正在使用索引节点链表和脏索引节点链表。每个索引节点对象总是出现在上面三种的一个。
b.这3个链表都是通过索引节点的i_list 域链接在一起的。
c.属于“正在使用“或“脏“链表的索引节点对象也同时存放在一个散列表中。
<散列表加快了对索引节点对象的搜索>.
5.一个索引节点代表文件系统中的一个文件,它也可 以是设备或管道这样的特殊文件。所以在索引节点结构体中有一些和特殊文件相关的项。
6.有时候某些文件系统并不能完整地包含索引节点结构体要求的所有信息。那么此时刚怎么办呢?
此时,可以给它赋一些其它的值。
例如:一个文件系统可能并不记录文件的访问时间,这时就可以在i_atime中存储0。
7.i_list和i_sb_list的区别
a:i_list:VFS中使用四个链表来管理不同状态的inode结点。inode_unused将当前未使用的inode链接起来,inode_in_use将当前正在被使用的inode链接起来,超级块中的s_dirty将所有脏inode链接起来,i_hash将所有hash值相同的inode链接起来。i_list中包含prev和next两个指针,分别指向与当前inode处于同一个状态链表的前后两个元素
b.i_sb_list:每个文件系统中的inode都会形成一个双联表,这个双链表的头结点存放在超级块的s_inodes中。而该字段中的prev和next指针分别指向在双链表中与其相邻的前后两个元素
c..索引结点中i_sb_list链表是链接一个文件系统中所有inode的链表,因此相邻的inode之间均会由此链表链接;而i_list链接的是处于同一个状态的所有inode。所以,相邻inode之间并不一定链接在一起。
4>与索引节点关联的方法叫索引节点操作表,它是在 struct inode_operations这个结构体中具体描述的。它的定义在 include/linux/fs.h头文件中定义。
1516struct inode_operations {
1517 int (*create) (struct inode *,struct dentry *,int, struct nameidata *);
1518 struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);
1519 int (*link) (struct dentry *,struct inode *,struct dentry *);
1520 int (*unlink) (struct inode *,struct dentry *);
1521 int (*symlink) (struct inode *,struct dentry *,const char *);
1522 int (*mkdir) (struct inode *,struct dentry *,int);
1523 int (*rmdir) (struct inode *,struct dentry *);
1524 int (*mknod) (struct inode *,struct dentry *,int,dev_t);
1525 int (*rename) (struct inode *, struct dentry *,
1526 struct inode *, struct dentry *);
1527 int (*readlink) (struct dentry *, char __user *,int);
1528 void * (*follow_link) (struct dentry *, struct nameidata *);
1529 void (*put_link) (struct dentry *, struct nameidata *, void *);
1530 void (*truncate) (struct inode *);
1531 int (*permission) (struct inode *, int);
1532 int (*check_acl)(struct inode *, int);
1533 int (*setattr) (struct dentry *, struct iattr *);
1534 int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);
1535 int (*setxattr) (struct dentry *, const char *,const void *,size_t,int);
1536 ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);
1537 ssize_t (*listxattr) (struct dentry *, char *, size_t);
1538 int (*removexattr) (struct dentry *, const char *);
1539 void (*truncate_range)(struct inode *, loff_t, loff_t);
1540 long (*fallocate)(struct inode *inode, int mode, loff_t offset,
1541 loff_t len);
1542 int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start,
1543 u64 len);
1544};
inode_operations对象包括了内核针对特定文件所能调用的方法。
现在我们对其中一些重要的结果进行分析:
create() :如果该inode描述一个目录文件,那么当在该目录下创建或打开一个文件时,内核必须为这个文件创建一个inode。VFS通过调用该inode的i_op->create()函数来完成上述新inode的创建。该函数的第一个参数为该目录的 inode,第二个参数为要打开新文件的dentry,第三个参数是对该文件的访问权限。如果该inode描述的是一个普通文件,那么该inode永远都不会调用这个create函数;
lookup() :查找指定文件的dentry;
link :用于在指定目录下创建一个硬链接。这个link函数最终会被系统调用link()调用。该函数的第一个参数是原始文件的dentry,第二个参数即为上述指定目录的inode,第三个参数是链接文件的dentry。
unlink ():在某个目录下删除指定的硬链接。这个unlink函数最终会被系统调用unlink()调用。 第一个参数即为上述硬链接所在目录的inode,第二个参数为要删除文件的dentry。
symlink ():在某个目录下新建
mkdir:在指定的目录下创建一个子目录,当前目录的inode会调用i_op->mkdir()。该函数会被系统调用mkdir()调用。第一个参数即为指定目录的inode,第二个参数为子目录的dentry,第三个参数为子目录权限;
rmdir ():从inode所描述的目录中删除一个指定的子目录时,该函数会被系统调用rmdir()最终调用;
mknod() :在指定的目录下创建一个特殊文件,比如管道、设备文件或套接字等。
以上对索引节点中数据结构的解释来自edsionte详细请看edsionte的博客http://www.edsionte.com/
-------------------------------------------------------------------------------------
Tiger-John总结:
1.对于不同的文件系统,上面的每个函数的具体实现是不同的,也不是每个函数都必须实现,没有实现的函数对应的域应当设置为NULL 。
2.上面我们说了两个主要的操作对像:superblock和inode。它们两个对象中都包含一个操作对象。super_operations和inode_opetations它们有什么区别呢
a.super_operations对象:其中包括内核针对特定文件系统 所有调用的方法。
b.inode_operations对象: 其中包括内核对特定文件 的所有调用的方法。
所以它们一个是针对文件系统,一个是针对文件 。
3.本来inode 中应该包括“目录节点”的名称,但由于符号链接的存在,导致一个物理文件可能有多个文件名,因此把和“目录节点”名称相关的部分从 inode 中分开,放在一个专门的 dentry 结构中。