Linux 系统中的buffer 与cache

一、Buffer cache简介
1、名称:buffer cache,又称bcache,其中文名称为缓冲器高速缓冲存储器,简称缓冲器高缓。另外,buffer cache按照其工作原理,又被称为块高缓。

  2、功能:在linux读写文件时,它用于缓存物理磁盘上的磁盘块,从而加快对磁盘上数据的访问。

3、大小:buffer cache的内容对应磁盘上一个块(block),块通常为1K,都是连续的。
       在linux下,为了更有效的使用物理内存,操作系统自动使用所有空闲内存作为Buffer Cache使用。当程序需要更多内存时,操作系统会自动减小Cache的大小
                       如果cache占用的内存过多了,影响正常运行程序需要的内存,那么会释放掉一部分cache内存,但是总量会保持一个很高的值,所以,linux总是能最大限度的使用内存,就算加到16G,32G内存,也会随着不断的IO操作,内存的free值会慢慢减少到只有几M,想要内存不发生这种情况,只有一个办法:把内存加到比硬盘大。

       在linux下,可通过命令cat /proc/meminfo和free -m查看buffer cache的内存使用情况。

二、Buffer cache的功能详解
在从外存的一页到内存的一页的映射过程中,page cache与buffer cache、swap cache共同实现了高速缓存功能,以下是其简单映射图,

外存的一页(分解为几块,可能不连续)
|
|
物理磁盘的磁盘块
|
|
内存的buffer Cache
|
|
内存的一页(由一个页框划分的几个连续buffer cache构成)
|
|
页高缓系统

在这个过程中,物理文件系统与Buffer Cache交互,负责在外围存储设备和Buffer Cache 之间交换数据。
由于bcache位于物理文件系统和块设备驱动程序之间,因此,当物理文件系统需要从块设备上读取数据时,它首先试图从bcache中去读。如果命中,则内核就不必在去访问慢速的块设备。否则如果命中失败,也即数据不在bcache中,则内核从块设备上读取相应的数据块,并将其在bcache中缓存起来,以备下次访问之用。
类似地,但物理文件系统需要向块设备上写数据时,也是先将数据写到相应的缓冲区中,并将这个缓冲区标记为脏(dirty),然后在将来的某些时候将buffer cache中的数据真正地回写到块设备上,或者将该缓冲区直接丢弃。从而实现减少磁盘写操作的频率

三、Buffer Cache的数据结构
  1、缓冲区头部对象buffer_head
  每一个缓冲区都有一个缓冲区头部来唯一地标识与描述该缓冲区。Linux通过数据结构buffer_head来定义缓冲区头部。如下所示(include/linux/fs.h)

