2010-3-29 page buffer_head

与前两个帖子有点重复,只是想全面具体一点。

 

通过阅读函数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 的值。

 

 

 

 

 

 

你可能感兴趣的:(struct,IO,File,buffer,2010,磁盘)