2010-3-29 page buffer_head

本文详细解析了Linux内核中的页缓存机制,包括page、buffer_head和bio之间的关系,以及它们在读取磁盘数据过程中的作用。特别关注了连续与非连续磁盘块的处理方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

 

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

 

 

 

 

 

 

/* Copyright (c) 2023 Renmin University of China RMDB is licensed under Mulan PSL v2. You can use this software according to the terms and conditions of the Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2 THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. See the Mulan PSL v2 for more details. */ #include "ix_index_handle.h" #include "ix_scan.h" /** * @brief 在当前node中查找第一个>=target的key_idx * * @return key_idx,范围为[0,num_key),如果返回的key_idx=num_key,则表示target大于最后一个key * @note 返回key index(同时也是rid index),作为slot no */ int IxNodeHandle::lower_bound(const char *target) const { int left = 0, right = get_size(); while (left < right) { int mid = left + (right - left) / 2; int cmp = ix_compare(get_key(mid), target, file_hdr->col_types_, file_hdr->col_lens_); if (cmp < 0) { left = mid + 1; } else { right = mid; } } return left; } /** * @brief 在当前node中查找第一个>target的key_idx * * @return key_idx,范围为[1,num_key),如果返回的key_idx=num_key,则表示target大于等于最后一个key * @note 注意此处的范围从1开始 */ int IxNodeHandle::upper_bound(const char *target) const { // 将left初始值从1改为0,确保从第一个键开始搜索 int left = 0, right = get_size(); while (left < right) { int mid = left + (right - left) / 2; // 比较中间键目标键 if (ix_compare(get_key(mid), target, file_hdr->col_types_, file_hdr->col_lens_) <= 0) { left = mid + 1; } else { right = mid; } } return left; } /** * @brief 用于叶子结点根据key来查找该结点中的键值对 * 值value作为传出参数,函数返回是否查找成功 * * @param key 目标key * @param[out] value 传出参数,目标key对应的Rid * @return 目标key是否存在 */ bool IxNodeHandle::leaf_lookup(const char *key, Rid **value) { int pos = lower_bound(key); if (pos < get_size() && ix_compare(get_key(pos), key, file_hdr->col_types_, file_hdr->col_lens_) == 0) { *value = get_rid(pos); return true; } return false; } /** * 用于内部结点(非叶子节点)查找目标key所在的孩子结点(子树) * @param key 目标key * @return page_id_t 目标key所在的孩子节点(子树)的存储页面编号 */ page_id_t IxNodeHandle::internal_lookup(const char *key) { // 处理最小键值情况(保持不变) if (key == nullptr) { return value_at(0); } // 更可靠的最小键值检查(保持不变) bool is_min_key = true; for (int i = 0; i < file_hdr->col_tot_len_; i++) { if (static_cast<unsigned char>(key[i]) != 0) { is_min_key = false; break; } } if (is_min_key) { return value_at(0); } int pos = upper_bound(key); // 修复:直接返回pos位置的值,而不是pos-1 return value_at(pos); } /** * @brief 在指定位置插入n个连续的键值对 * 将key的前n位插入到原来keys中的pos位置;将rid的前n位插入到原来rids中的pos位置 * * @param pos 要插入键值对的位置 * @param (key, rid) 连续键值对的起始地址,也就是第一个键值对,可以通过(key, rid)来获取n个键值对 * @param n 键值对数量 * @note [0,pos) [pos,num_key) * key_slot * / \ * / \ * [0,pos) [pos,pos+n) [pos+n,num_key+n) * key key_slot */ void IxNodeHandle::insert_pairs(int pos, const char *key, const Rid *rid, int n) { assert(pos >= 0 && pos <= get_size()); assert(get_size() + n <= get_max_size()); // 移动键数组 char *keys_dst = get_key(pos + n); char *keys_src = get_key(pos); int keys_bytes = (get_size() - pos) * file_hdr->col_tot_len_; memmove(keys_dst, keys_src, keys_bytes); // 移动值数组 Rid *rids_dst = get_rid(pos + n); Rid *rids_src = get_rid(pos); int rids_bytes = (get_size() - pos) * sizeof(Rid); memmove(rids_dst, rids_src, rids_bytes); // 插入新键值对 memcpy(get_key(pos), key, n * file_hdr->col_tot_len_); memcpy(get_rid(pos), rid, n * sizeof(Rid)); set_size(get_size() + n); // 修正拼写错误 } /** * @brief 用于在结点中插入单个键值对。 * 函数返回插入后的键值对数量 * * @param (key, value) 要插入的键值对 * @return int 键值对数量 */ int IxNodeHandle::insert(const char *key, const Rid &value) { int pos = lower_bound(key); if (pos < get_size() && ix_compare(get_key(pos), key, file_hdr->col_types_, file_hdr->col_lens_) == 0) { return get_size(); // 键已存在,不插入 } insert_pair(pos, key, value); return get_size(); } /** * @brief 用于在结点中的指定位置删除单个键值对 * * @param pos 要删除键值对的位置 */ void IxNodeHandle::erase_pair(int pos) { assert(pos >= 0 && pos < get_size()); // 移动键数组 char *keys_dst = get_key(pos); char *keys_src = get_key(pos + 1); int keys_bytes = (get_size() - pos - 1) * file_hdr->col_tot_len_; memmove(keys_dst, keys_src, keys_bytes); // 移动值数组 Rid *rids_dst = get_rid(pos); Rid *rids_src = get_rid(pos + 1); int rids_bytes = (get_size() - pos - 1) * sizeof(Rid); memmove(rids_dst, rids_src, rids_bytes); set_size(get_size() - 1); } /** * @brief 用于在结点中删除指定key的键值对。函数返回删除后的键值对数量 * * @param key 要删除的键值对key值 * @return 完成删除操作后的键值对数量 */ int IxNodeHandle::remove(const char *key) { int pos = lower_bound(key); if (pos < get_size() && ix_compare(get_key(pos), key, file_hdr->col_types_, file_hdr->col_lens_) == 0) { erase_pair(pos); } return get_size(); } IxIndexHandle::IxIndexHandle(DiskManager *disk_manager, BufferPoolManager *buffer_pool_manager, int fd) : disk_manager_(disk_manager), buffer_pool_manager_(buffer_pool_manager), fd_(fd), file_hdr_(nullptr) { // 初始化指针 char* buf = new char[PAGE_SIZE]; memset(buf, 0, PAGE_SIZE); // 读取文件头页面 disk_manager_->read_page(fd, IX_FILE_HDR_PAGE, buf, PAGE_SIZE); // 反序列化到file_hdr_ file_hdr_ = new IxFileHdr(); file_hdr_->deserialize(buf); delete[] buf; // 释放缓冲区 // 设置下一个可用页号(基于文件头记录的页数) int next_page_no = file_hdr_->num_pages_; disk_manager_->set_fd2pageno(fd, next_page_no); } /** * @brief 用于查找指定键所在的叶子结点 * @param key 要查找的目标key值 * @param operation 查找到目标键值对后要进行的操作类型 * @param transaction 事务参数,如果不需要则默认传入nullptr * @return [leaf node] and [root_is_latched] 返回目标叶子结点以及根结点是否加锁 * @note need to Unlatch and unpin the leaf node outside! * 注意:用了FindLeafPage之后一定要unlatch叶结点,否则下次latch该结点会堵塞! */ std::pair<IxNodeHandle *, bool> IxIndexHandle::find_leaf_page(const char *key, Operation operation, Transaction *transaction, bool find_first) { std::scoped_lock root_lock(root_latch_); bool root_is_latched = true; if (file_hdr_->root_page_ == IX_NO_PAGE) { return {nullptr, false}; } IxNodeHandle *node = fetch_node(file_hdr_->root_page_); while (!node->is_leaf_page()) { page_id_t child_page_no; if (find_first) { // 使用空指针表示最小键值 child_page_no = node->internal_lookup(nullptr); } else { child_page_no = node->internal_lookup(key); } IxNodeHandle *child = fetch_node(child_page_no); if (root_is_latched) { root_latch_.unlock(); root_is_latched = false; } // 释放当前节点资源 buffer_pool_manager_->unpin_page(node->get_page_id(), false); delete node; node = child; } return {node, root_is_latched}; } /** * @brief 用于查找指定键在叶子结点中的对应的值result * * @param key 查找的目标key值 * @param result 用于存放结果的容器 * @param transaction 事务指针 * @return bool 返回目标键值对是否存在 */ bool IxIndexHandle::get_value(const char *key, std::vector<Rid> *result, Transaction *transaction) { auto [leaf, root_is_latched] = find_leaf_page(key, Operation::FIND, transaction); if (root_is_latched) { root_latch_.unlock(); } Rid *rid; bool found = leaf->leaf_lookup(key, &rid); if (found) { result->push_back(*rid); } buffer_pool_manager_->unpin_page(leaf->get_page_id(), false); return found; } /** * @brief 将传入的一个node拆分(Split)成两个结点,在node的右边生成一个新结点new node * @param node 需要拆分的结点 * @return 拆分得到的new_node * @note need to unpin the new node outside * 注意:本函数执行完毕后,原node和new node都需要在函数外面进行unpin */ IxNodeHandle *IxIndexHandle::split(IxNodeHandle *node) { IxNodeHandle *new_node = create_node(); new_node->set_parent_page_no(node->get_parent_page_no()); // 计算分裂位置 - 确保左右节点都有足够的键 int split_pos = node->get_min_size(); new_node->set_size(node->get_size() - split_pos); // 复制键值对到新结点 memcpy(new_node->get_key(0), node->get_key(split_pos), new_node->get_size() * file_hdr_->col_tot_len_); memcpy(new_node->get_rid(0), node->get_rid(split_pos), new_node->get_size() * sizeof(Rid)); node->set_size(split_pos); // 维护叶子节点双向链表 if (node->is_leaf_page()) { new_node->set_prev_leaf(node->get_page_no()); new_node->set_next_leaf(node->get_next_leaf()); node->set_next_leaf(new_node->get_page_no()); if (new_node->get_next_leaf() != IX_NO_PAGE) { IxNodeHandle *next_leaf = fetch_node(new_node->get_next_leaf()); next_leaf->set_prev_leaf(new_node->get_page_no()); buffer_pool_manager_->unpin_page(next_leaf->get_page_id(), true); delete next_leaf; } else { file_hdr_->last_leaf_ = new_node->get_page_no(); } // 更新第一个叶子指针(如果需要) if (file_hdr_->first_leaf_ == node->get_page_no()) { // 原节点仍然是第一个叶子,不需要更改 } } return new_node; } /** * @brief Insert key & value pair into internal page after split * 拆分(Split)后,向上找到old_node的父结点 * 将new_node的第一个key插入到父结点,其位置在 父结点指向old_node的孩子指针 之后 * 如果插入后>=maxsize,则必须继续拆分父结点,然后在其父结点的父结点再插入,即需要递归 * 直到找到的old_node为根结点时,结束递归(此时将会新建一个根R,关键字为key,old_node和new_node为其孩子) * * @param (old_node, new_node) 原结点为old_node,old_node被分裂之后产生了新的右兄弟结点new_node * @param key 要插入parent的key * @note 一个结点插入了键值对之后需要分裂,分裂后左半部分的键值对保留在原结点,在参数中称为old_node, * 右半部分的键值对分裂为新的右兄弟节点,在参数中称为new_node(参考Split函数来理解old_node和new_node) * @note 本函数执行完毕后,new node和old node都需要在函数外面进行unpin */ void IxIndexHandle::insert_into_parent(IxNodeHandle *old_node, const char *key, IxNodeHandle *new_node, Transaction *transaction) { if (old_node->is_root_page()) { // 创建新根节点 IxNodeHandle *new_root = create_node(); new_root->set_parent_page_no(IX_NO_PAGE); new_root->page_hdr->is_leaf = false; new_root->set_size(1); *new_root->get_rid(0) = Rid{old_node->get_page_no(), 0}; memcpy(new_root->get_key(0), new_node->get_key(0), file_hdr_->col_tot_len_); *new_root->get_rid(1) = Rid{new_node->get_page_no(), 0}; old_node->set_parent_page_no(new_root->get_page_no()); new_node->set_parent_page_no(new_root->get_page_no()); file_hdr_->root_page_ = new_root->get_page_no(); if (old_node->is_leaf_page()) { file_hdr_->first_leaf_ = old_node->get_page_no(); file_hdr_->last_leaf_ = new_node->get_page_no(); } buffer_pool_manager_->unpin_page(new_root->get_page_id(), true); delete new_root; } else { IxNodeHandle *parent = fetch_node(old_node->get_parent_page_no()); // 关键修复:使用传入的key而不是new_node->get_key(0) int insert_pos = parent->insert(key, Rid{new_node->get_page_no(), 0}); if (parent->get_size() >= parent->get_max_size()) { IxNodeHandle *new_parent = split(parent); // 关键修复:使用新父节点的第一个键 insert_into_parent(parent, new_parent->get_key(0), new_parent, transaction); buffer_pool_manager_->unpin_page(new_parent->get_page_id(), true); delete new_parent; } else { maintain_parent(parent); } buffer_pool_manager_->unpin_page(parent->get_page_id(), true); delete parent; } } /** * @brief 将指定键值对插入到B+树中 * @param (key, value) 要插入的键值对 * @param transaction 事务指针 * @return page_id_t 插入到的叶结点的page_no */ page_id_t IxIndexHandle::insert_entry(const char *key, const Rid &value, Transaction *transaction) { auto [leaf, root_is_latched] = find_leaf_page(key, Operation::INSERT, transaction); if (!leaf) { // 处理根节点不存在的情况 IxNodeHandle *new_leaf = create_node(); new_leaf->page_hdr->is_leaf = true; new_leaf->insert(key, value); file_hdr_->root_page_ = new_leaf->get_page_no(); file_hdr_->first_leaf_ = new_leaf->get_page_no(); file_hdr_->last_leaf_ = new_leaf->get_page_no(); if (root_is_latched) { root_latch_.unlock(); } page_id_t ret = new_leaf->get_page_no(); buffer_pool_manager_->unpin_page(new_leaf->get_page_id(), true); delete new_leaf; return ret; } // 检查节点是否已满 if (leaf->get_size() < leaf->get_max_size()) { // 节点未满,直接插入 leaf->insert(key, value); } else { // 节点已满,需要分裂 IxNodeHandle *new_leaf = split(leaf); // 确定键值应该插入哪个节点 if (ix_compare(key, new_leaf->get_key(0), file_hdr_->col_types_, file_hdr_->col_lens_) >= 0) { new_leaf->insert(key, value); } else { leaf->insert(key, value); } // 处理父节点 insert_into_parent(leaf, new_leaf->get_key(0), new_leaf, transaction); // 释放新节点资源 buffer_pool_manager_->unpin_page(new_leaf->get_page_id(), true); delete new_leaf; } // 确保最后释放资源 if (root_is_latched) { root_latch_.unlock(); } page_id_t ret = leaf->get_page_no(); buffer_pool_manager_->unpin_page(leaf->get_page_id(), true); delete leaf; return ret; } /** * @brief 用于删除B+树中含有指定key的键值对 * @param key 要删除的key值 * @param transaction 事务指针 */ bool IxIndexHandle::delete_entry(const char *key, Transaction *transaction) { auto [leaf, root_is_latched] = find_leaf_page(key, Operation::DELETE, transaction); if (!leaf->remove(key)) { if (root_is_latched) { root_latch_.unlock(); } buffer_pool_manager_->unpin_page(leaf->get_page_id(), false); return false; } // 直接使用返回值 if (coalesce_or_redistribute(leaf, transaction, &root_is_latched)) { // 处理合并或重分配结果 } if (root_is_latched) { root_latch_.unlock(); } buffer_pool_manager_->unpin_page(leaf->get_page_id(), true); return true; } /** * @brief 用于处理合并和重分配的逻辑,用于删除键值对后调用 * * @param node 执行完删除操作的结点 * @param transaction 事务指针 * @param root_is_latched 传出参数:根节点是否上锁,用于并发操作 * @return 是否需要删除结点 * @note User needs to first find the sibling of input page. * If sibling&#39;s size + input page&#39;s size >= 2 * page&#39;s minsize, then redistribute. * Otherwise, merge(Coalesce). */ bool IxIndexHandle::coalesce_or_redistribute(IxNodeHandle *node, Transaction *transaction, bool *root_is_latched) { if (node->is_root_page()) { return adjust_root(node); } if (node->get_size() >= node->get_min_size()) { return false; } IxNodeHandle *parent = fetch_node(node->get_parent_page_no()); int index = parent->find_child(node); IxNodeHandle *neighbor; bool is_prev = (index > 0); if (is_prev) { neighbor = fetch_node(parent->value_at(index - 1)); } else { neighbor = fetch_node(parent->value_at(index + 1)); } if (node->get_size() + neighbor->get_size() >= node->get_max_size()) { redistribute(neighbor, node, parent, index); buffer_pool_manager_->unpin_page(parent->get_page_id(), true); buffer_pool_manager_->unpin_page(neighbor->get_page_id(), true); return false; } else { bool parent_deleted = coalesce(&neighbor, &node, &parent, index, transaction, root_is_latched); buffer_pool_manager_->unpin_page(parent->get_page_id(), true); buffer_pool_manager_->unpin_page(neighbor->get_page_id(), true); return parent_deleted; } } /** * @brief 用于当根结点被删除了一个键值对之后的处理 * @param old_root_node 原根节点 * @return bool 根结点是否需要被删除 * @note size of root page can be less than min size and this method is only called within coalesce_or_redistribute() */ bool IxIndexHandle::adjust_root(IxNodeHandle *old_root_node) { if (old_root_node->get_size() > 1 || old_root_node->is_leaf_page()) { return false; } // 根节点只有一个孩子,让孩子成为新的根节点 page_id_t child_page_no = old_root_node->remove_and_return_only_child(); IxNodeHandle *child = fetch_node(child_page_no); child->set_parent_page_no(IX_NO_PAGE); file_hdr_->root_page_ = child_page_no; release_node_handle(*old_root_node); buffer_pool_manager_->unpin_page(child->get_page_id(), true); return true; } /** * @brief 重新分配node和兄弟结点neighbor_node的键值对 * Redistribute key & value pairs from one page to its sibling page. If index == 0, move sibling page&#39;s first key * & value pair into end of input "node", otherwise move sibling page&#39;s last key & value pair into head of input "node". * * @param neighbor_node sibling page of input "node" * @param node input from method coalesceOrRedistribute() * @param parent the parent of "node" and "neighbor_node" * @param index node在parent中的rid_idx * @note node是之前刚被删除过一个key的结点 * index=0,则neighbor是node后继结点,表示:node(left) neighbor(right) * index>0,则neighbor是node前驱结点,表示:neighbor(left) node(right) * 注意更新parent结点的相关kv对 */ void IxIndexHandle::redistribute(IxNodeHandle *neighbor_node, IxNodeHandle *node, IxNodeHandle *parent, int index) { if (index > 0) { // neighbor是前驱节点 if (!node->is_leaf_page()) { // 内部节点需要移动最后一个键值对 int neighbor_size = neighbor_node->get_size(); node->insert_pair(0, neighbor_node->get_key(neighbor_size - 1), *neighbor_node->get_rid(neighbor_size - 1)); // 解引用 neighbor_node->erase_pair(neighbor_size - 1); parent->set_key(index, node->get_key(0)); maintain_child(node, 0); } else { // 叶子节点需要移动最后一个键值对 int neighbor_size = neighbor_node->get_size(); node->insert_pair(0, neighbor_node->get_key(neighbor_size - 1), *neighbor_node->get_rid(neighbor_size - 1)); // 解引用 neighbor_node->erase_pair(neighbor_size - 1); parent->set_key(index, node->get_key(0)); } } else { // neighbor是后继节点 if (!node->is_leaf_page()) { // 内部节点需要移动第一个键值对 node->insert_pair(node->get_size(), neighbor_node->get_key(0), *neighbor_node->get_rid(0)); // 解引用 neighbor_node->erase_pair(0); parent->set_key(1, neighbor_node->get_key(0)); maintain_child(node, node->get_size() - 1); } else { // 叶子节点需要移动第一个键值对 node->insert_pair(node->get_size(), neighbor_node->get_key(0), *neighbor_node->get_rid(0)); // 解引用 neighbor_node->erase_pair(0); parent->set_key(1, neighbor_node->get_key(0)); } } } /** * @brief 合并(Coalesce)函数是将node和其直接前驱进行合并,也就是和它左边的neighbor_node进行合并; * 假设node一定在右边。如果上层传入的index=0,说明node在左边,那么交换node和neighbor_node,保证node在右边;合并到左结点,实际上就是删除了右结点; * Move all the key & value pairs from one page to its sibling page, and notify buffer pool manager to delete this page. * Parent page must be adjusted to take info of deletion into account. Remember to deal with coalesce or redistribute * recursively if necessary. * * @param neighbor_node sibling page of input "node" (neighbor_node是node的前结点) * @param node input from method coalesceOrRedistribute() (node结点是需要被删除的) * @param parent parent page of input "node" * @param index node在parent中的rid_idx * @return true means parent node should be deleted, false means no deletion happend * @note Assume that *neighbor_node is the left sibling of *node (neighbor -> node) */ bool IxIndexHandle::coalesce(IxNodeHandle **neighbor_node, IxNodeHandle **node, IxNodeHandle **parent, int index, Transaction *transaction, bool *root_is_latched) { if (index == 0) { // 交换node和neighbor_node,保证node在右边 std::swap(*neighbor_node, *node); index = 1; } // 将node合并到neighbor_node int neighbor_insert_pos = (*neighbor_node)->get_size(); (*neighbor_node)->insert_pairs(neighbor_insert_pos, (*node)->get_key(0), (*node)->get_rid(0), (*node)->get_size()); if (!(*node)->is_leaf_page()) { // 内部节点需要更新子节点的父指针 for (int i = 0; i < (*node)->get_size(); i++) { maintain_child(*neighbor_node, neighbor_insert_pos + i); } } else { // 叶子节点需要更新链表指针 (*neighbor_node)->set_next_leaf((*node)->get_next_leaf()); if ((*node)->get_next_leaf() != IX_NO_PAGE) { IxNodeHandle *next_leaf = fetch_node((*node)->get_next_leaf()); next_leaf->set_prev_leaf((*neighbor_node)->get_page_no()); buffer_pool_manager_->unpin_page(next_leaf->get_page_id(), true); } else { file_hdr_->last_leaf_ = (*neighbor_node)->get_page_no(); } } // 从父节点中删除node对应的键值对 (*parent)->remove((*node)->get_key(0)); release_node_handle(**node); // 递归处理父节点 return coalesce_or_redistribute(*parent, transaction, root_is_latched); } /** * @brief 这里把iid转换成了rid,即iid的slot_no作为node的rid_idx(key_idx) * node其实就是把slot_no作为键值对数组的下标 * 换而言之,每个iid对应的索引槽存了一对(key,rid),指向了(要建立索引的属性首地址,插入/删除记录的位置) * * @param iid * @return Rid * @note iid和rid存的不是一个东西,rid是上层传过来的记录位置,iid是索引内部生成的索引槽位置 */ Rid IxIndexHandle::get_rid(const Iid &iid) const { IxNodeHandle *node = fetch_node(iid.page_no); if (iid.slot_no >= node->get_size()) { throw IndexEntryNotFoundError(); } buffer_pool_manager_->unpin_page(node->get_page_id(), false); // unpin it! return *node->get_rid(iid.slot_no); } /** * @brief FindLeafPage + lower_bound * * @param key * @return Iid * @note 上层传入的key本来是int类型,通过(const char *)&key进行了转换 * 可用*(int *)key转换回去 */ Iid IxIndexHandle::lower_bound(const char *key) { return Iid{-1, -1}; } /** * @brief FindLeafPage + upper_bound * * @param key * @return Iid */ Iid IxIndexHandle::upper_bound(const char *key) { return Iid{-1, -1}; } /** * @brief 指向最后一个叶子的最后一个结点的后一个 * 用处在于可以作为IxScan的最后一个 * * @return Iid */ Iid IxIndexHandle::leaf_end() const { IxNodeHandle *node = fetch_node(file_hdr_->last_leaf_); Iid iid = {.page_no = file_hdr_->last_leaf_, .slot_no = node->get_size()}; buffer_pool_manager_->unpin_page(node->get_page_id(), false); // unpin it! return iid; } /** * @brief 指向第一个叶子的第一个结点 * 用处在于可以作为IxScan的第一个 * * @return Iid */ Iid IxIndexHandle::leaf_begin() const { Iid iid = {.page_no = file_hdr_->first_leaf_, .slot_no = 0}; return iid; } /** * @brief 获取一个指定结点 * * @param page_no * @return IxNodeHandle* * @note pin the page, remember to unpin it outside! */ IxNodeHandle *IxIndexHandle::fetch_node(int page_no) const { Page *page = buffer_pool_manager_->fetch_page(PageId{fd_, page_no}); IxNodeHandle *node = new IxNodeHandle(file_hdr_, page); return node; } /** * @brief 创建一个新结点 * * @return IxNodeHandle* * @note pin the page, remember to unpin it outside! * 注意:对于Index的处理是,删除某个页面后,认为该被删除的页面是free_page * 而first_free_page实际上就是最新被删除的页面,初始为IX_NO_PAGE * 在最开始插入时,一直是create node,那么first_page_no一直没变,一直是IX_NO_PAGE * Record的处理不同,Record将未插入满的记录页认为是free_page */ IxNodeHandle *IxIndexHandle::create_node() { IxNodeHandle *node; file_hdr_->num_pages_++; PageId new_page_id = {.fd = fd_, .page_no = INVALID_PAGE_ID}; // 从3开始分配page_no,第一次分配之后,new_page_id.page_no=3file_hdr_.num_pages=4 Page *page = buffer_pool_manager_->new_page(&new_page_id); node = new IxNodeHandle(file_hdr_, page); return node; } /** * @brief 从node开始更新其父节点的第一个key,一直向上更新直到根节点 * * @param node */ void IxIndexHandle::maintain_parent(IxNodeHandle *node) { IxNodeHandle *curr = node; while (curr->get_parent_page_no() != IX_NO_PAGE) { // Load its parent IxNodeHandle *parent = fetch_node(curr->get_parent_page_no()); int rank = parent->find_child(curr); char *parent_key = parent->get_key(rank); char *child_first_key = curr->get_key(0); if (memcmp(parent_key, child_first_key, file_hdr_->col_tot_len_) == 0) { assert(buffer_pool_manager_->unpin_page(parent->get_page_id(), true)); break; } memcpy(parent_key, child_first_key, file_hdr_->col_tot_len_); // 修改了parent node curr = parent; assert(buffer_pool_manager_->unpin_page(parent->get_page_id(), true)); } } /** * @brief 要删除leaf之前调用此函数,更新leaf前驱结点的next指针和后继结点的prev指针 * * @param leaf 要删除的leaf */ void IxIndexHandle::erase_leaf(IxNodeHandle *leaf) { assert(leaf->is_leaf_page()); IxNodeHandle *prev = fetch_node(leaf->get_prev_leaf()); prev->set_next_leaf(leaf->get_next_leaf()); buffer_pool_manager_->unpin_page(prev->get_page_id(), true); IxNodeHandle *next = fetch_node(leaf->get_next_leaf()); next->set_prev_leaf(leaf->get_prev_leaf()); // 注意此处是SetPrevLeaf() buffer_pool_manager_->unpin_page(next->get_page_id(), true); } /** * @brief 删除node时,更新file_hdr_.num_pages * * @param node */ void IxIndexHandle::release_node_handle(IxNodeHandle &node) { file_hdr_->num_pages_--; } /** * @brief 将node的第child_idx个孩子结点的父节点置为node */ void IxIndexHandle::maintain_child(IxNodeHandle *node, int child_idx) { if (!node->is_leaf_page()) { // Current node is inner node, load its child and set its parent to current node int child_page_no = node->value_at(child_idx); IxNodeHandle *child = fetch_node(child_page_no); child->set_parent_page_no(node->get_page_no()); buffer_pool_manager_->unpin_page(child->get_page_id(), true); } }
最新发布
06-05
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值