struct buffer_head {
/* First cache line: */
struct buffer_head *b_next; /* Hash queue list */
unsigned long b_blocknr; /* block number */
unsigned short b_size; /* block size */
unsigned short b_list; /* List that this buffer appears */
kdev_t b_dev; /* device (B_FREE = free) */

atomic_t b_count; /* users using this block */
kdev_t b_rdev; /* Real device */
unsigned long b_state; /* buffer state bitmap (see above) */
unsigned long b_flushtime; /* Time when (dirty) buffer should be written */

struct buffer_head *b_next_free;/* lru/free list linkage */
struct buffer_head *b_prev_free;/* doubly linked list of buffers */
struct buffer_head *b_this_page;/* circular list of buffers in one page */
struct buffer_head *b_reqnext; /* request queue */

struct buffer_head **b_pprev; /* doubly linked list of hash-queue */
char * b_data; /* pointer to data block (512 byte) */
struct page *b_page; /* the page this bh is mapped to */
void (*b_end_io)(struct buffer_head *bh, int uptodate); /* I/O completion */
void *b_private; /* reserved for b_end_io */

unsigned long b_rsector; /* Real buffer location on disk */
wait_queue_head_t b_wait;

struct inode * b_inode;
struct list_head b_inode_buffers; /* doubly linked list of inode dirty buffers */
};
各字段的含义如下:
1b_next指针:指向哈希链表中的下一个buffer_head对象。
2.b_blocknr:本缓冲区对应的块号(block number)。
3.b_size:以字节计掉的块长度。合法值为:512、1024、2048、4096、8192、16384和32768。
4.b_list:记录这个缓冲区应该出现在哪个链表上。
5.d_dev:缓冲区对应的块所在的块设备标识符(对于位于free_list链表中的缓冲区,b_dev=B_FREE)。
6.b_count:本缓冲区的引用计数。
7.b_rdev:缓冲区对应的块所在的块设备的「真实」标识符。
8.b_state:缓冲区的状态,共有6种:
/* bh state bits */
#define BH_Uptodate 0 /* 1 if the buffer contains valid data */
#define BH_Dirty 1 /* 1 if the buffer is dirty */
#define BH_Lock 2 /* 1 if the buffer is locked */
#define BH_Req 3 /* 0 if the buffer has been invalidated */
#define BH_Mapped 4 /* 1 if the buffer has a disk mapping */
#define BH_New 5 /* 1 if the buffer is new and not yet written out */
#define BH_Protected 6 /* 1 if the buffer is protected */
9.b_flushtime:脏缓冲区必须被回写到磁盘的最后期限值。
10.b_next_free指针:指向lru/free/unused链表中的下一个缓冲区头部对象。
11b_prev_free指针:指向lru/free/unused链表中的前一个缓冲区头部对象。
12b_this_page指针:指向同属一个物理页帧的下一个缓冲区的相应缓冲区头部对象。同属一个物理页帧的所有缓冲区通过这个指针成员链接成一个单向循环链表。
13b_reqnext指针:用于块设备驱动程序的请求链表。
14b_pprev:哈希链表的后向指针。
15b_data指针:指向缓冲区数据块的指针。
16b_page指针:指向缓冲区所在物理页帧的page结构。
17b_rsector:实际设备中原始扇区的个数。
18b_wait:等待这个缓冲区的等待队列。
19b_inode指针:如果缓冲区属于某个索引节点,则这个指针指向所属的inode对象。
20b_inode_buffers指针:如果缓冲区为脏,且又属于某个索引节点,那么就通过这个指针链入inode的i_dirty_buffers链表中。

缓冲区头部对象buffer_head可以被看作是缓冲区的描述符,因此,对bcache中的缓冲区的管理就集中在如何高效地组织处于各种状态下的buffer_head对象上。

  2、buffer_head对象的SLAB分配器缓存
  缓冲区头部对象buffer_head本身有一个叫做bh__cachep的slab分配器缓存。因此对buffer_head对象的分配与销毁都要通过kmem_cache_alloc()函数和kmem_cache_free()函数来进行。
  注意不要把bh_cachep SLAB分配器缓存和缓冲区本身相混淆。前者只是buffer_head对象所使用的内存高速缓存,并不与块设备打交道,而仅仅是一种有效管理buffer_head对象所占用内存的方式。后者则是块设备中的数据块所使用的内存高速缓存。但是这二者又是相互关联的,也即缓冲区缓存的实现是以bh_cachep SLAB分配器缓存为基础的。而我们这里所说的bcache机制包括缓冲区头部和缓冲区本身这两个方面的概念。
  bh_cachep定义在fs/dcache.c文件中,并在函数vfs_caches_init()中被初始化,也即通过调用kmem_cache_create()函数来创建bh_cachep这个SLAB分配器缓存。
  注:函数vfs_caches_init()的工作就是调用kmem_cache_create()函数来为VFS创建各种SLAB分配器缓存,包括:names_cachep、filp_cachep、dquot_cachep和bh_cachep等四个SLAB分配器缓存。

