`
lando
  • 浏览: 34282 次
  • 性别: Icon_minigender_1
  • 来自: 北京
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论

VFS 缓冲区缓存Buffer Cache实现原理剖析Ⅰ

阅读更多

前段时间读的一篇文章,觉得写得很好。

zhanrk在linuxforum论坛上发布的VFS 缓冲区缓存Buffer Cache实现原理剖析一文,

————

 

VFS 缓冲区缓存Buffer Cache实现原理剖析
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 缓冲区访问接口getblkbrelse…………………………………………..……….22
3
3数据块读接口bread…………………………………………………….………….24
3
4 inodei_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 */
};
各字段的含义如下:
.b_next指针:指向哈希链表中的下一个buffer_head对象。
.b_blocknr:本缓冲区对应的块号(block number)。
.b_size:以字节计掉的块长度。合法值为:51210242048409681921638432768
.b_list:记录这个缓冲区应该出现在哪个链表上。
.d_dev:缓冲区对应的块所在的块设备标识符(对于位于free_list链表中的缓冲区,b_devB_FREE)。
.b_count:本缓冲区的引用计数。
.b_rdev:缓冲区对应的块所在的块设备的「真实」标识符。
.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 */
.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指针:如果缓冲区为脏,且又属于某个索引节点,那么就通过这个指针链入inodei_dirty_buffers链表中。

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

2
2 buffer_head对象的SLAB分配器缓存
缓冲区头部对象buffer_head本身有一个叫做bh__cachepslab分配器缓存。因此对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_cachepfilp_cachepdquot_cachepbh_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_nextb_pprev两个指针域链入哈希链表中,从而可以加快对buffer_head对象的查找(lookup)。


2.
最近最少使用链表lru_list:每个处在inuse状态下的buffer_head对象都通过b_next_freeb_prev_free这两个指针链入某一个lru_list链表中。


3.
空闲链表free_list每一个处于free状态下的buffer_head对象都根据它所关联的空闲缓冲区的大小链入某个free_list链表中(也是通过b_next_freeb_prev_free这两个指针)。


4.
未使用链表unused_list:所有处于unused状态下的buffer_head对象都通过指针域b_next_freeb_prev_free链入unused_list链表中。


5. inode
对象的脏缓冲区链表i_dirty_buffers如果一个脏缓冲区有相关联的inode对象的话,那么他就通过其b_inode_buffers指针域链入其所属的inode对象的i_dirty_buffers链表中。


下面,我们分别详细阐述上述这些链表。

2
31 哈希链表
   
内核对buffer_head对象的查找是相当频繁的,因此为了加快查找速度,bcache机制使用哈希链表来管理bcache中的每一个buffer_head对象。

每一个buffer_head对象都根据其中的设备标识符b_dev和块号b_blocknr来确定他所属的哈希链表,并通过b_nextb_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_maskbh_hash_shift变量的含义与icache机制中inode哈希链表的i_hash_maski_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
32 未使用的buffer_head对象链表unused_list
   
所有处于unused状态下的buffer_head对象都通过b_next_freeb_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_cachepbcache机制之间的一个中间辅助缓存。

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;
}
该函数的注释如下:
.如果链表元素个数nr_unused_buffer_heads大于NR_RESERVED,则可以直接从unused_list链表中摘取一个buffer_head对象返回给调用者(从链表的首部摘除),因此将unused_list指针修改为unused_list>b_next_free,然后将nr_unused_buffer_heads1后将可以直接返回了。
.否则,就调用kmem_cache_alloc()函数bh_cachep SLAB分配器缓存中试图分配一个新的buffer_head对象。在内存不紧张的情况下,这应该会分配成功。然后就对新分配的buffer_head对象做初始化,并返回其指针。
.如果kmem_cache_alloc()分配失败,则说明当前系统的内存紧张。因此此时就根据参数async判断是否在为异步的页I/O操作分配一个buffer_head对象。如果不是(async0),那就直接返回NULL给调用者。如果是,则在unused_list链表不为空的情况下,从unused_list链表中为页I/O保留的NR_RESERVEDbuffer_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
33 缓冲区头部对象的空闲链表free_list
   
不同逻辑块设备的数据块(并非扇区,扇区是物理块设备组织数据的单位,通常是512字节,数据块的大小必须是扇区大小的整数倍)大小通常都不同。Linux中允许有NR_SIZES(值为7,定义在buffer.c中)种大小的块,分别为:51210242048409681921638432768字节。由于缓冲区与块是对应的,因此缓冲区的大小也相应地有7种。但是由于一个块的大小不能超过物理页帧的大小,因此在i386体系结构中实际上只能使用前4种大小的块。
    Linux
将同样大小的空闲缓冲区的头部对象通过其中的b_next_freeb_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];
   
为了更方便地根据块大小找到相应的空闲缓冲区头部对象链表,Linuxbuffer.c中定义了数组buffersize_index65〕和宏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_listBUDSIZE_INDEXx)〕。

   
写到这里有必要再次澄清一个概念!那就是:一定要区分buffer_head对象缓存和缓冲区本身。buffer_head对象的缓存是bh_cachep这个SLAB分配器缓存,而链表unused_list又可以被看作是在bh_cachep之上的中间辅助缓存。而缓冲区本身所占用的内存是通过Buddy系统分配的;为了避免因频繁调用Buddy分配器而引起系统的抖动,LinuxBuddy分配器之上又为缓冲区增设了一个中间缓存,那就是空闲缓存,并通过buffer_head对象的free_list链表将这些空闲缓冲区有效地管理起来。这里我们认为bcache机制是广义的,也即包括了两个方面:(1buffer_head对象的SLAB分配器缓存、各种链表和相关的操作;(2)缓冲区本身以及相关的操作。这两个方面的关系如下图所示:

2
34 已用缓冲区的缓冲区首部链表
   
当一个缓冲区处于inuse状态下时,相应buffer_head对象中的b_state状态域就描述了缓冲区的当前状态,如:uptodatedirtylock等。
   
为了更佳方便地把脏缓冲区回写到磁盘上,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_freeb_prev_free指针则分别指向链表中的前、后元素。

2
35 bcache机制的初始化
   
函数buffer_init()完成bcache机制的初始化工作。它主要完成三件事:

1)为buffer_head哈希链表表头数组分配内存,并对其进行初始化;

2)初始化free_list链表数组中的元素;

3)初始化lru_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;

}

 

24 buffer_head对象链表的操作
2
41 对哈希链表的操作

.在哈希链表中查找一个特定的缓冲区首部——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)。

.哈希链表的插入与删除
   
内部函数__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
42 free_list链表的操作
   
函数__remove_from_free_list()用于从free_listindex〕摘除指定的bh对象。该函数首先是通过bh>b_next_free==bh来判断free_listindex〕是否只有bh对象这一个元素。如果是,就简单地让free_listindex.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_listindex〕链表中。实现步骤如下:

1)首先,根据bh对象的b_size值,通过BUFSIZE_INDEX确定该bh对象应该被放到哪一个free_list链表中;

2)将该bh对象的b_stateb_dev域分别置0B_FREE

3)然后,将这个bh对象插入到相应free_listindex〕链表的尾部。其源代码如下(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
43 lru_list链表的操作
.插入与删除
   
函数__insert_into_lru_list()用于将一个指定的bh对象插入到指定的lru_listblist〕链表中。其实现过程为:

1)首先判断lru_listblist〕链表是否为NULL,如果为空的话,就让链表表头指针指向这个bh对象,并且把该bh对象的b_prev_free指针指向为自身。

2)然后,将这个bh对象链入lru_listblist〕链表的表尾。

3)最后,将链表元素个数值nr_buffers_typeblist〕加1,同时增加这个链表的缓冲区总大小值size_buffers_typeblist〕。

该函数的源码如下(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_listblist〕链表中删除。实现过程为:

1)首先,修改与指定bh对象相邻的两个bh对象的b_next_freeb_prev_free指针,以将该bh对象从所属链表中去除。

2)如果这个bh对象是lru_listblist〕链表的第一个元素,则让表头指针指向链表中的下一个元素(即lru_listblist〕=bh>b_next_free);

3)如果lru_listblist〕仍然指向该bh对象,则说明链表中只有一个元素(即指定的bh对象),于是就让表头指针为NULL

4)修改相应的nr_buffers_typeblist〕和size_buffers_typeblist〕的值。该函数的源代码如下:
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;
}
}
另外,Linuxbuffer.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);
}

.重新确定一个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
44 改变inuse状态下的缓冲区首部的状态
.BH_uptodate标志位
函数mark_buffer_uptodate()用于设置或清除一个bh对象的BH_Uptodate标志位。NOTE! BH_Uptodate标志位的改变并不影响该bh对象在lru_list链表中的位置。因此,函数mark_buffer_uptodate()仅仅设置或清除BH_Uptodate标志位就可以了,不需要做任何额外的工作。该函数如下所示(include/linux/fs.h)<

分享到:
评论

相关推荐

    VFS文件系统简要分析

    VFS优化了数据缓存机制,通过缓冲区高速缓存(Buffer Cache)等技术提高文件读写性能。 #### 四、重要数据结构 VFS中的几个核心数据结构对于理解其工作原理至关重要: ##### 1. super_block `super_block` ...

    Linux内核缓冲区管理

    本文将深入探讨Linux内核缓冲区管理的原理、设计和实现,以及它对系统性能的影响。 首先,理解缓冲区(Buffer)的基本概念至关重要。在计算机系统中,缓冲区是一种临时存储区域,用于存放数据,以便在不同速度的...

    Linux内核内存Cache机制原理

    Linux内核内存Cache机制是操作系统优化性能的关键组成部分,它...通过深入源码分析,可以更详细地了解Cache的工作原理,例如如何实现LRU更新、如何触发页面的淘汰等。对于Linux内核的学习者来说,这是一项重要的技能。

    linux内核缓冲区管理课件.rar

    1. **缓冲区头(Buffer Heads)**:在Linux内核中,每个缓冲区都有一个与之关联的缓冲区头结构,用于存储关于该缓冲区的信息,如其在内存中的位置、是否被锁定以及相关的I/O状态等。 2. **缓冲区分配**:内核使用...

    解析Linux的VFS机制

    Linux的虚拟文件系统(Virtual Filesystem Switch,简称VFS)是操作系统的核心组件之一,它为上层...理解VFS的工作原理对于深入学习Linux内核和进行系统优化至关重要。通过阅读源代码和实践,可以更好地掌握这些概念。

    Linux VFS资料集

    9. **缓存机制**:为了提高性能,VFS使用了缓冲区缓存(buffer cache)和页面缓存(page cache)。缓冲区缓存用于存储磁盘块,而页面缓存用于存储文件数据。这两个缓存极大地减少了磁盘I/O操作。 10. **异步I/O**:...

    Linux 通用块设备层基础之buffer_head .

    在深入探讨`buffer_head`在Linux内核中的作用前,我们先明确几个核心概念:**缓冲区**(buffer)是磁盘块在物理内存中的映射;**缓冲区描述符**(buffer_head)则详细记录了缓冲区与磁盘块间的关联信息;而**bio**(block...

    linux的VFS机制分析.zip_VFS_VFS机制_vfs代码分析

    5. **缓存机制**:VFS使用了缓存来提高性能,包括inode缓存和dentry缓存,分析缓存的命中率和管理策略。 6. **异步I/O和锁管理**:在多线程或多进程环境中,VFS如何协调对文件的并发访问,以及如何支持异步I/O操作...

    vfs.rar_overview

    4. **缓存管理**:VFS使用缓冲区缓存(Buffer Cache)和页面缓存(Page Cache)来提高I/O性能。 5. **同步与异步**:VFS支持同步和异步I/O,异步I/O可以提高系统并发性。 6. **文件关闭**:当文件不再使用时,VFS...

    Linux系统内核文件Cache管理机制

    Page Cache和Buffer Cache之间的关系是,每个Page Cache条目可能包含多个Buffer Cache条目。VFS(虚拟文件系统)处理应用程序与Page Cache之间的数据交换,通过read和write等接口提供服务。内存管理系统在分配和回收...

    Linux环境下自制文件系统的VFS实现.pdf

    Linux环境下自制文件系统的VFS实现 Linux操作系统作为一个开放式的操作系统,支持多种文件系统,以满足不同的应用需求。 Linux文件系统可以分为三个部分:Virtual File System Switch(VFS)、Cache和文件系统本身...

    vfs.rar_VFS_linux vfs_linux 文件系统_vfs.c_vfs文件系统

    在`vfs.TXT`中,可能会详细解释VFS的工作原理,包括文件的打开、关闭、读取、写入、重命名、创建、删除等操作的流程,以及VFS如何管理和缓存dentry和inode对象,以提高文件系统的性能。 `www.pudn.com.txt`可能是一...

    VFS and filesystems in Linux

    page cache不仅提供了零拷贝语义,即文件内容直接从缓存中读取,无需额外的复制过程,极大地简化了VFS的操作流程,同时也简化了文件系统的实现难度。文件系统只需提供页面级别的操作,剩下的工作,如内存映射(mmap...

    存储技术原理分析:基于Linux 2.6内核源代码

    10. **缓存机制**:Linux内核使用Page Cache和Buffer Cache来缓存文件和文件系统的元数据,减少对硬盘的访问,提高系统响应速度。 通过分析Linux 2.6内核源代码,我们可以深入了解这些机制的实现细节,以及如何优化...

    设计和实现MINIX VFS

    设计和实现MINIX VFS 本书详细描述了minix3 vfs

    vfs_syscalls.rar_V2

    6. **缓存机制**:VFS利用缓冲区缓存(buffer cache)和页缓存(page cache)来提高文件操作的效率。系统调用会涉及这些缓存的使用,例如,读写操作可能会直接从内存缓存中读取或写入数据。 7. **异步I/O和多线程...

    cachefiles源码

    cachefiles是Linux内核中用于实现NFS(Network File System)缓存机制的一个关键组件。NFS是一种分布式文件系统协议,它允许一台计算机通过网络访问另一台计算机上的文件系统。由于网络通信的延迟,直接通过NFS进行...

    IO-buffer.zip_biggervnh_hit51l_io buffer

    - **内核空间中的缓冲**:操作系统为每个打开的文件维护缓冲区,提供VFS(虚拟文件系统)层和设备驱动之间的接口。 - **用户空间的缓冲**:例如,标准I/O库(如C语言的stdio.h)提供了用户级别的缓冲。 5. **异步...

    VFS_full.rar_VFS_full

    通过学习和分析这个压缩包中的代码,开发者可以深入理解VFS的工作原理,以及如何在实际操作中实现这些概念。这不仅对操作系统开发人员有极大的帮助,也对那些希望优化系统性能或理解底层操作的软件工程师大有裨益。

    linux内核2.6cache源代码

    在这个版本的内核中,缓存的设计和实现已经相当成熟,涉及到内存管理、I/O操作、进程调度等多个关键领域。 首先,我们要了解缓存的基本原理。缓存是一种高速数据存储层,用于临时存储频繁访问的数据,以便减少对...

Global site tag (gtag.js) - Google Analytics