与前两个帖子有点重复,只是想全面具体一点。
通过阅读函数do_mpage_readpage() 的代码,我可以确定 page 和 buffer_head 没有必然关系,即在页高速缓存中,如果页中的块在磁盘上不连续,那么就需要构造 buffer_head 链表,由 page 中的 private 字段指向该链表头,且 PG_private 标志置位;如果页中的块在磁盘上是连续的,就不用创建 buffer_head 链表了。
这些都可以从读流程中发现:
当要读的数据对应的页不在页高速缓存中时,do_generic_mapping_read() 函数会申请一个页,并将其添加到页高速缓存中(这两步由函数 page_cache_alloc_cold() 和 add_to_page_cache_lru() 实现),之后调用 a_ops->readpage() 函数读取这一个页。
对于ext2 文件系统, a_ops->readpage() 对应于函数 ext2_readpage() 函数,这个函数直接调用 mpage_readpage() 函数, mpage_readpage() 调用函数 do_generic_file_read() 函数来完成页的读取。
通过分析do_generic_file_read() 的代码,可以得知对于一个块在磁盘上连续的页是如何读取的,块在磁盘上不连续的页又是如何读取的。分析 do_generic_file_read() 的代码是主要工作,而前面叙述的两段是给定一个条件:页是刚分配的。
对于传入参数mp_bh 是经过 clear_buffer_mapped() 处理过的, do_generic_file_read() 的流程如下:
图中略去了关于文件洞、和对于一个新的页走不到的内容。
对于一个新分配的页,它的PG_private 是还没有被置位的。之后,通过页索引计算这个页对应的块在文件中的逻辑块号 block_in_file 和要传输的最后一块的块号。
之后,在while 循环中对页中的块作如下处理。
1、首先设定 map_bh->b_size ,之后调用 get_block() 函数得到的在文件中第 block_in_file ( block_in_file 会在循环过程中增加)块的磁盘映射(即给 mp_bh 的 b_blocknr 等赋值),调用 get_block() 函数时, map_bh->b_size 表示期望的以 block_in_file 为第一块的最大连续的块数,而调用返回后这个值会被设置为这次函数调用处理好了的,以 block_in_file 为第一块的实际的连续块数。
2、之后,这些块的在磁盘上的映射会在 for 循环中,通过语句 blocks[page_block] = map_bh->b_blocknr+relative_block; 记录到 blocks 数组中,之后 blocks 数组将会用来判断一个页中的块是否连续。
3、显然,如果这次 while 循环没有得到页中所有的块在磁盘中的映射,这 while 循环将会继续,此后在调用 get_block() 函数得到块的磁盘映射后,将会通过 blocks[page_block-1] != map_bh->b_blocknr-1 来判断是否和上次得到的连续,如果不连续则会跳转到 confused 。
对于块在磁盘上连续的页,函数最后将会调用mpage_alloc() 生成一个具有多个 io_vec 的 bio ,然后调用 bio_add_page() 将页添加进去。
如果confused ,则页中的块在磁盘上不连续(或有文件洞等其他原因),则函数会调用 block_read_full_page() 为该页读取块。
接下来,再看看mapge_alloc() 、 bio_add_page() 和 block_read_full_page() 将会有什么区别。
mpage_alloc()被调用时的语句为: mpage_alloc(bdev, blocks[0] << (blkbits - 9),min_t(int, nr_pages, bio_get_nr_vecs(bdev)),GFP_KERNEL); 第二个参数是这一页的第一个块对应的扇区号。 mpage_alloc() 先调用 bio_alloc(gfp_flags, nr_vecs) 来分配一个具有多个(在这里貌似只有一个) io_vec 的 bio ,然后给 bio->bi_bdev 、 bio->bi_sector 等赋值。
bio_add_page()将页添加到 bio 中,,它先获得该设备的 request ,然后调用函数 __bio_add_page() ,该函数对于无法合并到已有 io_vec 的页 "setup the new entry" ,通过相应的信息初始化 io_vec 中的字段,然后 bi_vcnt++ 。
对于一个新分配的页,block_read_full_page() 函数会先调用函数 create_empty_buffers() 来为该页构造构造 buffer_head 链表, 这就说明了页不是一开始就和buffer_head 相关的,它们之间没有必然的关系 。之后再以while (i++, iblock++, (bh = bh->b_this_page) != head) 为循环条件循环中为每个 buffer_head() 调用 get_block() 函数并将 buffer_head 保存到数组 arr 中。之后,遍历该数组,调用 submit_bh() 来提交 buffer_head() 。
在submit_bh 中,先调用 bio_alloc() 分配只含有一个 io_vec 的 bio ,之后将通过 buffer_head 中的内容给 bio 及其 io_vec 赋值。
综上所述,buffer_head 结构只是用来辅助块在磁盘上不连续的页来读取磁盘上的数据,与 page 没有必然的关系,并了解到 bio 至少有两种构造的方式。
do_mpage_readpage() 函数的注释在前面的贴之中有些错误,现在重新贴过(当然,可能还会有很多错误)
static struct bio *
do_mpage_readpage(struct bio *bio, struct page *page, unsigned nr_pages,
sector_t *last_block_in_bio, struct buffer_head *map_bh,
unsigned long *first_logical_block, get_block_t get_block)
{
struct inode *inode = page->mapping->host;
//将块的位数赋给blkbits
const unsigned blkbits = inode->i_blkbits;
//计算一个页面中的数据块数目
const unsigned blocks_per_page = PAGE_CACHE_SIZE >> blkbits;
//计算block 的大小
const unsigned blocksize = 1 << blkbits;
sector_t block_in_file;
sector_t last_block;
sector_t last_block_in_file;
sector_t blocks[MAX_BUF_PER_PAGE];
unsigned page_block;
unsigned first_hole = blocks_per_page;
struct block_device *bdev = NULL;
int length;
int fully_mapped = 1;
unsigned nblocks;
unsigned relative_block;
//如果是一个缓存区页(PG_private被置位),跳转到confused
//据目前的理解,PG_private置位意味着page中的缓冲区段在磁盘上不连续
if (page_has_buffers(page))
goto confused;
//通过页的索引获得相对于文件头的首个块号
block_in_file = (sector_t)page->index << (PAGE_CACHE_SHIFT - blkbits);
//计算要传输的最后一个块号,这里nr_pages是1,即该页中的最后一块
last_block = block_in_file + nr_pages * blocks_per_page;
//计算文件的最后一块块号
last_block_in_file = (i_size_read(inode) + blocksize - 1) >> blkbits;
//应该传输的最后一块的块号
if (last_block > last_block_in_file)
last_block = last_block_in_file;
page_block = 0;
/*
* Map blocks using the result from the previous get_blocks call first.
*/
//nblock使用来干什么的啊?是指一个buffer_head所能指向的块数吗?
//这么说来,buffer_head中的b_size所指的块大小和inode中的块大小不一样?
nblocks = map_bh->b_size >> blkbits;
//如果该缓冲区首部已经映射到磁盘(b_bdev、b_blocknr已经赋值),且……
//注:首次传入的map_bh是经过clear_buffer_mapped()处理过的
if (buffer_mapped(map_bh) && block_in_file > *first_logical_block &&
block_in_file < (*first_logical_block + nblocks)) {
unsigned map_offset = block_in_file - *first_logical_block;
unsigned last = nblocks - map_offset;
for (relative_block = 0; ; relative_block++) {
if (relative_block == last) {
clear_buffer_mapped(map_bh);
break;
}
if (page_block == blocks_per_page)
break;
blocks[page_block] = map_bh->b_blocknr + map_offset +
relative_block;
page_block++;
block_in_file++;
}
bdev = map_bh->b_bdev;
}
/*
* Then do more get_blocks calls until we are done with this page.
*/
map_bh->b_page = page;
//page_block初值为0,block_per_page是一页中的块数,即循环次数为页中块数,即对页中所有块进行处理
//即:一次调用get_block()获得的连续的磁盘的块数不够一页的话,就反复调用
while (page_block < blocks_per_page) {
map_bh->b_state = 0;
map_bh->b_size = 0;
if (block_in_file < last_block) {
//设置buffer_head中的size,这个buffer_head指向连续的块的块数,是指期望获得的最大连续块数?
//每次计算这个,是因为要反复使用这个结构来获取b_blocknr字段
map_bh->b_size = (last_block-block_in_file) << blkbits;
//get_block()函数将会返回buffer_head的磁盘映射,并为size的赋值
if (get_block(inode, block_in_file, map_bh, 0))
goto confused;
*first_logical_block = block_in_file;
}
//??map_bh没有映射,这应该就对应文件洞了
if (!buffer_mapped(map_bh)) {
//设置页全部映射到磁盘的标志为0
fully_mapped = 0;
//将文件洞记录下来
if (first_hole == blocks_per_page)
first_hole = page_block;
page_block++;
block_in_file++;
clear_buffer_mapped(map_bh);
//继续处理下一个块
continue;
}
/* some filesystems will copy data into the page during
* the get_block call, in which case we don't want to
* read it again. map_buffer_to_page copies the data
* we just collected from get_block into the page's buffers
* so readpage doesn't have to repeat the get_block call
*/
if (buffer_uptodate(map_bh)) {
map_buffer_to_page(page, map_bh, page_block);
goto confused;
}
//走到这步来了说明遇到了一个文件洞,但是之后的块又映射了,这时应该将遇到洞以前的块处理掉,故跳至confused
if (first_hole != blocks_per_page)
goto confused; /* hole -> non-hole */
/* Contiguous blocks? */
//通过比较blocks数组中的上一个元素中的值和这次获得的(b_blocknr-1)比较,判断是否连续
if (page_block && blocks[page_block-1] != map_bh->b_blocknr-1)
goto confused;
//计算出这个buffer_head指向的连续的块的块数
nblocks = map_bh->b_size >> blkbits;
//这个循环用来获取 块在磁盘上连续的页 中所有块 在磁盘中的块号
//循环的结束条件是buffer_head所指向的块数减一至0 或 page中所有块在磁盘中的块号都以得出
for (relative_block = 0; ; relative_block++) {
if (relative_block == nblocks) {
clear_buffer_mapped(map_bh);
break;
} else if (page_block == blocks_per_page)
break;
//为page中连续的块计算在磁盘中的编号,放入blocks数组中
blocks[page_block] = map_bh->b_blocknr+relative_block;
page_block++;
block_in_file++;
}
bdev = map_bh->b_bdev;
}
if (first_hole != blocks_per_page) {
char *kaddr = kmap_atomic(page, KM_USER0);
memset(kaddr + (first_hole << blkbits), 0,
PAGE_CACHE_SIZE - (first_hole << blkbits));
flush_dcache_page(page);
kunmap_atomic(kaddr, KM_USER0);
if (first_hole == 0) {
SetPageUptodate(page);
unlock_page(page);
goto out;
}
} else if (fully_mapped) {
SetPageMappedToDisk(page);
}
/*
* This page will go to BIO. Do we need to send this BIO off first?
*/
//首次调用时bio为NULL
//如果bio不为空,而其中最后一个block与现在的不连续,则提交以前的bio
if (bio && (*last_block_in_bio != blocks[0] - 1))
bio = mpage_bio_submit(READ, bio);
alloc_new:
if (bio == NULL) {
//通过mpage_alloc创建新的bio
// 2**9即512,blocks[0] << (blkbits - 9)得出首个扇区号
//min_t(int, nr_pages, bio_get_nr_vecs(bdev))即,这个bio中的iovec个数
bio = mpage_alloc(bdev, blocks[0] << (blkbits - 9),
min_t(int, nr_pages, bio_get_nr_vecs(bdev)),
GFP_KERNEL);
if (bio == NULL)
goto confused;
}
length = first_hole << blkbits;
//把新的页添加到已有的bio中,mpage_alloc()是不是只创建bio,但是里面的io_vec是空的?
//而给io_vec的赋值由bio_add_page函数来完成?
if (bio_add_page(bio, page, length, 0) < length) {
bio = mpage_bio_submit(READ, bio);
goto alloc_new;
}
if (buffer_boundary(map_bh) || (first_hole != blocks_per_page))
bio = mpage_bio_submit(READ, bio);
else
*last_block_in_bio = blocks[blocks_per_page - 1];
out:
return bio;
//跳转到这里的原因有:
// 1、pg_private已经置位,即该页已经存有磁盘上的内容,而且其中的块在磁盘上不连续
// 2、get_block()返回错误
// 3、遇到文件洞(页中有某块在磁盘上没有映射)
// 4、通过get_block()的在map_bh中填入的内容得出页中的块不连续
//这些原因中,对于一个新分配的页,在首次读入内容时,只会遇到后3中情况
//由此,我想我得到了为一个新页生成buffer_head链表的时机,同时也明确了不是所有的page都需要与buffer_head关联
confused:
//如果已经创建了bio,则提交它
if (bio)
bio = mpage_bio_submit(READ, bio);
//该页中的内容不是最新的,则通过block_read_full_page()函数来以每次读一块的方式读整个页
if (!PageUptodate(page))
block_read_full_page(page, get_block);
else
unlock_page(page);
goto out;
}
补充一点:page、buffer_head、bio之间的关系
峰哥叫我把page 、 buffer_head 、 bio 的关系弄清除,我感觉对于普通文件系统,这些弄得差不多了。
一、 page和 buffer_head 的关系
1、 页中的块在磁盘上连续
如果page 中的块在磁盘上连续,那么 page 的 PG_private 不会被置位, private 字段也不会指向 buffer_head 的链表。
但是page 还是得用到 buffer_head 结构,因为它需要通过 get_block() 函数来获得磁盘上的逻辑块号。
虽然ext2_getblock() 函数的代码我暂时还没有看,但是通过 do_mpage_readpage() 函数代码的阅读,可以对 get_block() 系列函数的功能进行如下猜想:
typedef int (get_block_t)(struct inode *inode, sector_t iblock,
struct buffer_head *bh_result, int create);
这类函数会得到在文件中块号iblock 在磁盘上的逻辑块号,然后赋给 bh 中的 b_blocknr 字段。在调用 get_block() 函数前, bh 中的 b_size 被赋为期望的连续的块数的总大小,返回前, get_block() 函数被设置为以 iblock 块为第一块,且在磁盘上连续的实际的块数(如果实际连续的比期望的小)。
在do_mpage_readpage() 函数中,得到了块在磁盘上的逻辑块号后, buffer_head 结构就没有什么用了,将其中的 b_blocknr 赋给了 blocks 数组后,生成 bio 的函数 mapge_alloc() 使用 blocks[0] 就行了。
2、 页中的块在磁盘上不连续
页一开始和buffer_head 是没有关系的,但是通过 get_block() 发现页中的块在磁盘上不连续等现象后,就需要调用 create_empty_buffers() 函数来为 page 创建 buffer_head 链表了。 create_empty_buffers 的结构很简单,它先调用 alloc_page_buffers() 来为 page 创建一个 buffer_head 的链表,之后为链表中每个 buffer_head 的 b_state 赋值,并顺便将该链表构造成循环链表,然后看情况设置 buffer_head 的 BH_dirty 和 BH_uptodate 标志,最 后调用attach_page_buffers() 来将 page 的 PG_private 置位。
链表建成后,page 和 buffer_head 的关系就如下图所示了:
二、 buffer_head和 bio 的关系
个人认为,buffer_head 和 bio 关系在 submit_bh() 函数中可以充分体现:
bio = bio_alloc(GFP_NOIO, 1);
bio->bi_sector = bh->b_blocknr * (bh->b_size >> 9);
bio->bi_bdev = bh->b_bdev;
bio->bi_io_vec[0].bv_page = bh->b_page;
bio->bi_io_vec[0].bv_len = bh->b_size;
bio->bi_io_vec[0].bv_offset = bh_offset(bh);
bio->bi_vcnt = 1;
bio->bi_idx = 0;
bio->bi_size = bh->b_size;
上述代码已经把buffer_head 和 bio 关系说的差不多了,就不多说了。
稍微注意一点的话,可以发现io_vec 中的 bv_page 指向了 buffer_head 中的 b_page ,即 bv_page 指向了也描述符,而 bv_offset 则是在页中的偏移,为 len 则为要传输的数据的(在这里就是块的大小)长度。
三、 page和 bio 的关系
page和 bio 的关系在上面一段中稍微说了一下,即 io_vec 中的 bv_page 字段会指向 page 。
将一个整页加到bio 中,可以看看 _add_page 函数中的如下几行( do_mpage_readpage() 函数调用 bio_add_page() 时, offset 参数是 0 ):
bvec = &bio->bi_io_vec[bio->bi_vcnt];
bvec->bv_page = page;
bvec->bv_len = len;
bvec->bv_offset = offset;
……
bio->bi_vcnt++;
这几行代码将page 、 len 等赋给一个新的 io_vec ,然后增加 bi_vcnt 的值。