(By 詹荣开)
Copyright © 2002 by 詹荣开
E-mail:zhanrk@sohu.com
Linux-2.4.0
Version 1.0.0,2002-9-16
摘要:本文主要从内核实现的角度分析Linux 2.4.0内核虚拟文件系统(VFS)中的缓冲区缓存(Buffer Cache)的实现原理。本文是为那些想要深入分析Linux文件系统原理的读者而写的。
关键词:文件系统、虚拟文件系统、VFS、缓冲区缓存
申明:这份文档是按照*软件开放源代码的精神发布的,任何人可以免费获得、使用和重新发布,但是你没有限制别人重新发布你发布内容的权利。发布本文的目的是希望它能对读者有用,但没有任何担保,甚至没有适合特定目的的隐含的担保。更详细的情况请参阅GNU通用公共许可证(GPL),以及GNU*文文件协议(GFDL)。
你应该已经和文档一起收到一份GNU通用公共许可证(GPL)的副本。如果还没有,写信给:
The Free Software Foundation, Inc., 675 Mass Ave, Cambridge,MA02139, USA
欢迎各位指出文档中的错误与疑问。
Table of Contents
Chapter 1 概述 …...……………………………………………………………….…1
Chapter 2 Buffer Cache的数据结构 …………………………………………….….2
2. 1 缓冲区头部对象 buffer_head……………………………………………………….2
2. 2 buffer_head对象的 SLAB分配器缓存 ……………………………………………..5
2. 3 bcache中的缓冲区头部对象链表 …………………………………………………..6
2. 4 对 buffer_head对象链表的操作 ……………………………………………..……12
Chapter 3 对缓冲区的操作 ………………………………………………………..19
3. 1 缓冲区的分配 ………………………………………………………………..…….18
3. 2 缓冲区访问接口 getblk/ brelse…………………………………………..……….22
3. 3数据块读接口 bread…………………………………………………….………….24
3. 4 对 inode的 i_dirty_buffers链表中的脏缓冲区的操作 ………………..…………24
Chapter 4 bcache中脏缓冲区的同步机制 ………………………………….…….29
4. 1 bcache中的脏缓冲区同步操作 …………………………………………..………..29
4. 2 缓冲区同步的系统调用 ………………………………………………..………….32
4. 3 bdflush内核线程 ……………………………………………………….…………..33
4. 4 kupdate内核线程 …………………………………………………………………..37
Chapter 1 概述
我们都知道, UNIX 操作系统通过在物理文件系统和块设备驱动程序之间引入了「缓冲区缓存」( Buffer Cache,以下简称 bcache)这一软件 cache机制,从而大大降低了操作系统内核对块设备的存取频率(实际上,包括 Windows在内的大多数操作系统也是这么做的)。
由于 bcache位于物理文件系统和块设备驱动程序之间,因此,但物理文件系统需要从块设备上读取数据时,它首先试图从 bcache中去读。如果命中,则内核就不必在去访问慢速的块设备。否则如果命中失败,也即数据不在 bcache中,则内核从块设备上读取相应的数据块,并将其在 bcache中缓存起来,以备下次访问之用。
类似地,但物理文件系统需要向块设备上写数据时,也是先将数据写到相应的缓冲区中,并将这个缓冲区标记为脏( dirty),然后在将来的某些时侯将 buffer cache中的数据真正地回写到块设备上,或者将该缓冲区直接丢弃。从而实现减少磁盘写操作的频率。
Chapter 2 Buffer Cache 的数据结构
2 .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 */
};
各字段的含义如下:
1 .b_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链表中的下一个缓冲区头部对象。
⑾ b_prev_free 指针:指向lru/free/unused链表中的前一个缓冲区头部对象。
⑿ b_this_page 指针:指向同属一个物理页帧的下一个缓冲区的相应缓冲区头部对象。同属一个物理页帧的所有缓冲区通过这个指针成员链接成一个单向循环链表。
⒀ b_reqnext 指针:用于块设备驱动程序的请求链表。
⒁ b_pprev :哈希链表的后向指针。
⒂ b_data 指针:指向缓冲区数据块的指针。
⒃ b_page 指针:指向缓冲区所在物理页帧的page结构。
⒄ b_rsector :实际设备中原始扇区的个数。
⒅ b_wait :等待这个缓冲区的等待队列。
⒆ b_inode 指针:如果缓冲区属于某个索引节点,则这个指针指向所属的inode对象。
⒇ b_inode_buffers 指针:如果缓冲区为脏,且又属于某个索引节点,那么就通过这个指针链入inode的i_dirty_buffers链表中。
缓冲区头部对象buffer_head可以被看作是缓冲区的描述符,因此,对bcache中的缓冲区的管理就集中在如何高效地组织处于各种状态下的buffer_head对象上。
2.2 buffer_head对象的SLAB分配器缓存
缓冲区头部对象buffer_head本身有一个叫做bh__cachep的slab分配器缓存。因此对buffer_head对象的分配与销毁都要通过kmem_cache_alloc()函数和kmem_cache_free()函数来进行。
NOTE!不要把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分配器缓存。
2.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链表中。
5. inode对象的脏缓冲区链表i_dirty_buffers:如果一个脏缓冲区有相关联的inode对象的话,那么他就通过其b_inode_buffers指针域链入其所属的inode对象的i_dirty_buffers链表中。
下面,我们分别详细阐述上述这些链表。
2.3.1 哈希链表
内核对buffer_head对象的查找是相当频繁的,因此为了加快查找速度,bcache机制使用哈希链表来管理bcache中的每一个buffer_head对象。
每一个buffer_head对象都根据其中的设备标识符b_dev和块号b_blocknr来确定他所属的哈希链表,并通过b_next和b_pprev这两个指针域链入他应该所属的哈希链表中。每个哈希链表表头都是一个buffer_head类型的指针,所有的表头指针放在一起就组成一个哈希链表表头指针数组,该数组的首地址由变量hash_table定义。有关哈希链表的定义如下(Buffer.c):
static unsigned int bh_hash_mask;
static unsigned int bh_hash_shift;
static struct buffer_head **hash_table;
static rwlock_t hash_table_lock = RW_LOCK_UNLOCKED;
其中,bh_hash_mask和bh_hash_shift变量的含义与icache机制中inode哈希链表的i_hash_mask和i_hash_shift的含义相同。而读写锁hash_table_lock则用来对buffer_head哈希链表进行访问(读、写)保护,以实现对哈希链表的互斥访问。
哈希链表表头数组hash_table的初始化是在buffer_init()函数中完成的。该函数实现整个bcache机制的初始化工作。
n 哈希函数(也称为「散列」函数)
二元组(设备标识符,块号)唯一地确定bcache中的一个buffer_head对象。宏_hashfn()被定义为用来计算一个二元组(dev,block)所对应的散列值(buffer.c):
#define _hashfn(dev,block) /
((((dev)<<(bh_hash_shift - 6)) ^ ((dev)<<(bh_hash_shift - 9))) ^ /
(((block)<<(bh_hash_shift - 6)) ^ ((block) >> 13) ^ /
((block) << (bh_hash_shift - 12))))
#define hash(dev,block) hash_table[(_hashfn(HASHDEV(dev),block) & bh_hash_mask)]
而宏hash()则根据宏_hashfn()生成的散列值来索引hash_table数组,从而得到二元组(dev,block)所确定的buffer_head对象所属哈希链表的表头指针。
2.3.2 未使用的buffer_head对象链表unused_list
所有处于unused状态下的buffer_head对象都通过b_next_free和b_prev_free指针链入未使用链表unused_list中。变量unused_list定义了未使用链表的表头指针,如下所示(buffer.c):
static struct buffer_head * unused_list;
static int nr_unused_buffer_heads;
static spinlock_t unused_list_lock = SPIN_LOCK_UNLOCKED;
static DECLARE_WAIT_QUEUE_HEAD(buffer_wait);
其中,变量nr_unused_buffer_heads表示unused_list链表中buffer_head对象的个数,而自旋锁unused_list_lock则是unused_list链表的访问保护锁,用以实现对unused_list链表的互斥访问。
宏MAX_UNUSED_BUFFERS(通常值为36)定义了unused_list链表中buffer_head对象的最大个数。而宏NR_RESERVED(通常为16)则定义了unused_list链表中buffer_head对象的最少个数。
当缓冲区首部对象不再被使用时,如果unused_list链表元素个数小于MAX_UNUSED_BUFFERS值时,就将其插入到unused_list链表中;否则就将其直接释放给bh_cachep这个SLAB分配器缓存。而当需要一个buffer_head对象时,只要unused_list链表元素个数不小于NR_RESERVED,那就应该首先从unused_list链表中进行分配;只有在unused_list链表元素个数小于NR_RESERVED时,才从bh_cachep这个SLAB分配器缓存中分配。
unused_list链表中的NR_RESERVED个元素被保留给页I/O操作,内核使用这个子集来避免由于缺乏空闲缓冲区头部而产生死锁。
综上所述,unused_list链表可以被看作是buffer_head对象的SLAB分配器缓存bh_cachep和bcache机制之间的一个中间辅助缓存。
n 从unused_list链表中获取一个buffer_head对象
Linux在buffer.c文件中实现了函数get_unused_buffer_head(),用来从unused_list链表中得到一个未使用的buffer_head对象。该函数实际上是一个基于kmem_cache_alloc()函数的高层分配接口,如下所示:
/*
* Reserve NR_RESERVED buffer heads for async IO requests to avoid
* no-buffer-head deadlock. Return NULL on failure; waiting for
* buffer heads is now handled in create_buffers().
*/
static struct buffer_head * get_unused_buffer_head(int async)
{
struct buffer_head * bh;
spin_lock(&unused_list_lock);
if (nr_unused_buffer_heads > NR_RESERVED) {
bh = unused_list;
unused_list = bh->b_next_free;
nr_unused_buffer_heads--;
spin_unlock(&unused_list_lock);
return bh;
}
spin_unlock(&unused_list_lock);
/* This is critical. We can't swap out pages to get
* more buffer heads, because the swap-out may need
* more buffer-heads itself. Thus SLAB_BUFFER.
*/
if((bh = kmem_cache_alloc(bh_cachep, SLAB_BUFFER)) != NULL) {
memset(bh, 0, sizeof(*bh));
init_waitqueue_head(&bh->b_wait);
return bh;
}
/*
* If we need an async buffer, use the reserved buffer heads.
*/
if (async) {
spin_lock(&unused_list_lock);
if (unused_list) {
bh = unused_list;
unused_list = bh->b_next_free;
nr_unused_buffer_heads--;
spin_unlock(&unused_list_lock);
return bh;
}
spin_unlock(&unused_list_lock);
}
#if 0
/*
* (Pending further analysis ...)
* Ordinary (non-async) requests can use a different memory priority
* to free up pages. Any swapping thus generated will use async
* buffer heads.
*/
if(!async &&
(bh = kmem_cache_alloc(bh_cachep, SLAB_KERNEL)) != NULL) {
memset(bh, 0, sizeof(*bh));
init_waitqueue_head(&bh->b_wait);
return bh;
}
#endif
return NULL;
}
该函数的注释如下:
1.如果链表元素个数nr_unused_buffer_heads大于NR_RESERVED,则可以直接从unused_list链表中摘取一个buffer_head对象返回给调用者(从链表的首部摘除),因此将unused_list指针修改为unused_list->b_next_free,然后将nr_unused_buffer_heads减1后将可以直接返回了。
2.否则,就调用kmem_cache_alloc()函数bh_cachep SLAB分配器缓存中试图分配一个新的buffer_head对象。在内存不紧张的情况下,这应该会分配成功。然后就对新分配的buffer_head对象做初始化,并返回其指针。
3.如果kmem_cache_alloc()分配失败,则说明当前系统的内存紧张。因此此时就根据参数async判断是否在为异步的页I/O操作分配一个buffer_head对象。如果不是(async=0),那就直接返回NULL给调用者。如果是,则在unused_list链表不为空的情况下,从unused_list链表中为页I/O保留的NR_RESERVED个buffer_head对象中摘下一个返回给调用者,以保证页I/O操作总能成功进行;否则如果unused_list链表为空的话,那就也只好返回NULL了。
n 释放一个buffer_head对象给unused_list链表
当一个buffer_head对象不再使用时,应该调用__put_unused_buffer_head()函数将其释放给unused_list链表或bh_cachep SLAB分配器缓存。如下所示(fs/buffer.c):
/*
* Note: the caller should wake up the buffer_wait list if needed.
*/
static __inline__ void __put_unused_buffer_head(struct buffer_head * bh)
{
if (bh->b_inode)
BUG();
if (nr_unused_buffer_heads >= MAX_UNUSED_BUFFERS) {
kmem_cache_free(bh_cachep, bh);
} else {
bh->b_blocknr = -1;
init_waitqueue_head(&bh->b_wait);
nr_unused_buffer_heads++;
bh->b_next_free = unused_list;
bh->b_this_page = NULL;
unused_list = bh;
}
}
可以看出,但nr_unused_buffer_head大于或等于MAX_UNUSED_BUFFERS时(表示unused_list链表已满),于是就调用kmem_cache_free()函数将这个不再使用的缓冲区头部对象直接释放回给bh_cachep SLAB分配器缓存;否则,就将其链入unused_list链表的首部,并相应地设置buffer_head对象中的某些成员,以表示这是一个处于unused状态下的缓冲区头部对象。
2.3.3 缓冲区头部对象的空闲链表free_list
不同逻辑块设备的数据块(并非扇区,扇区是物理块设备组织数据的单位,通常是512字节,数据块的大小必须是扇区大小的整数倍)大小通常都不同。Linux中允许有NR_SIZES(值为7,定义在buffer.c中)种大小的块,分别为:512、1024、2048、4096、8192、16384和32768字节。由于缓冲区与块是对应的,因此缓冲区的大小也相应地有7种。但是由于一个块的大小不能超过物理页帧的大小,因此在i386体系结构中实际上只能使用前4种大小的块。
Linux将同样大小的空闲缓冲区的头部对象通过其中的b_next_free和b_prev_free指针域链成一条循环链表,并用结构类型bh_free_head来描述该链表(表头指针和保护锁),如下所示(fs/buffer.c):
struct bh_free_head {
struct buffer_head *list;
spinlock_t lock;
};
而所有7条空闲缓冲区的头部对象链表一起组成一个数组,定义如下:
static struct bh_free_head free_list[NR_SIZES];
为了更方便地根据块大小找到相应的空闲缓冲区头部对象链表,Linux在buffer.c中定义了数组buffersize_index〔65〕和宏BUFSIZE_INDEX,如下所示(fs/buffer.c):
#define NR_SIZES 7
static char buffersize_index[65] =
{-1, 0, 1, -1, 2, -1, -1, -1, 3, -1, -1, -1, -1, -1, -1, -1,
4, -1, -1, -1, -1, -1, -1, -1, -1,-1, -1, -1, -1, -1, -1, -1,
5, -1, -1, -1, -1, -1, -1, -1, -1,-1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1,-1, -1, -1, -1, -1, -1, -1,
6};
#define BUFSIZE_INDEX(X) ((int) buffersize_index[(X)>>9])
可以看出,只有当块大小x属于集合〔512,1024,2048,…,32768〕时,它在数组buffersize_index中的索引值才有意义。宏BUFSIZE_INDEX是用来根据大小x索引数组free_list的,其用法通常为:free_list〔BUDSIZE_INDEX(x)〕。
写到这里有必要再次澄清一个概念!那就是:一定要区分buffer_head对象缓存和缓冲区本身。buffer_head对象的缓存是bh_cachep这个SLAB分配器缓存,而链表unused_list又可以被看作是在bh_cachep之上的中间辅助缓存。而缓冲区本身所占用的内存是通过Buddy系统分配的;为了避免因频繁调用Buddy分配器而引起系统的抖动,Linux在Buddy分配器之上又为缓冲区增设了一个中间缓存,那就是空闲缓存,并通过buffer_head对象的free_list链表将这些空闲缓冲区有效地管理起来。这里我们认为bcache机制是广义的,也即包括了两个方面:(1)buffer_head对象的SLAB分配器缓存、各种链表和相关的操作;(2)缓冲区本身以及相关的操作。这两个方面的关系如下图所示:
2.3.4 已用缓冲区的缓冲区首部链表
当一个缓冲区处于inuse状态下时,相应buffer_head对象中的b_state状态域就描述了缓冲区的当前状态,如:uptodate、dirty、lock等。
为了更佳方便地把脏缓冲区回写到磁盘上,Linux按照不同的b_state标志值将所有inuse的缓冲区所对应的buffer_head对象链接成四条不同类型的链表,它们的定义如下(fs/buffer.c):
static struct buffer_head *lru_list[NR_LIST];
static spinlock_t lru_list_lock = SPIN_LOCK_UNLOCKED;
static int nr_buffers_type[NR_LIST];
static unsigned long size_buffers_type[NR_LIST];
其中,lru_list_lock是对这4条链表进行保护的自旋锁,以实现对lru_list链表的互斥访问。数组nr_buffers_type〔〕表示这4条链表中每条链表的元素个数,数组size_buffers_type则表示这4条链表中每条链表中的所有缓冲区的大小总和。
Linux在fs.h头文件中为lru_list〔〕数组中的每一元素(也即链表表头)定义了索引宏,如下(include/linux/fs.h):
#define BUF_CLEAN 0
#define BUF_LOCKED 1 /* Buffers scheduled for write */
#define BUF_DIRTY 2 /* Dirty buffers, not yet scheduled for write */
#define BUF_PROTECTED 3 /* Ramdisk persistent storage */
#define NR_LIST 4
下面我们详细分析这4条链表:
1. BUF_CLEAN链表:这条链表集中存放干净缓冲区(没有设置BH_Dirty标志)的buffer_head对象。注意!该链表中的缓冲区为必是最新的。
2. BUF_DIRTY链表:这条链表主要集中存放脏缓冲区的缓冲区首部,但这些缓冲区还未被选中要把其中的内容回写到磁盘,也即BH_Dirty被置位,但BH_Lock未被置位。
3. BUF_LOCKED链表:这个链表主要存放已经被选中要把其中的内容回写到磁盘的脏缓冲区的buffer_head对象,也即:BH_Lock已经被置位,而BH_Dirty标志已经被清除。
4. BUF_PROTECTED链表:这个链表主要存放与ramdisk相关的缓冲区的buffer_head对象。
对于一个inuse状态下的缓冲区头部对象来说,其b_list成员的值指出了该buffer_head对象所属的lru_list链表。而b_next_free和b_prev_free指针则分别指向链表中的前、后元素。
2.3.5 bcache机制的初始化
函数buffer_init()完成bcache机制的初始化工作。它主要完成三件事:
(1)为buffer_head哈希链表表头数组分配内存,并对其进行初始化;
(2)初始化free_list链表数组中的元素;
该函数与icache的初始化函数inode_init()的实现思想类似,这里就不详细分析了。其源代码如下(fs/Buffer.c):
/* ===================== Init ======================= */
/*
* allocate the hash table and init the free list
* Use gfp() for the hash table to decrease TLB misses, use
* SLAB cache for buffer heads.
*/
void __init buffer_init(unsigned long mempages)
{
int order, i;
unsigned int nr_hash;
/* The buffer cache hash table is less important these days,
* trim it a bit.
*/
mempages >>= 14;
mempages *= sizeof(struct buffer_head *);
for (order = 0; (1 << order) < mempages; order++)
;
/* try to allocate something until we get it or we're asking
for something that is really too small */
do {
unsigned long tmp;
nr_hash = (PAGE_SIZE << order) / sizeof(struct buffer_head *);
bh_hash_mask = (nr_hash - 1);
tmp = nr_hash;
bh_hash_shift = 0;
while((tmp >>= 1UL) != 0UL)
bh_hash_shift++;
hash_table = (struct buffer_head **)
__get_free_pages(GFP_ATOMIC, order);
} while (hash_table == NULL && --order > 0);
printk("Buffer-cache hash table entries: %d (order: %d, %ld bytes)/n",
nr_hash, order, (PAGE_SIZE << order));
if (!hash_table)
panic("Failed to allocate buffer hash table/n");
/* Setup hash chains. */
for(i = 0; i < nr_hash; i++)
hash_table = NULL;
/* Setup free lists. */
for(i = 0; i < NR_SIZES; i++) {
free_list.list = NULL;
free_list.lock = SPIN_LOCK_UNLOCKED;
}
/* Setup lru lists. */
for(i = 0; i < NR_LIST; i++)
lru_list = NULL;
}
2.4 对buffer_head对象链表的操作
2.4.1 对哈希链表的操作
1.在哈希链表中查找一个特定的缓冲区首部——get_hash_table()
函数get_hash_table()在哈希链表中查找是否存在给定条件(dev,block,size)的buffer_head对象,如果存在则增加该buffer_head对象的引用计数值。如下所示(fs/Buffer.c):
struct buffer_head * get_hash_table(kdev_t dev, int block, int size)
{
struct buffer_head *bh;
read_lock(&hash_table_lock);
bh = __get_hash_table(dev, block, size);
read_unlock(&hash_table_lock);
return bh;
}
可以看出,该函数实际通过内部函数__get_hash_table()来完成实际的搜索工作(fs/Buffer.c):
static inline struct buffer_head * __get_hash_table(kdev_t dev, int block, int size)
{
struct buffer_head *bh = hash(dev, block);
for (; bh; bh = bh->b_next)
if (bh->b_blocknr == block &&
bh->b_size == size &&
bh->b_dev == dev)
break;
if (bh)
atomic_inc(&bh->b_count);
return bh;
}
函数的注释如下:
1首先,调用hash()宏确定这个buffer_head对象应该所属的哈希链表。
2然后,通过for循环来在相应的哈希链表中查找是否存在相匹配的buffer_head对象。如果找到,就增加该对象的引用计数b_count的值(加1)。
2.哈希链表的插入与删除
内部函数__hash_link()实现向特定的哈希链表中插入一个buffer_head对象。该函数将bh对象插入到哈希链表的首部,如下所示(fs/buffer.c):
static __inline__ void __hash_link(struct buffer_head *bh, struct buffer_head **head)
{
if ((bh->b_next = *head) != NULL)
bh->b_next->b_pprev = &bh->b_next;
*head = bh;
bh->b_pprev = head;
}
内部函数__hash_unlink()实现将一个bh对象从他当前所属的哈希链表中删除,如下(fs/buffer.c):
static __inline__ void __hash_unlink(struct buffer_head *bh)
{
if (bh->b_pprev) {
if (bh->b_next)
bh->b_next->b_pprev = bh->b_pprev;
*(bh->b_pprev) = bh->b_next;
bh->b_pprev = NULL;
}
}
2.4.2 对free_list链表的操作
函数__remove_from_free_list()用于从free_list〔index〕摘除指定的bh对象。该函数首先是通过bh->b_next_free==bh来判断free_list〔index〕是否只有bh对象这一个元素。如果是,就简单地让free_list〔index〕.list表头指针为NULL就可以了;否则按照正常的双向链表的元素摘除方法将bh对象从中摘除。最后将该bh对象的b_next_free指针和b_prev_free指针都置为NULL。如下:
static void __remove_from_free_list(struct buffer_head * bh, int index)
{
if(bh->b_next_free == bh)
free_list[index].list = NULL;
else {
bh->b_prev_free->b_next_free = bh->b_next_free;
bh->b_next_free->b_prev_free = bh->b_prev_free;
if (free_list[index].list == bh)
free_list[index].list = bh->b_next_free;
}
bh->b_next_free = bh->b_prev_free = NULL;
}
函数put_last_free()将一个刚转变为free状态的缓冲区的bh对象放入它应该所属的free_list〔index〕链表中。实现步骤如下:
(1)首先,根据bh对象的b_size值,通过BUFSIZE_INDEX宏确定该bh对象应该被放到哪一个free_list链表中;
(2)将该bh对象的b_state和b_dev域分别置0和B_FREE;
(3)然后,将这个bh对象插入到相应free_list〔index〕链表的尾部。其源代码如下(fs/buffer.c):
/* This function must only run if there are no other
* references _anywhere_ to this buffer head.
*/
static void put_last_free(struct buffer_head * bh)
{
struct bh_free_head *head = &free_list[BUFSIZE_INDEX(bh->b_size)];
struct buffer_head **bhp = &head->list;
bh->b_state = 0;
spin_lock(&head->lock);
bh->b_dev = B_FREE;
if(!*bhp) {
*bhp = bh;
bh->b_prev_free = bh;
}
bh->b_next_free = *bhp;
bh->b_prev_free = (*bhp)->b_prev_free;
(*bhp)->b_prev_free->b_next_free = bh;
(*bhp)->b_prev_free = bh;
spin_unlock(&head->lock);
}
2.4.3 对lru_list链表的操作
1.插入与删除
函数__insert_into_lru_list()用于将一个指定的bh对象插入到指定的lru_list〔blist〕链表中。其实现过程为:
(1)首先判断lru_list〔blist〕链表是否为NULL,如果为空的话,就让链表表头指针指向这个bh对象,并且把该bh对象的b_prev_free指针指向为自身。
(2)然后,将这个bh对象链入lru_list〔blist〕链表的表尾。
(3)最后,将链表元素个数值nr_buffers_type〔blist〕加1,同时增加这个链表的缓冲区总大小值size_buffers_type〔blist〕。
该函数的源码如下(fs/buffer.c):
static void __insert_into_lru_list(struct buffer_head * bh, int blist)
{
struct buffer_head **bhp = &lru_list[blist];
if(!*bhp) {
*bhp = bh;
bh->b_prev_free = bh;
}
bh->b_next_free = *bhp;
bh->b_prev_free = (*bhp)->b_prev_free;
(*bhp)->b_prev_free->b_next_free = bh;
(*bhp)->b_prev_free = bh;
nr_buffers_type[blist]++;
size_buffers_type[blist] += bh->b_size;
}
函数__remove_from_lru_list()实现将一个指定的bh对象从其当前所属的lru_list〔blist〕链表中删除。实现过程为:
(1)首先,修改与指定bh对象相邻的两个bh对象的b_next_free和b_prev_free指针,以将该bh对象从所属链表中去除。
(2)如果这个bh对象是lru_list〔blist〕链表的第一个元素,则让表头指针指向链表中的下一个元素(即lru_list〔blist〕=bh->b_next_free);
(3)如果lru_list〔blist〕仍然指向该bh对象,则说明链表中只有一个元素(即指定的bh对象),于是就让表头指针为NULL;
(4)修改相应的nr_buffers_type〔blist〕和size_buffers_type〔blist〕的值。该函数的源代码如下:
static void __remove_from_lru_list(struct buffer_head * bh, int blist)
{
if (bh->b_prev_free || bh->b_next_free) {
bh->b_prev_free->b_next_free = bh->b_next_free;
bh->b_next_free->b_prev_free = bh->b_prev_free;
if (lru_list[blist] == bh)
lru_list[blist] = bh->b_next_free;
if (lru_list[blist] == bh)
lru_list[blist] = NULL;
bh->b_next_free = bh->b_prev_free = NULL;
nr_buffers_type[blist]--;
size_buffers_type[blist] -= bh->b_size;
}
}
另外,Linux在buffer.c文件中还封装了两个函数__insert_into_queues()和__remove_from_queues(),用于实现对哈希链表和lru_list链表的同时插入和删除操作。如下所示:
static void __remove_from_queues(struct buffer_head *bh)
{
__hash_unlink(bh);
__remove_from_lru_list(bh, bh->b_list);
}
static void __insert_into_queues(struct buffer_head *bh)
{
struct buffer_head **head = &hash(bh->b_dev, bh->b_blocknr);
__hash_link(bh, head);
__insert_into_lru_list(bh, bh->b_list);
}
2.重新确定一个bh对象所属的lru_list链表
当一个inuse状态下的bh对象中的b_state状态值发生变化时,就可能需要重新确定该bh对象所属的lru_list链表。函数__refile_buffer()实现这一功能:
(1)首先,它根据b_state的值确定该bh对象应该被放置到哪一条lru_list链表中,并以变量dispose表示。
(2)由于b_list值表示该bh对象当前所处的lru_list链表。因此如果dispose的值与b_list的值不相等,则需要将该bh对象从原来的lru_list链表中摘除,然后将他插入到新的lru_list链表中;且如果如果新lru_list链表是BUF_CLEAN链表,则还需要调用remove_inode_queue()函数将该bh对象从相应inode的脏缓冲区链表i_dirty_buffers中删除。函数的源码如下(fs/buffer.c):
/*
* A buffer may need to be moved from one buffer list to another
* (e.g. in case it is not shared any more). Handle this.
*/
static void __refile_buffer(struct buffer_head *bh)
{
int dispose = BUF_CLEAN;
if (buffer_locked(bh))
dispose = BUF_LOCKED;
if (buffer_dirty(bh))
dispose = BUF_DIRTY;
if (buffer_protected(bh))
dispose = BUF_PROTECTED;
if (dispose != bh->b_list) {
__remove_from_lru_list(bh, bh->b_list);
bh->b_list = dispose;
if (dispose == BUF_CLEAN)
remove_inode_queue(bh);
__insert_into_lru_list(bh, dispose);
}
}
在__refile_buffer()函数的基础上,Linux封装了一个向外开放的函数refile_buffer():
void refile_buffer(struct buffer_head *bh)
{
spin_lock(&lru_list_lock);
__refile_buffer(bh);
spin_unlock(&lru_list_lock);
}
2.4.4 改变inuse状态下的缓冲区首部的状态
1.BH_uptodate标志位
函数mark_buffer_uptodate()用于设置或清除一个bh对象的BH_Uptodate标志位。NOTE! BH_Uptodate标志位的改变并不影响该bh对象在lru_list链表中的位置。因此,函数mark_buffer_uptodate()仅仅设置或清除BH_Uptodate标志位就可以了,不需要做任何额外的工作。该函数如下所示(include/linux/fs.h):
static inline void mark_buffer_uptodate(struct buffer_head * bh, int on)
{
if (on)
set_bit(BH_Uptodate, &bh->b_state); //设置
else
clear_bit(BH_Uptodate, &bh->b_state); //清除
}
2.将缓冲区标记为「干净的」(clean)
函数mark_buffer_clean()将一个缓冲区标记为clean,并且在需要时,仅该bh对象移到BUF_CLEAN链表中。如下所示(include/linux/fs.h):
static inline void mark_buffer_clean(struct buffer_head * bh)
{
//清除BH_Dirty标志位,返回BH_Dirty标志位的原有值
if (atomic_set_buffer_clean(bh))
__mark_buffer_clean(bh);
}
对该函数的注释如下:
(1)函数首先调用原子操作atomic_set_buffer_clean()清除BH_Dirty标志位,该原子操作将返回BH_Dirty标志位的原有值。该原子操作也是定义在fs.h头文件中的宏:
#define atomic_set_buffer_clean(bh) test_and_clear_bit(BH_Dirty, &(bh)->b_state)
(2)如果该bh对象原来是脏的,那就调用__mark_buffer_clean()函数重新确定该bh对象在lru_list中的位置(include/linux/fs.h):
static inline void __mark_buffer_clean(struct buffer_head *bh)
{
refile_buffer(bh);
}
可以看出,该函数仅仅是对refile_buffer()函数的封装。
3.将缓冲区标记为「脏」(dirty)
函数mark_buffer_dirty()将一个缓冲区标记为脏。该函数的实现过程为:
(1)它首先调用原子操作atomic_set_buffer_dirty设置缓冲区bh对象的BH_Dirty标志位,并同时返回BH_Dirty标志位的原有值。
(2)因此,如果atomic_set_buffer_dirty宏返回为1,这说明该bh对象原来就是脏的,所以不需要任何额外的操作函数就可以直接返回了;否则,如果atomic_set_buffer_dirty宏返回为0,则说明该bh对象原来是「干净」的(处在BUF_CLEAN链表中),因此需要重新确定该bh对象所属的lru_list链表,并且平衡相应块设备的脏缓冲区个数。
该函数的源码如下(fs/buffer.c):
void mark_buffer_dirty(struct buffer_head *bh)
{
if (!atomic_set_buffer_dirty(bh)) {
__mark_dirty(bh);
balance_dirty(bh->b_dev);
}
}
宏atomic_set_buffer_dirty定义在fs.h头文件中:
#define atomic_set_buffer_dirty(bh) test_and_set_bit(BH_Dirty, &(bh)->b_state)
函数__mark_dirty()的主要责任就是:
1.更新bh对象的b_flushtime成员的值,以确定该脏缓冲区回写磁盘的时间期限;
2.调用refile_buffer()函数,将该bh对象移到新的lru_list链表中(在这里就是移到BUF_DIRTY链表中)。如下所示(fs/Buffer.c):
static __inline__ void __mark_dirty(struct buffer_head *bh)
{
bh->b_flushtime = jiffies + bdf_prm.b_un.age_buffer;
refile_buffer(bh);
}
函数balance_dirty()用于平衡指定块设备对应的脏缓冲区个数。也即,如果指定块设备所对应的脏缓冲区过多的话,则应将他们回写到磁盘中,如下所示(fs/Buffer.c):
void balance_dirty(kdev_t dev)
{
int state = balance_dirty_state(dev);
if (state < 0)
return;
wakeup_bdflush(state);
}
可以看出:
(1)函数首先调用balance_dirty_state()函数确定指定块设备对应的脏缓冲区数量的情况。返回值-1 表示脏缓冲区还不太多,因此不需要回写。
返回值0 表示可以用异步方式回写脏缓冲区(表明脏缓冲区已经多到必须回写的地步了,但又还可以忍受,因此采用异步方式回写)。
返回值1 表示必须采用同步阻塞方式回写脏缓冲区(脏缓冲区已经多到系统无法忍受的地步了,所以其它事情就等到把脏缓冲区都回写完以后再说吧:)。
(2)然后调用wakeup_bdflush()函数唤醒bdflush内核线程以异步或同步方式回写指定块设备所对应的脏缓冲区。
函数__mark_buffer_dirty()与函数mark_buffer_dirty()的功能类似,唯一的区别是:前者不调用balance_dirty()函数。因此调用者必须手工调用balance_dirty()函数。
void __mark_buffer_dirty(struct buffer_head *bh)
{
if (!atomic_set_buffer_dirty(bh))
__mark_dirty(bh);
}
4.将缓冲区标记为「protected」
函数mark_buffer_protected()将一个缓冲区标记为「受保护的」(即设置BH_Protected标志位)。该函数与mark_buffer_clean()的实现类似,如下(fs.h):
static inline void mark_buffer_protected(struct buffer_head * bh)
{
if (!atomic_set_buffer_protected(bh))
__mark_buffer_protected(bh);
}
宏atomic_set_buffer_protected()和函数__mark_buffer_protected()也都定义在fs.h头文件中:
#define atomic_set_buffer_protected(bh) test_and_set_bit(BH_Protected, &(bh)->b_state)
static inline void __mark_buffer_protected(struct buffer_head *bh)
{
refile_buffer(bh);
}