发表于 2016-03-20 | 分类于 Linux内核 |
文章系原创,如需转载请注明:转载自”Blog of UnicornX” (http://unicornx.github.io/)
最新更新于:2016-04-21
总结得还是有点粗糙,但基本思路和涉及的内核主要数据结构都到位了。以后再慢慢改进吧。
主要参考:
Linux下的文件系统中宏观上主要分为三层:
这个下面的是另外一张类似的图解,表达的应该是相同的意思:
上面的图主要想要表达的意思是:VFS作为一个中间层,主要的作用是提供了一种软件机制,对上层应用屏蔽底层不同的调用方法,提供一套统一的调用接口;对下层的不同的文件系统,实现一种组织管理和映射,映射的实现可以理解为并抽象和定义了一套规范的行为要求不同的文件系统来适配并实现,类似于OOP中的虚函数或者Java中的接口类的意思。因此,VFS其实就是文件系统组织管理中的一个抽象层。
VFS层由四个主要部件构成
以上组成元素只存在于内存中,每次系统初始化期间Linux都会先在内存中构造一棵VFS的目录树(也就是源码中的namespace)。注意VFS作为一个虚拟的文件系统,其存在于内存中,一旦关机后就不复存在,而实际的文件系统(不考虑虚拟文件系统,譬如sysfs或者proc等),即我们常说的ext,ntfs等常见的磁盘文件系统存在于硬盘上,或者叫外存上。为方便区别,后面凡是提到”文件系统”指的是外存上的,特指磁盘上的一个分区。而VFS我们叫它虚拟文件系统,则表示其存在于内存中。
上面说的VFS的组成,譬如superblock,inode,dentry等概念其实都来自Ext这种文件系统的组织方式,所以两者非常相似(毕竟Ext是Linux的原生文件系统)。所以如果VFS映射的是ext文件系统,很多对应关系是一对一的,操作速度会非常快,而如果映射的是其他的文件系统,转换的效率就可想而知了。后面的内容在涉及到实际的文件系统时为方便起见,都是以ext文件系统为例。
先看一张实际的磁盘文件系统ext组织磁盘上的数据的方式,
这张图告诉我们在磁盘(外存)上:
/home/fs
里面包括两个路径分量home
和fs
对应两个目录项,分别在两个目录块中存放home
和fs
的信息。注意,以上描述针对的是物理磁盘上的信息。而这张图最下面画的i节点数组
,以及目录块所对应的i节点号+文件名
实际上应该对应的是VFS在内存中创建的inode
和dentry
的缓存。我们也从图中可以看到每个dentry
,其文件名
来自数据块区中的目录块的内容,而其i节点号
指向实际的inode
,这应该表达的就是一种硬链接的概念。
因为我们说过,VFS的实现就是以ext文件系统为范本的,所以在内存中的VFS长的样子大致也是如此。譬如VFS在内存中,有super_block
对象对应外存上的超级快,inode
对应外存上的节点,dentry
对应外存上的目录块。只不过对于VFS来说:实际的数据还是放在物理磁盘上,内存中的数据是为了访问方便,特别地,节点
和目录块
不会全部加载到内存中,只会对部分当前正在频繁操作的节点
和目录块
会以inode
和dentry
的形式缓存在内存中以加快访问速度。具体参考上面的第二张图中的cache。
超级块,是每个FS最基本的元数据,它保存了文件系统的类型、大小、状态和其他信息等。超级快对于文件系统是非常关键的,因此一般文件系统都会冗余存储多份(参考上图)。
VFS中的一个超级块对象可以认为就对应着一个实际的FS。用结构体类型super_block
表示,定义在文件中。更多的操作函数,参考fs/super.c
。
struct super_block {
struct list_head s_list; /* Keep this first */
...
const struct super_operations *s_op;
...
struct list_head s_inodes; /* all inodes */
...
struct list_head s_instances;
...
};
有关超级块,只摘录了几个我认为比较重要的成员供参考理解,总结如下:
s_list
和超级快链表系统中所有(挂载)的FS,即所有的超级快对象通过一个链表维护,参见fs/super.c
LIST_HEAD(super_blocks);
而s_list
这个成员就是用来串联所有的超级块对象的,见下图。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CfNW5nYg-1589775803222)(http://img.dnbcw.info/2010125/pxup2592610.gif)]
s_instances
和文件系统类型对于特定的文件系统类型, 属于同一种文件系统类型的所有的超级快对象也通过一个链表串联起来。
提到这里,需要知道VFS中还定义了一个结构体类型file_sytem_type
,用来表示一种文件系统类型,参考``。
struct file_system_type {
const char *name;
...
int (*get_sb) (struct file_system_type *, int,
const char *, void *, struct vfsmount *);
void (*kill_sb) (struct super_block *);
struct module *owner;
struct file_system_type * next;
struct list_head fs_supers;
...
};
其中:
get_sb()/kill_sb()
: 虚函数,由具体的文件系统实现,get_sb()
在文件系统被安装时被调用,用于从磁盘上读取超级快,并在内存中组装为super_block
对象。kill_sb()
则是对应的析构函数。super_block
的s_instances
是对应的关系。内核维护了一个全局的链表,用于保存当前支持的文件系统类型的列表,定义在fs/filesystems.c
static struct file_system_type *file_systems;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S2QaUZ7i-1589775803224)(http://img.dnbcw.info/2010125/pxup2592611.gif)]
注意LKD3rd上有这么一句话:
There is only one file_system_type per filesystem, regardless of how
many instances of the filesystem are mounted on the system, or whether
the filesystem is even mounted at all.
在Linux中添加新文件系统类型的方法是调用register_filesystem()
。删除文件系统可以调用unregister_filesystem()
。
另外在命令行上输入cat /proc/filesystems
就可以查看这个文件系统类型的列表。
s_op
和超级快操作这个也是超级快中最重要的一个域,它指向超级块的操作函数表。由结构体super_operations
表示,定义在文件``中。这里依然是虚函数的概念,这些回调函数由各个具体的文件系统实现,VFS会在合适的时间点上调用它们,针对实际的FS操作其超级块和文件节点对象。
每一个具体回调函数的意义,查看LKD3第13.6章节的内容。
s_inodes
该超级块中当前操作的inodes对象,采用一个链表维护在这里,具体参考后面有关文件节点inode的总结。
inode包含了一个文件的元数据,要理解所谓元数据的概念,
VFS的索引节点inode仅当文件被访问时,才会在内存中创建。
索引节点用inode结构体表示,定义在文件``中。
struct inode {
struct hlist_node i_hash;
struct list_head i_list; /* backing dev IO list */
struct list_head i_sb_list;
struct list_head i_dentry;
unsigned long i_ino;
atomic_t i_count;
...
unsigned int i_nlink;
uid_t i_uid;
gid_t i_gid;
...
loff_t i_size;
...
struct timespec i_mtime;
...
umode_t i_mode;
...
const struct inode_operations *i_op;
...
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
};
...
void *i_private; /* fs or device private pointer */
};
有关索引节点,只摘录了几个我认为比较重要的成员供参考理解,总结如下:
i_ino
索引节点号,注意节点号在FS中独立分配,所以两个FS之间的索引节点号有可能重复。
i_count
以下三个成员和索引节点的组织有关:
struct hlist_node i_hash;
struct list_head i_list; /* backing dev IO list */
struct list_head i_sb_list;
管理inode的四个链表,系统根据inode的使用状态,将它们组织在多个双向链表中:
inode_unused
:将目前还没有使用的inode链接起来(通过i_list
域链接)inode_in_use
:目前正在使用的inode链接起来(通过i_list
域链接)super_block
结构体中的s_dirty
:该链表链接了所有被修改过的inode,这些inode将被更新到磁盘上。(通过i_list域链接起来)。inode_hashtable
:注意为了加快inode的查找效率,将正在使用的inode和脏inode也会放在inode_hashtable
这样一个hash结构中,但是,不同的inode的hash值可能相等,所以将hash值相等的这些inode通过这个i_hash
字段连接起来。参考fs/inode.c
/*
* Each inode can be on two separate lists. One is
* the hash list of the inode, used for lookups. The
* other linked list is the "type" list:
* "in_use" - valid inode, i_count > 0, i_nlink > 0
* "dirty" - as "in_use" but also dirty
* "unused" - valid inode, i_count = 0
*
* A "dirty" list is maintained for each super block,
* allowing for low-overhead inode sync() operations.
*/
LIST_HEAD(inode_in_use);
LIST_HEAD(inode_unused);
static struct hlist_head *inode_hashtable __read_mostly;
inode的大致组织图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OHuEQJir-1589775803226)(http://img.dnbcw.info/2010125/pxup2592612.gif)]
...
unsigned int i_nlink;
uid_t i_uid;
gid_t i_gid;
...
loff_t i_size;
...
struct timespec i_mtime;
...
umode_t i_mode;
...
i_op
是结构体inode_operations
的指针类型。该结构体类型定义在``文件中,VFS通过这个结构体定义了所有操作索引节点的虚函数,需要由具体的文件系统实现。具体内容参考LKD3rd的13.8章节。
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
};
...
void *i_private; /* fs or device private pointer */
如果我们做过驱动开发的话,会发现这些成员很熟悉,对,在驱动模块中它们会出现在文件操作函数的参数中传递到驱动的回调函数中。
目录项是描述文件的逻辑属性,和前面描述的超级块和索引节点不同,目录项对象没有对应的磁盘数据结构,它们是VFS根据需要在内存中现场创建出来的,更确切的说是存在于内存的目录项缓存,为了提高查找性能而设计,所以也不存在”脏”标记。注意不管是文件夹还是最终的文件,都是属于目录项,所有的目录项在一起构成一颗庞大的目录树。例如:open一个文件/home/xxx/yyy.txt
,需要涉及四个目录项,它们分别是/
、home
、xxx
和yyy.txt
,VFS在查找的时候,根据一层一层的目录项找到对应的每个目录项的inode,那么沿着目录项进行操作就可以找到最终的文件。
注意:
该结构体类型定义在``文件中,
struct dentry {
atomic_t d_count;
...
struct inode *d_inode; /* Where the name belongs to - NULL is
* negative */
...
struct dentry *d_parent; /* parent directory */
struct qstr d_name;
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 list_head d_alias; /* inode alias list */
...
const struct dentry_operations *d_op;
struct super_block *d_sb; /* The root of the dentry tree */
void *d_fsdata; /* fs-specific data */
unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */
};
dentry对象存在于三个双向链表中:
d_subdirs
成员维护了一个链表,用于管理其子entry。每个子entry通过d_child
域链接。d_count
为0),该目录项仍然保留在内存中以便需要时再使用,如果内存紧张时可以释放它们。我查了一下v2.6.32.2的代码,这个链表的链表头不是dentry_unused
,而是改为超级快的一个成员s_dentry_lru
。具体可以参考dentry_lru_add_tail
这个函数的实现。LRU的意思是Least Recently Used
,很少使用项,内存紧张时可以丢弃的意思。d_count
>0)。该项在缓存中不可以被丢弃。该链表的链表头维护在inode的i_dentry
成员。譬如表达硬链接的概念。inode_hashtable
(这个暂不介绍,主要用于加快查找速度)。更多有关所谓“正在被使用”和“未被使用”,包括源代码注释中的NULL is negative
的描述,涉及目录项的三个状态,主要和缓存的使用机制有关,详细参考LKD3的13.9.1章节。
以上概念参考下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZJasKfg2-1589775803227)(http://img.dnbcw.info/2010125/pxup2592613.gif)]
有关inode和dentry之间的关系,譬如VFS是如何利用它们表达硬链接,符号链接等概念的,可以参考一篇不错的博客VFS文件系统结构分析,上面有些图画的真心不错。
从进程的角度来看待虚拟文件系统。正如LKD3所述,如果我们从用户的角度来看待VFS,文件对象会首先进入我们的视野。进程直接处理的是文件,而不是超级块,索引节点以及目录项。
进程打开文件时涉及到的内核结构体:
namespace
。有关命名空间的概念,参考Linux Namespaces机制,我们这主要关心的应该是mnt_namespace
。每个进程都看到了一棵大树,这棵大树是怎么创建出来的,参考后面的”VFS的构建”。fs_struct
用于表示进程与文件系统之间的结构关系,比如当前的工作目录,进程的根目录等等。files_struct
用于维护当前进程打开的所有的文件。file
对象来表示。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P49pFDLM-1589775803229)(http://img.dnbcw.info/2010125/pxup2592614.gif)]
有关open等系统调用对进程打开文件影响的例子,依然参考VFS文件系统结构分析,上面有些图画的真心不错。
其中file
结构体值得仔细看看。
其主要成员
f_op
: file_operations
的结构体的指针。这些操作是标准Unix系统调用的基础,也是驱动开发中最多涉及的东东。和目录项dentry
一样,文件对象实际上没有对应的磁盘数据,所以结构体中没有“脏”标志。
file
和dentry
之间是多对一的关系,dentry
和inode
之间又是多对一的关系。讲到这里,以前一直困惑我们的一张关系图应该很明了了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uLFAaoSm-1589775803230)(http://blog.fangjian.me/images/uploads/2010/10/inode.jpg)]
fs_struct
files_struct
主要涉及挂载的概念。
所以说这里的列表维护的仅仅是当前支持的文件系统类型,具体一个文件系统的挂载情况通过另外一个结构体vfsmount
来表达。The vfsmount structure is defined in 。
系统维护了一张”文件系统安装表”,记录了所有挂载在当前系统上的挂载点的实例。
因为一个文件系统可以被挂载多次,所以一个超级快可以对应多个vfsmount
。
应用程序通过mount系统调用挂载文件系统时,
file_system_type
结构体的get_sb()
读该设备的超级块,建立该设备的内存超级快对象和根目录项对象vfsmount
并填写其各项内容,包括其指向的超级快指针,设备名,安装点,根目录项指针等。这么多结构体和对象,之间的对应关系,可以总结一下。LINUX VFS精华版PPT上有一张类图,但个人感觉不是很清楚。
VFS作为一个中间层,对下层的文件系统和驱动开发提出了哪些要求,即有哪些虚函数需要我们去支持。可以总结一下。
另外一张有关VFS和驱动开发之间的关系图可以参考宋宝华《Linux设备驱动开发详解(第2版)》的图5.1
其中:
字符设备:
VFS->字符设备文件(/dev/…)->字符设备驱动(直接提供file_operations)
块设备:
VFS->块设备文件系统(ext, fat, jffs等,由这些文件系统实现file_operations)->块设备驱动(看不到file_operations,是不是完全看不到,标识怀疑)。
这么多结构体和对象,之间的对应关系,可以总结一下。LINUX VFS精华版PPT上有一张类图,但个人感觉不是很清楚。
VFS作为一个中间层,对下层的文件系统和驱动开发提出了哪些要求,即有哪些虚函数需要我们去支持。可以总结一下。
另外一张有关VFS和驱动开发之间的关系图可以参考宋宝华《Linux设备驱动开发详解(第2版)》的图5.1
其中:
字符设备:
VFS->字符设备文件(/dev/…)->字符设备驱动(直接提供file_operations)
块设备:
VFS->块设备文件系统(ext, fat, jffs等,由这些文件系统实现file_operations)->块设备驱动(看不到file_operations,是不是完全看不到,标识怀疑)。
这张图还体现了一个概念,就是我们通过设备文件(/dev/…)直接访问设备,也通过驱动实现,这里指的是ioctl吗?