分类: LINUX
2007-08-07 11:45:55
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()完成上述功能。
(1)该函数首先调用balance_dirty()函数来平衡lru_list链表中的脏缓冲区个数;
(2)然后就调用free_shortage()函数看看各内存区(ZONE)中是否缺少空闲物理页帧,如果是,那就调用page_launder()函数来清洗那些不活跃的脏物理页帧。
(3)最后,调用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如下:
(1)首先,判断参数size是否为512的倍数,是否大于PAGE_SIZE。
(2)然后,调用Buddy系统的alloc_page()宏分配一个新的物理页帧。如果分配失败,则跳转到out部分,直接返回(返回值为0)。如果分配成功,则调用LockPage()宏(Mm.h)对该物理页帧进行加锁(即设置page->flags的PG_locked标志位)。
(3)然后调用create_buffers()函数在所分配的物理页帧中创建空闲缓冲区,该函数返回该物理页帧中的第一个缓冲区(首地址的页内偏移为0的那个缓冲区)的buffer_head对象指针。每一个buffer_head对象中的b_this_page指向该物理页帧中的下一个缓冲区,但是最后一个缓冲区的buffer_head对象的b_this_page指针为NULL。
(4)如果create_buffers()函数返回NULL,则说明创建缓冲区失败,失败的原因是不能从buffer_head对象的缓存(包括unused_list链表和bh_cachep SLAB缓存)中得到一个未使用的buffer_head对象。于是跳转到no_buffer_head部分,该部分做两件事:
<1>用UnlockPage宏对所分配的缓冲区进行解锁;
<2>调用page_cache_release()宏(实际上就是Buddy系统的__free_page宏)释放所分配的缓冲区。
(5)如果create_buffers()函数返回非NULL指针。则接下来的while循环将把所创建的空闲缓冲区的buffer_head对象插入到相对应的free_list〔I〕链表的首部。然后,修改缓冲区中的最后一个缓冲区的b_this_page指针,使其指向第一个缓冲区的buffer_head对象;同时修改free_list〔I〕链表的表头指针。
(6)最后,将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) {
//从unused_list链表中或bh_cachep SLAB中得到一个bh对象
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_page指针和b_data指针
bh->b_list = BUF_CLEAN; //是否表示链入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如下:
(1)函数首先用一个while循环从缓冲区页的尾部开始创建缓冲区(逆续),也即从最后一个缓冲区(首地址页内偏移为PAGE_SIZE-size)到第一个缓冲区(首地址页内偏移为0)的逆续创建该缓冲区页内的缓冲区。
(2)调用get_unused_buffer_head()函数从unused_list链表中或bh_cachep SLAB中得到一个bh对象,如果失败,则跳转到no_grow部分。注意!即使对于异步页I/O而言,get_unused_buffer_head()函数也是可能失败的。
(3)如果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指针。
(4)如果上述while循环成功结束,则返回第一个缓冲区的bh对象的指针。函数成功地结束。
(5)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()还是释放对该缓冲区的引用。
1.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;
}
对该函数的注释如下:
(1)首先调用__get_hash_table()在bcache中查找是否存在相应的缓冲区。如果找到,__get_hash_table()函数将增加该缓冲区的bh对象的引用计数。然后跳转到out部分,并在该部分调用touch_buffer()宏(实际上就是SetPageReferenced()宏)设置缓冲区所在物理页帧的PG_Referenced标志位。然后就可以返回了。
(2)否则如果__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部分的代码,函数成功返回。
(3)如果相应的free_list〔BUFSIZE_INDEX(size)〕链表为NULL的话,则调用refill_freelist()函数为该空闲缓冲区链表分配新的空闲缓冲区。然后跳转到repeat,再执行一次。
2.释放接口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:
(1)如果b_count在减一后还大于0,或者是缓冲区已经被加锁,则说明该缓冲区还在使用中,于是跳转到in_use部分,直接返回。
(2)否则,就调用__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:
(1)调用getblk()在bcache中搜索对应的缓冲区是否存在。
(2)如果在bcache中找到对应的缓冲区,则判断该缓冲区是否是最新的(也即设置了BH_Uptodate标志位)。如果是最新的,那就可以直接返回了。
(3)如果不是最新的,则调用ll_rw_block()函数让块设备驱动程序将相应的块读到缓冲区中。
(4)然后,调用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链表的操作
1.插入操作
函数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);
}
2.删除操作
内部函数__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。