8.3.1  块高速缓存

Linux支持的文件系统大多以块的形式组织文件,为了减少对物理块设备的访问,在文件以块的形式调入内存后,使用块高速缓存(buffer_cache)对它们进行管理。每个缓冲区由两部分组成,第一部分称为缓冲区首部,用数据结构buffer_head表示,第二部分是真正的缓冲区内容(即所存储的数据)。由于缓冲区首部不与数据区域相连,数据区域独立存储。因而在缓冲区首部中,有一个指向数据的指针和一个缓冲区长度的字段。图8.6给出了一个缓冲区的格式。

缓冲区首部包含:

·用于描述缓冲区内容的信息,包括:所在设备号、起始物理块号、包含在缓冲区中的字节数。

·缓冲区状态的域:是否有有用数据、是否正在使用、重新利用之前是否要写回磁盘等。

·用于管理的域。

         


                  8.6 缓冲区格式

buffer-head数据结构在include\linux\fs.h中定义如下:

/*

  * Try to keep the most commonly used fields in single cache lines (16

  * bytes) to improve performance.  This ordering should be

  * particularly beneficial on 32-bit processors.

  *

  * We use the first 16 bytes for the data which is used in searches

  * over the block hash lists (ie. getblk() and friends).

  *

  * The second 16 bytes we use for lru buffer scans, as used by

  * sync_buffers() and refill_freelist().  -- sct

  */

struct buffer_head {

        /* First cache line: */

         struct buffer_head *b_next;     /* 哈希队列链表t */

         unsigned long b_blocknr;        /* 逻辑块号 */

         unsigned short b_size;          /* 块大小 */

         unsigned short b_list;          /* 本缓冲区所出现的链表 */

         kdev_t b_dev;                   /* 虚拟设备标示符(B_FREE = free) */

 

         atomic_t b_count;               /* 块引用计数器 */

          kdev_t b_rdev;                  /* 实际设备标识符*/

         unsigned long b_state;          /* 缓冲区状态位图 */

unsigned long b_flushtime;      /* 对脏缓冲区进行刷新的时间*/

         struct buffer_head *b_next_free;/* 指向lru/free 链表中的下一个元素 */

         struct buffer_head *b_prev_free;/* 指向链表中的上一个元素*/

 struct buffer_head *b_this_page;/* 每个页面中的缓冲区链表*/

         struct buffer_head *b_reqnext;  /*请求队列 */

 

         struct buffer_head **b_pprev;   /* 哈希队列的双向链表 */

        char * b_data;                  /* 指向数据块 */

        struct page *b_page;            /* 这个bh所映射的页面*/

void (*b_end_io)(struct buffer_head *bh, int uptodate); /* I/O结束方法*/

         void *b_private;                /* b_end_io保留 */

 

        unsigned long b_rsector;        /* 缓冲区在磁盘上的实际位置*/

         wait_queue_head_t b_wait;     /* 缓冲区等待队列 */

 

         struct inode *       b_inode;

struct list_head    b_inode_buffers;   /* inode脏缓冲区的循环链表*/

};

 

其中缓冲区状态在fs.h中定义为枚举类型:

/* bh state bits */

enum bh_state_bits {

        BH_Uptodate,    /* 如果缓冲区包含有效数据则置1 */

        BH_Dirty,       /* 如果缓冲区数据被改变则置1 */

        BH_Lock,        /* 如果缓冲区被锁定则置1*/

        BH_Req,         /* 如果缓冲区数据无效则置0  */

        BH_Mapped,      /* 如果缓冲区有一个磁盘映射则置1 */

        BH_New,         /* 如果缓冲区为新且还没有被写出则置1 */

        BH_Async,       /* 如果缓冲区是进行end_buffer_io_async I/O 同步则置1 */

        BH_Wait_IO,     /* 如果我们应该把这个缓冲区写出则置1 */

        BH_launder,     /* 如果我们应该“清洗”这个缓冲区则置1 */

        BH_JBD,         /* 如果与journal_head相连接则置1 */

 

