当内核需要读或写一个单独的物理设备块时(例如一个超级块),必须检查所请求的块缓冲区是否已经在页高速缓存中。在页高速缓存中搜索指定的块缓冲区(由块设备描述符的地址bdev和逻辑块号nr表示)的过程分成三个步骤:
1. 获取一个指针,让它指向包含指定块的块设备的address_space对象(bdev->bd_inode->i_mapping)。
2. 获得设备的块大小(bdev->bd_block_size),并计算包含指定块的页索引。这需要在逻辑块号上进行位移操作。例如,如果块的大小是1024字节,每个缓冲区页包含四个块缓冲区,那么页的索引是nr/4。
3. 在块设备的基树中搜索缓冲区页。获得页描述符之后,内核访问缓冲区首部,它描述了页中块缓冲区的状态。
不过,实现的细节要更为复杂。为了提高系统性能,内核维持一个小磁盘高速缓存数组bh_lrus(每个CPU对应一个数组元素),即所谓的最近最少使用(LRU)块高速缓存。每个磁盘高速缓存有8个指针,指向被指定CPU最近访问过的缓冲区首部。对每个CPU数组的元素排序,使指向最后被使用过的那个缓冲区首部的指针索引为0。相同的缓冲区首部可能出现在几个CPU数组中(但是同一个CPU数组中不会有相同的缓冲区首部)。在LRU块高速缓存中每出现一次缓冲区首部,该缓冲区首部的使用计数器b_count就加1。
函数__find_get_block()的参数有:block_device描述符地址bdev,块号block和块大小size。函数返回页高速缓存中的块缓冲区对应的缓冲区首部的地址;如果不存在指定的块,就返回NULL:
struct buffer_head *
__find_get_block(struct block_device *bdev, sector_t block, int size)
{
struct buffer_head *bh = lookup_bh_lru(bdev, block, size);
if (bh == NULL) {
bh = __find_get_block_slow(bdev, block);
if (bh)
bh_lru_install(bh);
}
if (bh)
touch_buffer(bh);
return bh;
}
数本质上执行下面的操作:
1. 检查执行CPU的LRU块高速缓存数组中是否有一个缓冲区首部,其b_bdev、b_blocknr和b_size字段分别等于bdev、block和size:
static struct buffer_head *
lookup_bh_lru(struct block_device *bdev, sector_t block, int size)
{
struct buffer_head *ret = NULL;
struct bh_lru *lru;
int i;
check_irqs_on();
bh_lru_lock();
lru = &__get_cpu_var(bh_lrus);
for (i = 0; i < BH_LRU_SIZE; i++) {
struct buffer_head *bh = lru->bhs[i];
if (bh && bh->b_bdev == bdev &&
bh->b_blocknr == block && bh->b_size == size) {
if (i) {
while (i) {
lru->bhs[i] = lru->bhs[i - 1];
i--;
}
lru->bhs[0] = bh;
}
get_bh(bh);
ret = bh;
break;
}
}
bh_lru_unlock();
return ret;
}
2. 如果缓冲区首部在LRU块高速缓存中,就刷新数组中的元素,以便让指针指在第一个位置(索引为0)刚找到的缓冲区首部,递增它的b_count字段,并跳转到第8步。
3. 如果缓冲区首部不在LRU块高速缓存中,就调用__find_get_block_slow:
static struct buffer_head *
__find_get_block_slow(struct block_device *bdev, sector_t block)
{
struct inode *bd_inode = bdev->bd_inode;
struct address_space *bd_mapping = bd_inode->i_mapping;
struct buffer_head *ret = NULL;
pgoff_t index;
struct buffer_head *bh;
struct buffer_head *head;
struct page *page;
int all_mapped = 1;
index = block >> (PAGE_CACHE_SHIFT - bd_inode->i_blkbits);
page = find_get_page(bd_mapping, index);
if (!page)
goto out;
spin_lock(&bd_mapping->private_lock);
if (!page_has_buffers(page))
goto out_unlock;
head = page_buffers(page);
bh = head;
do {
if (bh->b_blocknr == block) {
ret = bh;
get_bh(bh);
goto out_unlock;
}
if (!buffer_mapped(bh))
all_mapped = 0;
bh = bh->b_this_page;
} while (bh != head);
/* we might be here because some of the buffers on this page are
* not mapped. This is due to various races between
* file io on the block device and getblk. It gets dealt with
* elsewhere, don't buffer_error if we had some unmapped buffers
*/
if (all_mapped) {
printk("__find_get_block_slow() failed. "
"block=%llu, b_blocknr=%llu/n",
(unsigned long long)block,
(unsigned long long)bh->b_blocknr);
printk("b_state=0x%08lx, b_size=%zu/n",
bh->b_state, bh->b_size);
printk("device blocksize: %d/n", 1 << bd_inode->i_blkbits);
}
out_unlock:
spin_unlock(&bd_mapping->private_lock);
page_cache_release(page);
out:
return ret;
}
__find_get_block_slow首先根据块号和块大小得到与块设备相关的页的索引:
index = block >> (PAGE_SHIFT - bdev->bd_inode->i_blkbits)
4. 调用find_get_page()确定存有所请求的块缓冲区的缓冲区页的描述符在页高速缓存中的位置。该函数传递的参数有:指向块设备的address_space对象的指针(bdev->bd_mode->i_mapping)和页索引。页索引用于确定存有所请求的块缓冲区的缓冲区页的描述符在页高速缓存中的位置。如果高速缓存中没有这样的页,就返回NULL(失败)。具体内容请查看“页高速缓存处理函数”博文。
5. 此时,函数已经得到了缓冲区页描述符的地址:它扫描链接到缓冲区页的缓冲区首部链表,查找逻辑块号等于block的块。
6. 递减页描述符的count字段(find_get_page曾经递增它的值)。
7. 调用bh_lru_install把LRU块高速缓存中的所有元素向下移动一个位置,并把指向所请求块的缓冲区首部的指针插入到第一个位置。如果一个缓冲区首部已经不在LRU块高速缓存中,就递减它的引用计数器b_count。
8. 如果需要,就调用mark_page_accessed()把缓冲区页移至适当的LRU链表中。
9. 返回缓冲反首部指针。
古老的函数__getblk()现在的重要性也跟当年一样重要,即如果查找不到就分配一个缓冲区头。__getblk()其与__find_get_block()接收相同的参数,也就是block_device描述符的地址bdev、块号block和块大小size,并返回与缓冲区对应的缓冲区首部的地址。即使块根本不存在,该函数也不会失败,__getblk()会友好地分配块设备缓冲区页并返回将要描述块的缓冲区首部的指针。注意,__getblk()返回的块缓冲区不必存有有效数据——缓冲区首部的BH_Uptodate标志可能被清0。
struct buffer_head *
__getblk(struct block_device *bdev, sector_t block, int size)
{
struct buffer_head *bh = __find_get_block(bdev, block, size);
might_sleep();
if (bh == NULL)
bh = __getblk_slow(bdev, block, size);
return bh;
}
函数__getblk()本质上执行下面的步骤:
1. 调用__find_get_block()检查块是否已经在页高速缓存中。如果找到块,则函数返回其缓冲区首部的地址。
2. 否则,调用__getblk_slow,触发grow_buffers()为所请求的页分配一个新的缓冲区页(参见前面“把块存放在页高速缓存中”博文):
static struct buffer_head *
__getblk_slow(struct block_device *bdev, sector_t block, int size)
{
/* Size must be multiple of hard sectorsize */
if (unlikely(size & (bdev_hardsect_size(bdev)-1) ||
(size < 512 || size > PAGE_SIZE))) {
printk(KERN_ERR "getblk(): invalid block size %d requested/n",
size);
printk(KERN_ERR "hardsect size: %d/n",
bdev_hardsect_size(bdev));
dump_stack();
return NULL;
}
for (;;) {
struct buffer_head * bh;
int ret;
bh = __find_get_block(bdev, block, size);
if (bh)
return bh;
ret = grow_buffers(bdev, block, size);
if (ret < 0)
return NULL;
if (ret == 0)
free_more_memory();
}
}
3. 如果grow_buffers()分配这样的页时失败,__getblk()试图通过调用函数free_more_memory()回收一部分内存。
4. 跳转到第1步。
函数__bread()接收与__getblk()相同的参数,即block_device描述符的地址bdev、块号block和块大小size,并返回与缓冲区对应的缓冲区首部的地址。与__getblk()相反的是,如果需要的话,在返回缓冲区首部之前函数__bread()从磁盘读块,将分配到的一个空的buffer_head填满:
struct buffer_head *
__bread(struct block_device *bdev, sector_t block, int size)
{
struct buffer_head *bh = __getblk(bdev, block, size);
if (likely(bh) && !buffer_uptodate(bh))
bh = __bread_slow(bh);
return bh;
}
static struct buffer_head *__bread_slow(struct buffer_head *bh)
{
lock_buffer(bh);
if (buffer_uptodate(bh)) {
unlock_buffer(bh);
return bh;
} else {
get_bh(bh);
bh->b_end_io = end_buffer_read_sync;
submit_bh(READ, bh);
wait_on_buffer(bh);
if (buffer_uptodate(bh))
return bh;
}
brelse(bh);
return NULL;
}
函数__bread()执行下述步骤:
1. 调用__getblk()在页高速缓存中查找与所请求的块相关的缓冲区页,并获得指向相应的缓冲区首部的指针。
2. 如果块已经在页高速缓存中并包含有效数据(if(buffer_uptodate(bh))检查BH_Uptodate标志被置位),就返回缓冲区首部的地址。
3. 否则,get_bh(bh)递增缓冲区首部的引用计数器。
4. 把end_buffer_read_sync()的地址赋给b_end_io字段(参见下一博文)。
5. 调用submit_bh()把缓冲区首部传送到通用块层(参见下一博文)。
6. 调用wait_on_buffer()把当前进程插入等待队列,直到I/O操作完成,即直到缓冲区首部的BH_Lock标志被清0。
7. 返回缓冲区首部的地址。