3、bcache中的缓冲区头部对象链表
  一个缓冲区头部对象buffer_head总是处于以下四种状态之一:
    1未使用(unused)状态:该对象是可用的,但是其b_data指针为NULL,也即这个缓冲区头部没有和一个缓冲区相关联。
    2空闲(free)状态:其b_data指针指向一个空闲状态下的缓冲区(也即该缓冲区没有具体对应块设备中哪个数据块);而b_dev域值为B_FREE(值为0xffff)。
    3正在使用(inuse)状态:其b_data指针指向一个有效的、正在使用中的缓冲区,而b_dev域则指明了相应的块设备标识符,b_blocknr域则指明了缓冲区所对应的块号。
    4异步(async)状态:其b_data域指向一个用来实现page I/O操作的临时缓冲区。
    
  为了有效地管理处于上述这些不同状态下的缓冲区头部对象,bcache机制采用了各种链表来组织这些对象(这一点,bcache机制与VFS的其它cache机制是相同的):
    1哈希链表:所有buffer_head对象都通过其b_next与b_pprev两个指针域链入哈希链表中,从而可以加快对buffer_head对象的查找(lookup)。
    2最近最少使用链表lru_list:每个处在inuse状态下的buffer_head对象都通过b_next_free和b_prev_free这两个指针链入某一个lru_list链表中。
    3空闲链表free_list:每一个处于free状态下的buffer_head对象都根据它所关联的空闲缓冲区的大小链入某个free_list链表中(也是通过b_next_free和b_prev_free这两个指针)。
    4未使用链表unused_list:所有处于unused状态下的buffer_head对象都通过指针域b_next_free和b_prev_free链入unused_list链表中。
    5inode对象的脏缓冲区链表i_dirty_buffers:如果一个脏缓冲区有相关联的inode对象的话,那么他就通过其b_inode_buffers指针域链入其所属的inode对象的i_dirty_buffers链表中。
    (更详细的介绍请见参考资料二)

四、buffer cache的回写
  有些是直接写(write-through):数据将被立刻写入磁盘,当然,数据也被放入缓存中。如果写操作是在以后做的,那么该缓存被称为后台写(write-back)。后台写比直接写更有效,但也容易出错:如果机器崩溃,或者突然掉电,缓冲中改变过的数据就被丢失了。如果仍未被写入的数据含有重要的薄记信息,这甚至可能意味着文件系统(如果有的话)已不完整。
  针对以上的原因,出现了很多的日志文件系统,数据在缓冲区修改后,同时会被文件系统记录修改信息,这样即使此时系统掉电,系统重启后会首先从日志记录中恢复数据,保证数据不丢失。当然这些问题不再本文的叙述范围。
  由于上述原因,在使用适当的关闭过程之前,绝对不要关掉电源,sync命令可以清空(flushes)缓冲,也即,强迫所有未被写的数据写入磁盘,可用以确定所有的写操作都已完成。在传统的 UNIX系统中,有一个叫做update(kupdate)的程序运行于后台,每隔30秒做一次sync操作,因此通常无需手工使用sync命令了。Linux另外有一个后台程序,bdflush,这个程序执行更频繁的但不是全面的同步操作,以避免有时sync的大量磁盘I/O操作所带来的磁盘的突然冻结。

五、Buffer Cache和Page Cache及其它
  page不会同时存在于buffer cache和page cache。add_page_to_hash_queue将此思想显露无余。buffer_head 定义在fs.h,和文件系统有着更为紧密的关系。从文件读写角度看buffer cache缓存文件系统的管理信息像root entry, inode等,而page cache缓存文件的内容。
  注意函数block_read_full_page,虽然位于buffer.c,但并没有使用buffer cache. 但是确实使用了buffer:只是再指定page上创建buffer提交底层驱动读取文件内容.这个流程有两个值得注意的地方:
  一是普通file的read通过page cache进行
  二是page cache读取的时候不和buffer cache进行同步
  三是page cache的确使用了buffer,不过注意,buffer 不是buffer cache。

  2.4的改进:page cache和buffer cache耦合得更好了。在2.2里,磁盘文件的读使用page cache,而写绕过page cache,直接使用buffer cache,因此带来了同步的问题:写完之后必须使用update_vm_cache()更新可能有的page cache。2.4中page cache做了比较大的改进,文件可以通过page cache直接写了,page cache优先使用high memory。而且,2.4引入了新的对象:file address space,它包含用来读写一整页数据的方法。这些方法考虑到了inode的更新、page cache处理和临时buffer的使用。page cache和buffer cache的同步问题就消除了。原来使用inode+offset查找page cache变成通过file address space+offset;原来struct page 中的inode成员被address_space类型的mapping成员取代。这个改进还使得匿名内存的共享成为可能(这个在2.2很难实现,许多讨论过)。

留下评论

邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据