        BH_PrivateStart,/* 这不是一个状态位,但是,第一位由其他实体用于私有分配*/

                        

显然一个缓冲区可以同时具有上述状态的几种。

块高速缓存的管理很复杂,下面先对空缓冲区、空闲缓冲区、正使用的缓冲区、缓冲区的大小以及缓冲区的类型作一个简短的介绍:

缓冲区可以分为两种,一种是包含了有效数据的,另一种是没有被使用的,即空缓冲区。

具有有效数据并不能表明某个缓冲区正在被使用,毕竟,在同一时间内,被进程访问的缓冲区(即处于使用状态)只有少数几个。当前没有被进程访问的有效缓冲区和空缓冲区称为空闲缓冲区。其实,buffer_head结构中的b_count就可以反映出缓冲区是否处于使用状态。如果它为0,则缓冲区是空闲的。大于0,则缓冲区正被进程访问。

缓冲区的大小不是固定的,当前Linux支持5种大小的缓冲区,分别是5121024204840968192字节。Linux所支持的文件系统都使用共同的块高速缓存,在同一时刻,块高速缓存中存在着来自不同物理设备的数据块,为了支持这些不同大小的数据块,Linux使用了几种不同大小的缓冲区。

当前的Linux缓冲区有3种类型,在include/linux/fs.h中有如下的定义:

#define BUF_CLEAN        0       /*未使用的、干净的缓冲区*/

#define BUF_LOCKED       1       /*被锁定的缓冲区,正等待写入*/

#define BUF_DIRTY        2      /*脏的缓冲区,其中有有效数据,需要写回磁盘*/

 

VFS使用了多个链表来管理块高速缓存中的缓冲区。

首先,对于包含了有效数据的缓冲区,用一个哈希表来管理,用hash_table来指向这个哈希表。哈希索引值由数据块号以及其所在的设备标识号计算(散列)得到。所以在buffer_head这个结构中有一些用于哈希表管理的域。使用哈希表可以迅速地查找到所要寻找的数据块所在的缓冲区。

对于每一种类型的未使用的有效缓冲区,系统还使用一个LRU(最近最少使用)双链表管理,即lru-list链。由于共有三种类型的缓冲区,所以有三个这样的LRU链表。当需要访问某个数据块时,系统采取如下算法:

首先,根据数据块号和所在设备号在块高速缓存中查找,如果找到,则将它的b-count域加1,因为这个域正是反映了当前使用这个缓冲区的进程数。如果这个缓冲区同时又处于某个LRU链中,则将它从LRU链中解开。

如果数据块还没有调入缓冲区,则系统必须进行磁盘I/O操作,将数据块调入块高速缓存,同时将空缓冲区分配一个给它。如果块高速缓存已满(即没有空缓冲区可供分配),则从某个LRU链首取下一个,先看是否置了“脏”位,如已置,则将它的内容写回磁盘。然后清空内容,将它分配给新的数据块。

在缓冲区使用完了后,将它的b_count域减1,如果b_count变为0,则将它放在某个LRU链尾,表示该缓冲区已可以重新利用。

为了配合以上这些操作,以及其它一些多块高速缓存的操作,系统另外使用了几个链表,主要是:

·对于每一种大小的空闲缓冲区,系统使用一个链表管理,即free_list链。

·对于空缓冲区,系统使用一个unused_list链管理。

以上几种链表都在fs/buffer.c定义。

Linux中,用bdflush守护进程完成对块高速缓存的一般管理。bdflush守护进程是一个简单的内核线程,在系统启动时运行,它在系统中注册的进程名称为 kflushd,你可以使用ps命令看到此系统进程。它的一个作用是监视块高速缓存中的“脏”缓冲区,在分配或丢弃缓冲区时,将对“脏”缓冲区数目作一个统计。通常情况下,该进程处于休眠状态,当块高速缓存中“脏”缓冲区的数目达到一定的比例,默认是60%,该进程将被唤醒。但是,如果系统急需,则在任何时刻都可能唤醒这个进程。使用update命令可以看到和改变这个数值。

# update -d

 

当有数据写入缓冲区使之变成“脏”时,所有的“脏”缓冲区被连接到一个BUF_DIRTY_LRU链表中,bdflush会将适当数目的缓冲区中的数据块写到磁盘上。这个数值的缺省值为500,可以用update命令改变这个值。

另一个与块高速缓存管理相关的是update命令,它不仅仅是一个命令,还是一个后台进程。当以超级用户的身份运行时(在系统初始化时),它将周期性调用系统服务例程将老的“脏”缓冲区中内容“冲刷”到磁盘上去。它所完成的这个工作与bdflush类似,不同之处在于,当一个“脏”缓冲区完成这个操作后, 它将把写入到磁盘上的时间标记到buffer_head结构中。update每次运行时它将在系统的所有“脏”缓冲区中查找那些“冲刷”时间已经超过一定期限的,这些过期缓冲区都要被写回磁盘。