一、Page cache和buffer cache的区别
Page cache和buffer cache到底有什么区别呢?很多时候我们不知道系统在做IO操作的时候到底是走了page cache还是buffer cache?其实,buffer cache和page cache是Linux中两个比较简单的概念,在此对其总结说明。
Page cache是vfs文件系统层的cache,例如 对于一个ext3文件系统而言,每个文件都会有一棵radix树管理文件的缓存页,这些被管理的缓存页被称之为page cache。所以,page cache是针对文件系统而言的。例如,ext3文件系统的页缓存就是page cache。Buffer cache是针对设备的,每个设备都会有一棵radix树管理数据缓存块,这些缓存块被称之为buffer cache。通常对于ext3文件系统而言,page cache的大小为4KB,所以ext3每次操作的数据块大小都是4KB的整数倍。Buffer cache的缓存块大小通常由块设备的大小来决定,取值范围在512B~4KB之间,取块设备大小的最大公约数。具体关于buffer cache的块大小问题可以参见我的另一篇博文《Linux中Buffer cache性能问题一探究竟》见 http://www.linuxidc.com/Linux/2013-01/77573.htm 。
这里我们可以通过一个小实验来观察一下buffer cache和page cache的差别。运行top命令,我们可以看到实验机器当前内存使用情况:
可以看出内存总容量为16GB左右,page cache用了将近10GB(10209072K-303796K),buffer cache用了300MB(303796K),其余6GB(6255076K)空闲剩余。在这种情况下,如果对设备dm0进行裸盘写操作,即运行如下命令:
dd if=/dev/zero of=/dev/dm0 count=4096
那么,我么可以通过top命令发现,buffer cache的容量越来越大,空闲内存越来越少,相当一部分内存被buffer cache占用,并且在IO操作的过程中发现bdi(flush-254:176)线程在繁忙的进行数据回刷操作。
经过一段时间以后,大约6GB(6401432KB)的内存被buffer cache占用,89MB(89312KB)内存空闲,其余大约10GB(16374836KB - 6401432KB)的内存还是被page cache占用。通过这个实验,可以说明对于裸盘的读写操作会占用buffer cache,并且当读写操作完成之后,这些buffer cache会归还给系统。为了验证page cache的占用情况,我做了文件系统级的读写操作,运行如下命令进行文件系统写操作:
cp /dev/zero ./test_file
这是一次文件拷贝操作,因此会采用page cache对文件数据进行缓存。通过top工具我们可以看出在数据拷贝的过程中,page cache的容量越来越大,空闲内存数量急剧下降,而buffer cache保持不变。
拷贝一定时间之后,空闲内存将为1.9GB(1959468KB),文件系统page cache增长为将近14.5GB(14504680KB – 235108KB),buffer cache维持在235MB左右(235108KB)。
由此我们可以得出,page cache和buffer cache最大的差别在于:page cache是对文件数据的缓存;buffer cache是对设备数据的缓存。两者在实现上差别不是很大,都是采用radix树进行管理。
(内核2.4.37)
一、首先,看看磁盘,超级块,inode节点在物理上整体的分布情况:
(图示来自:www.daoluan.net)
对于一个分区,对应一个文件系统,一个文件系统其实本质上还是磁盘的一部分,所以可以包括多个柱面。对于柱面上的数据,主要看看inode节点位图、block位图,i节点,数据块。inode节点位图是为了记录inode节点的使用情况,之前的违章中已经说过,inode节点在文件系统安装的时候,会初始化所有的inode节点,那么形成的位图表示使用or没使用的大表。对于block块也是一样的,记录数据块使用情况。
对于inode节点来说,每个文件都会对应一个inode节点,目录项也会对应一个inode节点。对于一个文件来说,只对应一个inode节点,但是一个文件可以有多个数据块,因为一个比较大的文件,一个数据块根本存放不了。所以inode中记录多个文件数据块的信息。
对于目录块来说,主要是为了索引而存在,所以里面的内容主要是inode节点号和文件名,其实就是一个映射表形式的东西。
二、
上一篇中对于VFS有一个简单的介绍与理解,我们知道,与用户打交道的是VFS,然后VFS与底层真正的文件系统交流。那我们知道在一个VFS下面,允许存在很多的“文件系统类型”,例如ext2,ext3,ext4,sysfs,proc等等。这些类型是可以共存的,同时,对于每一个类型来说,可以存在多个文件系统实体,例如:在一个目录下有多个子目录,子目录之间的文件系统类型可以不一样,也可以有部分是一样的类型,借用windows中的例子说就是,D盘和E盘可以都是NTFS类型文件系统,也可以是不一样的文件类型系统。
在Linux中,系统有一个全局变量叫做file_systems,这个变量用来管理所有的“文件系统类型”链表。也就是所有的文件系统类型都必须注册到(链接到)这个链表中,才可以被使用。如果是自己的文件系统,只要符合VFS的标准,也是可以注册进去的。最终形成一个单链表形式结构。
而对于一个文件系统类型,使用file_system_type结构表示:
- <span style="font-size:14px;">995 struct file_system_type {
- 996 const char *name;
- 997 int fs_flags;
- 998 struct super_block *(*read_super) (struct super_block *, void *, int);
- 999 struct module *owner;
- 1000 struct file_system_type * next;
- 1001 struct list_head fs_supers;
- 1002 };</span>
name:文件系统类型名称,如ext2。
flags:安装文件类型标志,在fs.h中有定义。
read_super:各种文件系统读入其“超级块”的函数指针,不同的文件系统之间可能不一样,因此读入函数也不一样。
owner:如果这个文件系统是通过一个可安装模块实现的,那么这个指针指向这个模块。
next:这个就是链接到下一个“文件类型”的指针。
fs_supers:属于相同的文件系统类型的所有的super_blocks构成一个双向链表。在超级块中有一个s_instance就是连接这个双链表的连接点。(超级块在上一篇有介绍)
1、
那么根据上面的解释,一个大的框架图如下:
同时,在内核中,有一个全局的变量super_blocks用于将所有的suoer_block连接在一起,形成一个双向链表,这样就会发现s_list字段就有意义了!
如图:
2、我们在之前也提过,文件系统最终还是要和进程一起协作的,任何对于文件的操作都是基于进程的。由操作系统的知识我们知道,对于进程来说,管理进程的叫“进程控制块PCB”,这个在内核中的结构为:task_struct,这个是很复杂的一个结构体,对于进程来说,可以有自己的操作的文件,那么进程的文件的信息,也是包含在这个结构中:
- 283 struct task_struct {
- ... ...
- 391 /* filesystem information */
- 392 struct fs_struct *fs;
- 393 /* open file information */
- 394 struct files_struct *files;
- ... ...
- }
代码中的两个字段就是涉及进程的文件的字段。每个进程在PCB中保存着一份文件描述符表,文件描述符就是这个表的索引(数组的下标),每个表项都有一个指向已打开文件的指针。
代码中第一个字段fs:代码本进程自身的文件系统的信息。例如进程本身的根目录,挂载点,当前目录等信息。
代码中第二个字段files:保存着本进程涉及的所有的文件的信息的指针。files_struct结构之前已经说过:files_struct
里面有两个重要字段:
- 172 struct files_struct {
- ... ...
- 178 struct file ** fd; /* current fd array */
- ... ...
- 183 struct file * fd_array[NR_OPEN_DEFAULT];
- 184 };
对于一个文件数组来说例如:fd[],所谓“文件描述符”其实就是这个数组的下标!例如:默认0就是标准输入文件描述符,1是标准输出,2是标准错误。对于用户来说操作的是这个“文件描述符”,但是对于内核来说,“文件描述符”仅仅是为了找到对应的文件而已!然后所有的在内核中的操作,都是使用实际文件的file指针进行的!关于file结构体在上一篇也说了(之前files_struct链接)。延伸一下:我们在写C语言程序的时候会遇到两个函数,open和fopen。对于前者,返回的就是一个“文件描述符”,即那个文件数组的下标,对于fopen,返回的是一个FILE的指针,这里面其实除了“文件描述符”之外,还包括IO缓冲这些信息。文件指针FILE*更上层,FILE指针将文件描述符和缓冲区封装在一起。
OK,那么用户进程打开一个文件的具体的过程是什么呢?下面分析总结一下:首先我们知道用户使用open返回一个“文件描述符”(具体怎么获得,之后再说),然后在进程PCB中,即task_struct文件数组中找到对应“文件描述符”(数组下标)的文件(file)指针,在file结构体中,f_dentry记录了这个文件的完整目录项,一般在内存中会有dentry的缓存,通过这个我们可以找到文件的inode。对于一个dentry来说,也是有自己的inode,目录名称之类信息。总之通过dentry,可以找到最终文件的inode,找到inode之后,就可以定位到具体的文件数据在磁盘上的位置了!
对于上面的过程,整体的一张图如下:
3、多个进程和多个文件之间的关系:
对于一个进程来说,可以打开多个文件,也可以多个进程打开一个文件,对于文件来说,不变的永远是自己的inode节点,变化的仅仅是和进程直接关系的file结构。可以看一下下面的大图: