VFS 缓冲区缓存Buffer Cache实现原理剖析(1)

    技术2022-05-11  49

    摘要:本文主要从内核实现的角度分析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 概述…...……………………………………………………………….…3 Chapter 2 Buffer Cache的数据结构…………………………………………….….4 2.1 缓冲区头部对象buffer_head……………………………………………………….4 2.2 buffer_head对象的SLAB分配器缓存……………………………………………..5 2.3 bcache中的缓冲区头部对象链表…………………………………………………..6 2.4 对buffer_head对象链表的操作……………………………………………..……12 Chapter 3 对缓冲区的操作………………………………………………………..18 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 */ }; 各字段的含义如下: ⑴b_next指针:指向哈希链表中的下一个buffer_head对象。 ⑵b_blocknr:本缓冲区对应的块号(block number)。 ⑶b_size:以字节计掉的块长度。合法值为:512、1024、2048、4096、8192、16384和32768。 ⑷b_list:记录这个缓冲区应该出现在哪个链表上。 ⑸d_dev:缓冲区对应的块所在的块设备标识符(对于位于free_list链表中的缓冲区,b_dev=B_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:脏缓冲区必须被回写到磁盘的最后期限值。 ⑽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; } 该函数的注释如下: ①如果链表元素个数nr_unused_buffer_heads大于NR_RESERVED,则可以直接从unused_list链表中摘取一个buffer_head对象返回给调用者(从链表的首部摘除),因此将unused_list指针修改为unused_list->b_next_free,然后将nr_unused_buffer_heads减1后将可以直接返回了。 ②否则,就调用kmem_cache_alloc()函数bh_cachep SLAB分配器缓存中试图分配一个新的buffer_head对象。在内存不紧张的情况下,这应该会分配成功。然后就对新分配的buffer_head对象做初始化,并返回其指针。 ③如果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链表数组中的元素;(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;

    }

    2.4 对buffer_head对象链表的操作 2.4.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; } 函数的注释如下: ①首先,调用hash()宏确定这个buffer_head对象应该所属的哈希链表。 ②然后,通过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.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链表的操作 ⑴插入与删除 函数__insert_into_lru_list()用于将一个指定的bh对象插入到指定的lru_list[blist]链表中。其实现过程为:①首先判断lru_list[blist]链表是否为NULL,如果为空的话,就让链表表头指针指向这个bh对象,并且把该bh对象的b_prev_free指针指向为自身。②然后,将这个bh对象链入lru_list[blist]链表的表尾。③最后,将链表元素个数值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]链表中删除。实现过程为:①首先,修改与指定bh对象相邻的两个bh对象的b_next_free和b_prev_free指针,以将该bh对象从所属链表中去除。②如果这个bh对象是lru_list[blist]链表的第一个元素,则让表头指针指向链表中的下一个元素(即lru_list[blist]=bh->b_next_free);③如果lru_list[blist]仍然指向该bh对象,则说明链表中只有一个元素(即指定的bh对象),于是就让表头指针为NULL;④修改相应的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); }

    ⑵重新确定一个bh对象所属的lru_list链表 当一个inuse状态下的bh对象中的b_state状态值发生变化时,就可能需要重新确定该bh对象所属的lru_list链表。函数__refile_buffer()实现这一功能:①首先,它根据b_state的值确定该bh对象应该被放置到哪一条lru_list链表中,并以变量dispose表示。②由于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状态下的缓冲区首部的状态 ⑴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); }

    ⑵将缓冲区标记为“干净的”(clean) 函数mark_buffer_clean()将一个缓冲区标记为clean,并且在需要时,仅该bh对象移到BUF_CLEAN链表中。如下所示(include/linux/fs.h): static inline void mark_buffer_clean(struct buffer_head * bh) { if (atomic_set_buffer_clean(bh)) __mark_buffer_clean(bh); } 对该函数的注释如下: ①函数首先调用原子操作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) ②如果该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()函数的封装。

    ⑶将缓冲区标记为“脏”(dirty) 函数mark_buffer_dirty()将一个缓冲区标记为脏。该函数的实现过程为:它首先调用原子操作atomic_set_buffer_dirty设置缓冲区bh对象的BH_Dirty标志位,并同时返回BH_Dirty标志位的原有值。因此,如果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()的主要责任就是:①更新bh对象的b_flushtime成员的值,以确定该脏缓冲区回写磁盘的时间期限;②调用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); } 可以看出: ①函数首先调用balance_dirty_state()函数确定指定块设备对应的脏缓冲区数量的情况。返回值-1表示脏缓冲区还不太多,因此不需要回写。返回值0表示可以用异步方式回写脏缓冲区(表明脏缓冲区已经多到必须回写的地步了,但又还可以忍受,因此采用异步方式回写)。返回值1表示必须采用同步阻塞方式回写脏缓冲区(脏缓冲区已经多到系统无法忍受的地步了,所以其他事情就等到把脏缓冲区都回写完以后再说吧:)。 ②然后调用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); }

    ⑷将缓冲区标记为“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); } Chapter 3 对缓冲区的操作 Bcache机制中对缓冲区本身的操作函数主要可以分为以下几类: 1. 缓冲区的分配:也即如何从Buddy分配器中分配空闲缓冲区的内存。 2. 缓冲区的访问接口getblk/brelse。 3. 数据块读接口bread。 4. 如何同步一个inode对象的i_dirty_buffers链表中的脏缓冲区。 5. 缓冲区同步机制。 本章将讨论前4类操作。缓冲区同步机制将在第4章讨论。 

    发表于 @ 2006年12月03日 20:26:00 | 评论 (0)  VFS 缓冲区缓存Buffer Cache实现原理剖析(2)

    Chapter 3 对缓冲区的操作 Bcache机制中对缓冲区本身的操作函数主要可以分为以下几类: 1. 缓冲区的分配:也即如何从Buddy分配器中分配空闲缓冲区的内存。 2. 缓冲区的访问接口getblk/brelse。 3. 数据块读接口bread。 4. 如何同步一个inode对象的i_dirty_buffers链表中的脏缓冲区。 5. 缓冲区同步机制。 本章将讨论前4类操作。缓冲区同步机制将在第4章讨论。

    3.1 缓冲区的分配 处于效率的考虑,缓冲区并不是作为单个内存对象来分配的。相反,Linux直接通过Buddy系统以物理页帧为单位为缓冲区分配物理内存。通常,这种物理页帧也称为“缓冲区页”(buffer page)。 在PC体系结构中,根据所允许的块的大小不同,一个buffer page中可以包含8、4、2甚至1个缓冲区(对应的buffer大小为512、1024、2048和4096)。在同一个缓冲区页中的所有缓冲区都必须有相同的大小。缓冲区首部对象buffer_head中的b_this_page指针域把一个缓冲区页中所包含的所有缓冲区连接成一个单向循环链表,其b_page指针域指向相应物理页帧的页描述符page结构。而如果某个页描述符page结构指向一个缓冲区页,则该page结构中的buffers指针域就指向该页中所包含的第一个缓冲区(物理地址最低的那个)的缓冲区首部;否则该域就为NULL。 全局变量buffermem_pages表示缓冲区页的总数量。它定义域buffer.c文件中: atomic_t buffermem_pages = ATOMIC_INIT(0); 在系统运行时,如果某个空闲缓冲区链表free_list[i]为空,则需要从Buddy系统中申请分配额外的缓冲区页,并在其中创建相应大小的新空闲缓冲区。 函数refill_freelist()完成上述功能。①该函数首先调用balance_dirty()函数来平衡lru_list链表中的脏缓冲区个数;②然后就调用free_shortage()函数看看各内存区(ZONE)中是否缺少空闲物理页帧,如果是,那就调用page_launder()函数来清洗那些不活跃的脏物理页帧。③最后,调用grow_buffer()函数来实际进行新缓冲区的分配工作。函数源代码如下(fs/buffer.c): static void refill_freelist(int size) { balance_dirty(NODEV); if (free_shortage()) page_launder(GFP_BUFFER, 0); grow_buffers(size); }

    函数grow_buffer()为某个特定的空闲缓冲区链表free_list[i]分配相应大小的新缓冲区。其源代码如下所示(fs/buffer.c): /* * Try to increase the number of buffers available: the size argument * is used to determine what kind of buffers we want. */ static int grow_buffers(int size) { struct page * page; struct buffer_head *bh, *tmp; struct buffer_head * insert_point; int isize;

    if ((size & 511) || (size > PAGE_SIZE)) { printk("VFS: grow_buffers: size = %d/n",size); return 0; }

    page = alloc_page(GFP_BUFFER); if (!page) goto out; LockPage(page); bh = create_buffers(page, size, 0); if (!bh) goto no_buffer_head;

    isize = BUFSIZE_INDEX(size);

    spin_lock(&free_list[isize].lock); insert_point = free_list[isize].list; tmp = bh; while (1) { if (insert_point) { tmp->b_next_free = insert_point->b_next_free; tmp->b_prev_free = insert_point; insert_point->b_next_free->b_prev_free = tmp; insert_point->b_next_free = tmp; } else { tmp->b_prev_free = tmp; tmp->b_next_free = tmp; } insert_point = tmp; if (tmp->b_this_page) tmp = tmp->b_this_page; else break; } tmp->b_this_page = bh; free_list[isize].list = bh; spin_unlock(&free_list[isize].lock);

    page->buffers = bh; page->flags &= ~(1 << PG_referenced); lru_cache_add(page); UnlockPage(page); atomic_inc(&buffermem_pages); return 1;

    no_buffer_head: UnlockPage(page); page_cache_release(page); out: return 0; } 对该函数的NOTE如下: ①首先,判断参数size是否为512的倍数,是否大于PAGE_SIZE。 ②然后,调用Buddy系统的alloc_page()宏分配一个新的物理页帧。如果分配失败,则跳转到out部分,直接返回(返回值为0)。如果分配成功,则调用LockPage()宏(Mm.h)对该物理页帧进行加锁(即设置page->flags的PG_locked标志位)。 ③然后调用create_buffers()函数在所分配的物理页帧中创建空闲缓冲区,该函数返回该物理页帧中的第一个缓冲区(首地址的页内偏移为0的那个缓冲区)的buffer_head对象指针。每一个buffer_head对象中的b_this_page指向该物理页帧中的下一个缓冲区,但是最后一个缓冲区的buffer_head对象的b_this_page指针为NULL。 ④如果create_buffers()函数返回NULL,则说明创建缓冲区失败,失败的原因是不能从buffer_head对象的缓存(包括unused_list链表和bh_cachep SLAB缓存)中得到一个未使用的buffer_head对象。于是跳转到no_buffer_head部分,该部分做两件事:①用UnlockPage宏对所分配的缓冲区进行解锁;②调用page_cache_release()宏(实际上就是Buddy系统的__free_page宏)释放所分配的缓冲区。 ⑤如果create_buffers()函数返回非NULL指针。则接下来的while循环将把所创建的空闲缓冲区的buffer_head对象插入到相对应的free_list[I]链表的首部。然后,修改缓冲区中的最后一个缓冲区的b_this_page指针,使其指向第一个缓冲区的buffer_head对象;同时修改free_list[I]链表的表头指针。 ⑥最后,将page->buffers指针指向第一个缓冲区的buffer_head对象,并对缓冲区页进行解锁,增加变量buffermem_pages的值(加1),然后返回1表示grow_buffers函数执行成功。

    函数create_buffers()在指定的空闲缓冲区页内常见特定大小的缓冲区。NOTE! 如果参数async=1的话,则表明函数是在为异步页I/O创建空闲缓冲区,此时该函数必须总是执行成功。其源代码如下(fs/buffer.c): /* * Create the appropriate buffers when given a page for data area and * the size of each buffer.. Use the bh->b_this_page linked list to * follow the buffers created. Return NULL if unable to create more * buffers. * The async flag is used to differentiate async IO (paging, swapping) * from ordinary buffer allocations, and only async requests are allowed * to sleep waiting for buffer heads. */ static struct buffer_head * create_buffers(struct page * page, unsigned long size, int async) { struct buffer_head *bh, *head; long offset;

    try_again: head = NULL; offset = PAGE_SIZE; while ((offset -= size) >= 0) { bh = get_unused_buffer_head(async); if (!bh) goto no_grow;

    bh->b_dev = B_FREE; /* Flag as unused */ bh->b_this_page = head; head = bh;

    bh->b_state = 0; bh->b_next_free = NULL; bh->b_pprev = NULL; atomic_set(&bh->b_count, 0); bh->b_size = size;

    set_bh_page(bh, page, offset);

    bh->b_list = BUF_CLEAN; bh->b_end_io = NULL; } return head; /* * In case anything failed, we just free everything we got. */ no_grow: if (head) { spin_lock(&unused_list_lock); do { bh = head; head = head->b_this_page; __put_unused_buffer_head(bh); } while (head); spin_unlock(&unused_list_lock);

    /* Wake up any waiters ... */ wake_up(&buffer_wait); }

    /* * Return failure for non-async IO requests. Async IO requests * are not allowed to fail, so we have to wait until buffer heads * become available. But we don't want tasks sleeping with * partially complete buffers, so all were released above. */ if (!async) return NULL;

    /* We're _really_ low on memory. Now we just * wait for old buffer heads to become free due to * finishing IO. Since this is an async request and * the reserve list is empty, we're sure there are * async buffer heads in use. */ run_task_queue(&tq_disk);

    /* * Set our state for sleeping, then check again for buffer heads. * This ensures we won't miss a wake_up from an interrupt. */ wait_event(buffer_wait, nr_unused_buffer_heads >= MAX_BUF_PER_PAGE); goto try_again; } 对该函数的NOTE如下: ①函数首先用一个while循环从缓冲区页的尾部开始创建缓冲区(逆续),也即从最后一个缓冲区(首地址页内偏移为PAGE_SIZE-size)到第一个缓冲区(首地址页内偏移为0)的逆续创建该缓冲区页内的缓冲区。 ②调用get_unused_buffer_head()函数从unused_list链表中或bh_cachep SLAB中得到一个bh对象,如果失败,则跳转到no_grow部分。注意!即使对于异步页I/O而言,get_unused_buffer_head()函数也是可能失败的。 ③如果get_unused_buffer_head()函数成功地返回一个未使用的bh对象,则对该bh对象进行初始化:(a)b_dev被设置成B_FREE,表明这是一个空闲缓冲区。(b)正确地设置b_this_page指针。注意,最后一个缓冲区的bh对象的b_this_page指针为NULL;(c)调用set_bh_page()函数设置bh对象的b_page指针和b_data指针。 ④如果上述while循环成功结束,则返回第一个缓冲区的bh对象的指针。函数成功地结束。 ⑤no_grow部分: n 首先判断前面的while循环是否已经分配了部分bh对象。如果是,则通过__put_unused_buffer_head()函数将这些bh对象重新释放回bh对象缓存(unused_list链表和bh_cachep SLAB)中。然后,通过wake_up函数唤醒buffer_wait等待队列中的睡眠进程。 n 判断是同步I/O还是异步I/O。如果是同步I/O的话,则直接返回NULL,表示失败。如果create_buffer()函数是在为异步I/O创建缓冲区的话,那么说明有人则在使用异步缓冲区的bh对象,因此我们只有等待,然后重试即可。

    3.2 缓冲区访问接口getblk/brelse 缓冲区访问接口getblk()函数和brelse()函数是bcache机制向内核其它模块所提供的最重要的服务例程之一。当内核需要读写某块设备上的某个块时,首先必须通过getblk()函数来得到该块在bcache中相对应的缓冲区,并在使用完该缓冲区后调用brelse()还是释放对该缓冲区的引用。

    ⑴getblk()函数接口 该函数得到块(dev,block)在缓冲区缓存中相应的缓冲区,大小则由参数size指定。如果相应的缓冲区还不存在于bcache机制中,则必须从空闲缓冲区链表中摘取一个新项。注意!getblk()函数必须总是执行成功。其源代码如下(fs/buffer.c):] struct buffer_head * getblk(kdev_t dev, int block, int size) { struct buffer_head * bh; int isize;

    repeat: spin_lock(&lru_list_lock); write_lock(&hash_table_lock); bh = __get_hash_table(dev, block, size); if (bh) goto out;

    isize = BUFSIZE_INDEX(size); spin_lock(&free_list[isize].lock); bh = free_list[isize].list; if (bh) { __remove_from_free_list(bh, isize); atomic_set(&bh->b_count, 1); } spin_unlock(&free_list[isize].lock);

    /* * OK, FINALLY we know that this buffer is the only one of * its kind, we hold a reference (b_count>0), it is unlocked, * and it is clean. */ if (bh) { init_buffer(bh, NULL, NULL); bh->b_dev = dev; bh->b_blocknr = block; bh->b_state = 1 << BH_Mapped;

    /* Insert the buffer into the regular lists */ __insert_into_queues(bh); out: write_unlock(&hash_table_lock); spin_unlock(&lru_list_lock); touch_buffer(bh); return bh; }

    /* * If we block while refilling the free list, somebody may * create the buffer first ... search the hashes again. */ write_unlock(&hash_table_lock); spin_unlock(&lru_list_lock); refill_freelist(size); goto repeat; } 对该函数的注释如下: ①首先调用__get_hash_table()在bcache中查找是否存在相应的缓冲区。如果找到,__get_hash_table()函数将增加该缓冲区的bh对象的引用计数。然后跳转到out部分,并在该部分调用touch_buffer()宏(实际上就是SetPageReferenced()宏)设置缓冲区所在物理页帧的PG_Referenced标志位。然后就可以返回了。 ②否则如果__get_hash_table()函数返回为NULL,也即指定的块在bcache中还没有相对应的缓冲区。那么就根据参数size,从相应free_list[BUFSIZE_INDEX(size)]链表中摘下一个空闲的缓冲区,并将相应的bh对象的引用计数设置为1。如果这一步成功,那么就对该bh对象进行初始化(主要是设置b_dev、b_blocknr和d_state三个成员),然后就调用__insert_into_queues()函数将这个bh对象插入到lru_list链表和哈希链表中。最后,通过执行out部分的代码,函数成功返回。 ③如果相应的free_list[BUFSIZE_INDEX(size)]链表为NULL的话,则调用refill_freelist()函数为该空闲缓冲区链表分配新的空闲缓冲区。然后跳转到repeat,再执行一次。

    ⑵释放接口brelse()函数 函数__brelse()用于释放对一个bh对象的引用。注意:该函数仅在b_count>0时将引用计数值减1。如下所示(fs/buffer.c): void __brelse(struct buffer_head * buf) { if (atomic_read(&buf->b_count)) { atomic_dec(&buf->b_count); return; } printk("VFS: brelse: Trying to free free buffer/n"); } Linux又在头文件fs.h中以__brelse()为基础封装了brelse()函数,如下: static inline void brelse(struct buffer_head *buf) { if (buf) __brelse(buf); } 函数__bforget()用于也是用于释放对一个bh对象的引用。但它与__brelse的区别是:__bforget()在将引用计数减到0时,将把该缓冲区移到相应的free_list链表中。如下所示(fs/buffer.c): void __bforget(struct buffer_head * buf) { /* grab the lru lock here to block bdflush. */ spin_lock(&lru_list_lock); write_lock(&hash_table_lock); if (!atomic_dec_and_test(&buf->b_count) || buffer_locked(buf)) goto in_use; __hash_unlink(buf); remove_inode_queue(buf); write_unlock(&hash_table_lock); __remove_from_lru_list(buf, buf->b_list); spin_unlock(&lru_list_lock); put_last_free(buf); return;

    in_use: write_unlock(&hash_table_lock); spin_unlock(&lru_list_lock); } NOTE: ①如果b_count在减一后还大于0,或者是缓冲区已经被加锁,则说明该缓冲区还在使用中,于是跳转到in_use部分,直接返回。 ②否则,就调用__hash_unlink()将这个bh对象从哈希链表中摘除,调用remove_inode_queue()函数将这个缓冲区从相应inode的I_dirty_buffers链表中摘除;调用__remove_freom_lru_list()函数将这个缓冲区从相应的lru_list链表中摘除。最后调用put_last_free()将这个缓冲区移到相应的空闲缓冲区链表中。 Linux又在fs.h头文件中已__bforget()为基础封装了函数bforget(),如下: static inline void bforget(struct buffer_head *buf) { if (buf) __bforget(buf); }

    3.3数据块读接口bread 为了读一个磁盘块,进程可以调用bcache机制的高层接口bread。该函数首先调用getblk()在缓冲区缓存中搜索这个磁盘块所对应的缓冲区。如果命中,则内核就可以不必物理地从磁盘上读该块,而可以直接返回。如果所对应的缓冲区不在bcache中,则启动块设备驱动程序的磁盘读例程,然后让进程去睡眠。其源代码如下所示(fs/buffer.c): /* * bread() reads a specified block and returns the buffer that contains * it. It returns NULL if the block was unreadable. */ struct buffer_head * bread(kdev_t dev, int block, int size) { struct buffer_head * bh;

    bh = getblk(dev, block, size); if (buffer_uptodate(bh)) return bh; ll_rw_block(READ, 1, &bh); wait_on_buffer(bh); if (buffer_uptodate(bh)) return bh; brelse(bh); return NULL; } NOTE: ①调用getblk()在bcache中搜索对应的缓冲区是否存在。 ②如果在bcache中找到对应的缓冲区,则判断该缓冲区是否是最新的(也即设置了BH_Uptodate标志位)。如果是最新的,那就可以直接返回了。 ③如果不是最新的,则调用ll_rw_block()函数让块设备驱动程序将相应的块读到缓冲区中。 ④然后,调用wait_on_buffer()函数调用等待在该缓冲区上。从该函数醒来后,再度判断缓冲区是否是最新的。如果是,就直接返回该缓冲区。否则就调用brelse()函数释放缓冲区,然后返回NULL。

    3.4 对inode的i_dirty_buffers链表中的脏缓冲区的操作 如果一个缓冲区和某个inode关联,则该缓冲区通过b_inode_buffers域链入相应inode对象的i_dirty_buffers链表中。 函数inode_has_buffers()判断一个inode对象是否有相关联的脏缓冲区,如下所示(buffer.c): int inode_has_buffers(struct inode *inode) { int ret;

    spin_lock(&lru_list_lock); ret = !list_empty(&inode->i_dirty_buffers); spin_unlock(&lru_list_lock);

    return ret; } 可以看出,如果i_dirty_buffers表头不为空,则该inode对象就有相关联的脏缓冲区。

    3.4.1 对i_dirty_buffers链表的操作 ⑴插入操作 函数buffer_insert_inode_queue()将一个缓冲区的bh对象插入到指定inode对象的i_dirty_buffers链表的头部,如下所示(fs/buffer.c): void buffer_insert_inode_queue(struct buffer_head *bh, struct inode *inode) { spin_lock(&lru_list_lock); if (bh->b_inode) list_del(&bh->b_inode_buffers); bh->b_inode = inode; list_add(&bh->b_inode_buffers, &inode->i_dirty_buffers); spin_unlock(&lru_list_lock); }

    ⑵删除操作 内部函数__remove_inode_queue()将一个指定的bh对象从他所属的I_dirty_buffers链表中删除。而函数remove_inode_queue()则是它的封装。如下所示(fs/buffer.c): /* The caller must have the lru_list lock before calling the remove_inode_queue functions. */ static void __remove_inode_queue(struct buffer_head *bh) { bh->b_inode = NULL; list_del(&bh->b_inode_buffers); }

    static inline void remove_inode_queue(struct buffer_head *bh) { if (bh->b_inode) __remove_inode_queue(bh); } 注意!调用这两个函数之前,调用者必须先持有自旋锁lru_list_lock。 ⑶mark_buffer_dirty_inode()函数 该函数将一个缓冲区标记为脏,然后将它插入到指定的inode对象的i_dirty_buffers链表的头部。如下所示(fs.h): static inline void mark_buffer_dirty_inode(struct buffer_head *bh, struct inode *inode) { mark_buffer_dirty(bh); buffer_insert_inode_queue(bh, inode); }

    3.4.2 i_dirty_buffers链表中脏缓冲区的同步

    ⑴使I_dirty_buffers链表中脏缓冲区无效 函数invalidate_inode_buffer()是一个给定inode对象的I_dirty_buffers链表中的所有缓冲区都无效。NOTE!该函数仅仅将链表中的脏缓冲区从链表中摘除,除此之外,他不做任何事情。如下(fs/buffer.c): void invalidate_inode_buffers(struct inode *inode) { struct list_head *list, *next;

    spin_lock(&lru_list_lock); list = inode->i_dirty_buffers.next; while (list != &inode->i_dirty_buffers) { next = list->next; remove_inode_queue(BH_ENTRY(list)); list = next; } spin_unlock(&lru_list_lock); }

    ⑵同步回写I_dirty_buffers中的脏缓冲区 函数fsync_inode_buffers()将一个指定inode对象中I_dirty_buffers链表这两个的所有脏缓冲区同步地回写到磁盘设备中。 该函数的实现过程主要分两个阶段:①第一个阶段,将指定inode对象的I_dirty_buffers链表中的所有脏缓冲区拷贝到一个临时inode对象tmp的I_dirty_buffers链表中,并对其中的每一个脏缓冲区安排回写请求(通过ll_rw_block()函数)。②第二阶段,对tmp对象这两个I_dirty_buffers链表中的每一个缓冲区调用wait_on_buffer()函数,以等待该缓冲区被解锁(也即等待该缓冲区的回写请求完成)。 由于在第二个阶段期间,当函数通过wait_on_buffer()等待某个缓冲区被解锁期间,其他进程可能会对指定inode对象对应的文件发出写操作,因此前面已经被解锁的缓冲区可能会再次变脏,因而它们可能会再次进入该inode对象的I_dirty_buffers链表中。因此为了让这些缓冲区也得到被等待的机会(也即对它们调用wait_on_buffer()函数),函数fsync_inode_buffers()最后调用osync_inode_buffers()函数,从而使在第二个阶段期间又变脏的缓冲区都同步地被回写。 该函数的源码如下(fs/buffer.c): /* * Synchronise all the inode's dirty buffers to the disk. * * We have conflicting pressures: we want to make sure that all * initially dirty buffers get waited on, but that any subsequently * dirtied buffers don't. After all, we don't want fsync to last * forever if somebody is actively writing to the file. * * Do this in two main stages: first we copy dirty buffers to a * temporary inode list, queueing the writes as we go. Then we clean * up, waiting for those writes to complete. * * During this second stage, any subsequent updates to the file may end * up refiling the buffer on the original inode's dirty list again, so * there is a chance we will end up with a buffer queued for write but * not yet completed on that list. So, as a final cleanup we go through * the osync code to catch these locked, dirty buffers without requeuing * any newly dirty buffers for write. */

    int fsync_inode_buffers(struct inode *inode) { struct buffer_head *bh; struct inode tmp; int err = 0, err2;

    INIT_LIST_HEAD(&tmp.i_dirty_buffers);

    spin_lock(&lru_list_lock);

    while (!list_empty(&inode->i_dirty_buffers)) { bh = BH_ENTRY(inode->i_dirty_buffers.next); list_del(&bh->b_inode_buffers); if (!buffer_dirty(bh) && !buffer_locked(bh)) bh->b_inode = NULL; else { bh->b_inode = &tmp; list_add(&bh->b_inode_buffers, &tmp.i_dirty_buffers); if (buffer_dirty(bh)) { atomic_inc(&bh->b_count); spin_unlock(&lru_list_lock); ll_rw_block(WRITE, 1, &bh); brelse(bh); spin_lock(&lru_list_lock); } } }

    while (!list_empty(&tmp.i_dirty_buffers)) { bh = BH_ENTRY(tmp.i_dirty_buffers.prev); remove_inode_queue(bh); atomic_inc(&bh->b_count); spin_unlock(&lru_list_lock); wait_on_buffer(bh); if (!buffer_uptodate(bh)) err = -EIO; brelse(bh); spin_lock(&lru_list_lock); }

    spin_unlock(&lru_list_lock); err2 = osync_inode_buffers(inode);

    if (err) return err; else return err2; } 注释: ①首先,将临时inode对象tmp的I_dirty_buffers链表中初始化为NULL。 ②第一个while循环完成上述所说的第一个阶段的任务。它先将指定inode对象的I_dirty_buffers链表中的脏缓冲区从链表中删除。然后判断这个缓冲区的状态是否既不为脏也未被上锁,如果是这样,则简单地将缓冲区的bh对象的b_inode指针设置为NULL就可以了。否则就将这个缓冲区加到tmp对象的I_dirty_buffers链表中;同时对于脏的缓冲区还要调用ll_rw_block()函数安排回写请求(注意!对于已经加锁的缓冲区我们已经安排了回写请求,因此这里就不必再安排了)。函数ll_rw_block()将清除BH_Dirty标志位,并设置BH_Lock标志位。 ③接下来的while循环完成第二个阶段的任务。首先调用remove_inode_queue()函数将缓冲区从tmp对象的I_dirty_buffers链表中摘除。然后,调用wait_on_buffer()函数等待该缓冲区的回写请求被完成(回写操作完成后,BH_Lock标志将被清除,BH_Uptodate标志将被置位)。 ④最后,由于上面我们所述的原因,调用osync_inode_buffers()函数让第二个阶段期间再次变脏且已经被安排了回写的缓冲区再次得到被等待的机会。

    函数osync_inode_buffers()的源代码如下: /* * osync is designed to support O_SYNC io. It waits synchronously for * all already-submitted IO to complete, but does not queue any new * writes to the disk. * * To do O_SYNC writes, just queue the buffer writes with ll_rw_block as * you dirty the buffers, and then use osync_inode_buffers to wait for * completion. Any other dirty buffers which are not yet queued for * write will not be flushed to disk by the osync. */

    int osync_inode_buffers(struct inode *inode) { struct buffer_head *bh; struct list_head *list; int err = 0;

    spin_lock(&lru_list_lock);

    repeat:

    for (list = inode->i_dirty_buffers.prev; bh = BH_ENTRY(list), list != &inode->i_dirty_buffers; list = bh->b_inode_buffers.prev) { if (buffer_locked(bh)) { atomic_inc(&bh->b_count); spin_unlock(&lru_list_lock); wait_on_buffer(bh); if (!buffer_uptodate(bh)) err = -EIO; brelse(bh); spin_lock(&lru_list_lock); goto repeat; } }

    spin_unlock(&lru_list_lock); return err; } NOTE: ①for循环从指定inode对象的I_dirty_buffers链表的表尾开始扫描其中的缓冲区,并判断链表中的最后一个缓冲区是否已被加锁(设置了BH_Lock标志)。如果是这说明已经对该缓冲区安排了回写操作(也即已经对这个缓冲区调用ll_rw_block()函数),于是对这个缓冲区调用wait_on_buffer()函数以等待这个缓冲区的回写操作被完成。从wait_on_buffer()函数醒来以后,立即检查缓冲区的状态是否设置了BH_Uptodate标志,如果不是,这说明发生了I/O错误。然后,跳转到repeat重新执行for循环。 ②从for循环退出后,I_dirty_buffers链表中将不再有任何被加锁的缓冲区。

     

     

     

     

     

     

     

     

     

    Chapter 4 bcache中脏缓冲区的同步机制 Unix/Linux系统对脏缓冲区的同步问题采用了延迟写的办法。当进程发出块设备I/O写请求时,数据内容实际上是先被写到某个对应的缓冲区中(因而也是对应的缓冲区变脏),而不是立即写到物理块设备中。因为随后对这一相同的块还可能会发生些操作,所以当前内容可能会被覆盖。从而也避免了多余的磁盘物理写操作。 由于脏缓冲区可能直到最后一刻(即直到系统关闭时)都一直逗留在主存中。因此这种延迟写方法有两个缺点: 1. 如果发生硬件错误或电源掉电的情况,那就无法在得到RAM中的内容。因此,从系统启动以来所有对文件进行的很多修改都将丢失。 2. bcache中的脏缓冲区会越来越多,因此会使bcache中的空闲缓冲区变得紧缺。 由于以上缺点,因此必须在某些条件下,有内核把bcache中的脏缓冲区真正地回写到磁盘。为此bcache提供了三种方法: 1. 当脏缓冲区变得太满,但内核由还需要更多的缓冲区时,就会激活bdflush内核线程,将一部分脏缓冲区回写到磁盘中。 2. 如果自从脏缓冲区变脏以来已经过去太长时间,kupdate内核线程会周期性地刷新“年长”(old)的脏缓冲区。 3. 进程可以显示地通过系统调用sync()、fsync()或fdatasync()来刷新特定块设备的所有缓冲区或特定文件的所有缓冲区。

    4.1 bcache中的脏缓冲区同步操作 函数sync_buffers()用来实现将属于某个特定块设备的所有脏缓冲区真正地回写到块设备中。其原型如下: static int sync_buffers(kdev_t dev, int wait) 参数dev指定逻辑块设备的设备标识符,参数wait指定是否等待所有脏缓冲区的回写操作完成后函数才返回。 对于wait=0的情况,sync_buffers()函数仅仅只是扫描BUF_DIRTY链表,并对其中的脏缓冲区安排回写操作即可(通过调用块设备驱动程序的ll_rw_block()函数)。 对于wait非0的情况,处理就比较复杂些。Sync_buffers()函数在一个do{}while循环中分三次扫描处理BUF_DIRTY链表和BUF_LOCKED链表:①第一遍扫描,仅仅对BUF_DIRTY链表中的Dirty且unlocked的缓冲区通过ll_rw_block()函数安排回写操作;②第二编循环中主要调用wait_on_buffer()函数等待第一遍所安排的回写操作真正完成。由于在等待过程中,可能会有新的脏缓冲区插入到BUF_DIRTY链表中,因此在第二编循环中,对BUF_DIRTY链表和BUF_LOCKED链表的扫描每次总是从链表的表头开始,如果扫描的过程中碰到Dirty缓冲区,那么也要通过ll_rw_block()函数对其安排回写操作。在第二编循环结束时,BUF_DIRTY链表中将不再有任何Dirty缓冲区。③第三便循环时这仅仅是为了等待第二遍所安排的回写操作结束。 函数的源代码如下(fs/buffer.c): /* Godamity-damn. Some buffers (bitmaps for filesystems) * spontaneously dirty themselves without ever brelse being called. * We will ultimately want to put these in a separate list, but for * now we search all of the lists for dirty buffers. */ static int sync_buffers(kdev_t dev, int wait) { int i, retry, pass = 0, err = 0; struct buffer_head * bh, *next;

    /* One pass for no-wait, three for wait: * 0) write out all dirty, unlocked buffers; * 1) write out all dirty buffers, waiting if locked; * 2) wait for completion by waiting for all buffers to unlock. */ do { retry = 0;

    /* We search all lists as a failsafe mechanism, not because we expect * there to be dirty buffers on any of the other lists. */ repeat: spin_lock(&lru_list_lock); bh = lru_list[BUF_DIRTY]; if (!bh) goto repeat2;

    for (i = nr_buffers_type[BUF_DIRTY]*2 ; i-- > 0 ; bh = next) { next = bh->b_next_free;

    if (!lru_list[BUF_DIRTY]) break; if (dev && bh->b_dev != dev) continue; if (buffer_locked(bh)) { /* Buffer is locked; skip it unless wait is * requested AND pass > 0. */ if (!wait || !pass) { retry = 1; continue; } atomic_inc(&bh->b_count); spin_unlock(&lru_list_lock); wait_on_buffer (bh); atomic_dec(&bh->b_count); goto repeat; }

    /* If an unlocked buffer is not uptodate, there has * been an IO error. Skip it. */ if (wait && buffer_req(bh) && !buffer_locked(bh) && !buffer_dirty(bh) && !buffer_uptodate(bh)) { err = -EIO; continue; }

    /* Don't write clean buffers. Don't write ANY buffers * on the third pass. */ if (!buffer_dirty(bh) || pass >= 2) continue;

    atomic_inc(&bh->b_count); spin_unlock(&lru_list_lock); ll_rw_block(WRITE, 1, &bh); atomic_dec(&bh->b_count); retry = 1; goto repeat; }

    repeat2: bh = lru_list[BUF_LOCKED]; if (!bh) { spin_unlock(&lru_list_lock); break; } for (i = nr_buffers_type[BUF_LOCKED]*2 ; i-- > 0 ; bh = next) { next = bh->b_next_free;

    if (!lru_list[BUF_LOCKED]) break; if (dev && bh->b_dev != dev) continue; if (buffer_locked(bh)) { /* Buffer is locked; skip it unless wait is * requested AND pass > 0. */ if (!wait || !pass) { retry = 1; continue; } atomic_inc(&bh->b_count); spin_unlock(&lru_list_lock); wait_on_buffer (bh); spin_lock(&lru_list_lock); atomic_dec(&bh->b_count); goto repeat2; } } spin_unlock(&lru_list_lock);

    /* If we are waiting for the sync to succeed, and if any dirty * blocks were written, then repeat; on the second pass, only * wait for buffers being written (do not pass to write any * more buffers on the second pass). */ } while (wait && retry && ++pass<=2); return err; } 对该函数的详细注释如下: ①函数的主体就是一个do{}while循环,并依据wait的值决定循环次数(1or3)。 ②首先,通过一个for循环来扫描BUF_DIRTY链表。For循环的循环次数是I=nr_buffers_type[BUF_DIRTY]×2。由于BUF_DIRTY链表是一个双向循环链表,因此for循环将链表扫描两次(why? I don’t know^_^ If you know, please tell me.)对于每一次被扫描的缓冲区,循环体将作如下处理: n 首先判断lru_list[BUF_DIRTY]链表是否为空。如果为NULL,则终止扫描过程。因为每一个被安排回写的脏缓冲区都会被移到BUF_LOCKED链表中,从而使BUF_DIRTY链表中的元素会越来越少。因此这里在开始处理之前有必要进行一下判断。 n 如果参数dev非0,则进一步判断当前被扫描的缓冲区是否属于指定的块设备。如果不是,则扫描量表中的下一个元素。当dev=0时,sync_buffers()函数同步所有脏缓冲区(不论它是属于哪个块设备)。 n 通过buffer_locked()宏判断被扫描的缓冲区是否已经被加锁(是否以被选中去做回写操作)。如果是,则进一步判断wait和pass的值。如果wait=0或pass=0(即第一遍do{}while循环),则不等待该缓冲区的回写操作完成,而是继续扫描链表中的下一个元素。否则(wait!=0且pass!=0)就调用wait_on_buffer()函数等待该缓冲区被解锁,然后执行goto repeat语句,重新从链表的开头开始扫描(原因如前所述)。 n 否则就检查这个unlocked缓冲区的状态是否正确。如果不正确,就忽略它,继续扫描下一个链表元素。 n 如果这个unlocked缓冲区的状态正确,则进一步判断缓冲区是否不为脏,或者是为第三编循环(在第三编循环中,即使遇到脏缓冲区,也不  

     

    最新回复(0)