【CMU15445】Fall 2019, Project 1: Buffer Pool 实验记录

实验准备

由于课程原生放出的代码中的 test case 不全,gradescope 上的测试更加严格、更加完整,可以去 https://github.com/zhiyiYo/CMU15445/ 上拷贝下来里面的 /test 目录。

实验测试

Task 1:

mkdir build
cd build
make clock_replacer_test
./test/clock_replacer_test

Task 2:

cd build
make buffer_pool_manager_test
./test/buffer_pool_manager_test

Task 1: CLOCK REPLACEMENT POLICY

这个 task 主要就是实现 ClockReplacer 类,它使用时钟替换策略来选择将 buffer pool 中的哪些无用的 page 换出到 disk 中。

头文件在 /src/include/clock_replacer.h 中,实现文件在 /src/buffer/clock_replacer.cpp 中,我们的代码写在实现文件中。

buffer pool 中 frame 的数量是固定的,每个 frame 可以存放一个 page,这些 page 中有当前数据库正在使用的,也有已经用完的,clock_replacer 负责决定将哪些已经不再使用的 page 换出到 disk 中。

buffer pool 的 frames 与 clock_replacer 的 frames 是对应的,即数量相同,即 replacer 成员变量 frames_ 的长度就是 buffer pool 的 frame 的数量。

ClockReplacer 主要有如下几个成员变量:

  • std::mutex mu_:即 latch,用于在实现内部做并发控制
  • std::vector<std::array<bool, 2>> frames_:所有的 frames 信息,每个 frame 里存放的 page 内容我们不需要关心,我们只需要关心哪些 frame 可以被换出以及换出哪一个。这里每个 frame 对应一个含有两个 bool 元素的数组:
    • 第一个 bool 元素:表示 replacer 是否拥有这个 frame,也就是这个 frame 是否可以被换出。只有当没人使用一个 frame 的时候,这个 frame 才被 replacer 所拥有,并能够决定将其换出到 disk 中。
    • 第二个 bool 元素:ref flag,用于时钟替换策略,replacer 会先换出首个被 clock_hand_ 指到且 ref flag 为 false 的 frame
  • const size_t num_pages_:表示 buffer pool 可以存放多少个 pages,也就是这里 frames_ 的 size
  • size_t clock_hand_:时钟替换策略的指针,当指针指向一个可以换出的元素时,如果它的 ref flag 为 false,则将其换出;如果 ref flag 为 true,则置为 false。

本 task 所需要实现的函数如下:

1.1 ClockReplacer 构造函数

初始化各成员变量的值,其中每个 frame 所对应的数组的两个 bool 元素均为 false,表示初始时 replacer 不拥有任何 frame。

代码如下:

ClockReplacer::ClockReplacer(size_t num_pages): frames_(num_pages), num_pages_(num_pages), clock_hand_(0) {
    for (auto& frame: frames_) {
        frame.fill(false);
    }
}

1.2 Victim

该函数利用时钟替换策略,将一个可以换出的 frame 中的 page 换出到磁盘中。如果找不到可以换出的 frame,则返回 false。

auto ClockReplacer::Victim(frame_id_t *frame_id) -> bool {
    mu_.lock();
    int size = 0;  // 计算当前 replacer 拥有多少 frame
    for (auto& frame: frames_) {
        if (frame[0]) {
            size++;
        }
    }
    if (size <= 0) {
        mu_.unlock();
        return false;
    }

    bool found = false;
    while (true) {
        if (clock_hand_ >= num_pages_) {  // clock_hand 超过 frames 的长度,则置 0,模拟时钟的重新循环
            clock_hand_ = 0;
            continue;
        }
        auto &frame = frames_[clock_hand_];
        if (!frame[0]) {  // 如果该 frame 不存在,指向下一个
            clock_hand_++;
            continue;
        }
        if (frame[1]) {  // 如果 ref flag 为 true,则将其置为 false 并继续下一个
            frame[1] = false;
            clock_hand_++;
            continue;
        }  
        found = true;
        *frame_id = static_cast<int>(clock_hand_);
        frame[0] = false;  // 找到了之后,clock_hand_ 不需要再 +1,下次还是从这个地方开始扫描,因为 test case 就是这么写的
        break;
    }
    mu_.unlock();
    return found;
}

1.3 Pin

表示上层数据库程序需要使用某个 frame,因此将其 pin 住,这样 replacer 不再拥有这个 frame,这个 frame 也不能被换出。代码实现:

void ClockReplacer::Pin(frame_id_t frame_id) {
    mu_.lock();
    auto &frame = frames_[frame_id];
    frame[0] = false;
    mu_.unlock();
}

1.4 Unpin

当上层程序没人使用某个 frame 时,也就是它的 pin_count 变为 0 时,Unpin() 方法才会被调用,这样 replacer 再次拥有这个 frame,并可以决定将其换出到 disk 中。

void ClockReplacer::Unpin(frame_id_t frame_id) {
    mu_.lock();
    auto& frame = frames_[frame_id];
    frame[0] = true;
    frame[1] = true;
    mu_.unlock();
}

1.5 Size

计算 replacer 目前拥有多少个 frames,也就是找多少个 frame 的第一个 bool 元素为 true:

auto ClockReplacer::Size() -> size_t {
    size_t size = 0;
    mu_.lock();
    for (auto& frame: frames_) {
        if (frame[0]) {
            size++;
        }
    }
    mu_.unlock();
    return size;
}

Task 2: BUFFER POOL MANAGER

这个 task 主要就是实现 BufferPoolManager 这个类,它负责管理 buffer pool,上层的程序通过这个类来从 disk 中取出 page 放入 buffer pool,并在使用完一个 page 后告诉它,由它来完成 page 的换入换出。

BufferPoolManager(后面简称为 manager) 主要管理了固定数量的 frames,每个 frame 有一个 frame_id,值为从 0~N。每个 frame 中可以存放一个 page,因此 manager 中的每个 page 有两种 id:

  • page_id:page 在磁盘中的唯一 id,无论在哪,都可以唯一代表这个 page
  • frame_id:manager 用于存放这个 page 的 frame 的 id,通过这个 frame_id,可以在 manager 中找到这个 page

头文件位于 /src/include/buffer/buffer_pool_manager.h 中,实现文件位于 /src/buffer/buffer_pool_manager.cpp 中。

manager 的重要成员变量:

  1. std::unordered_map<page_id_t, frame_id_t> page_table_:记录已被换入到 manager 中的所有 pages,它是一个由 page_id → \to frame_id 的 map,这些 page 有的还在被上层程序使用,有的已经被用完。
  2. Replacer replacer_:上一个 task 实现的 replacer,当 buffer pool 空间不足时,用来决定将哪个已经不用了的 page 换出到 disk 中。
  3. std::list<frame_id_t> free_list_:用来记录还有哪些 frame 没有被使用的 list,当需要将一个 page 从 disk 换入到 buffer pool 中时,可以从这里取出一个空闲的 frame 放入 page。
  4. Page *pages_:这就是 frames,一个存放 buffer pool 所有 pages 的 Page 数组。
    • 当有一个 frame_id 时,pages_ + frame_id 就是所要找的 page 的指针
  5. DiskManager *disk_manager_:用于申请磁盘空间、根据 page_id 读取这个 page 的数据、将内存的数据写入到 page_id 的磁盘空间中等。

本 task 所需要实现的接口如下:

2.1 FetchPage

当上层程序需要 page_id 的 page 时,便会调用这个接口来获得此 page 的内存指针:

auto BufferPoolManager::FetchPageImpl(page_id_t page_id) -> Page * {
    ...
}

实现逻辑:

  1. 首先在 page_table 中查找有没有这个 page,有的话,pin 一下然后立刻返回即可。
    • pin 一下表示它的使用者 +1,类似于引用计数。
  2. 如果 page_table 中找不到,说明这个 page 还不在 buffer pool 中,需要从 disk 中将这个 page 读入到 buffer pool 中,而为了完成这一步,需要先找一个空闲的 frame,找空闲 frame 的方法如下:
    • 看一下 free_list 中是否还有空闲的 frame
    • 如果没有的话,使用 replacer 将一个没用的 page 换出到 disk 中,从而得到一个空闲的 frame
    • 如果还找不到空闲 frame 的话,直接 return false 来表示 fetch 失败即可。
  3. 找到空闲 frame 后,使用 disk_manager 将 page_id 的 page 读入到 buffer pool 的 frame 中,pin 一下后返回这个内存指针。

注意将 buffer pool 中的 page 换出到磁盘中的时候,需要检查一下这个 page 是否 dirty,如果 dirty 的话,代表使用期间有人修改了这个 page,因此需要将数据覆写到 disk 中。

2.2 UnpinPage

上层程序使用完一个 page 后,会调用这个方法对一个 page_id 进行 unpin 一次,manager 会负责记录每个 page 的 pin_count,每 pin 一次,pin_count 会加 1,每 unpin 一次,pin_count 会减 1,当 pin_count 大于 0 时,replacer 不允许将其换出到 disk 中,而 pin_count == 0 时,则将该 page 交给 replacer 来管理,由其决定在合适的时刻换出到 disk 中,从而腾出 buffer pool 的空间。

接口如下:

auto BufferPoolManager::UnpinPageImpl(page_id_t page_id, bool is_dirty) -> bool {
    ...
}

实现逻辑就是找到 page_id 对应的 frame,然后将它的 pin_count 减 1,并修改它的 is_dirty 状态。如果 pin_count 减到 0,则将其交给 replacer 来管理(replacer_->Unpin(frame_id))。

⚠️ 注意修改 frame 的 is_dirty 状态时,并不是直接将函数的 is_dirty 参数赋值给 frame->is_dirty 就可以,因为如果原本这个 frame 就是 dirty 的,即便这次 unpin 传入的 is_dirty 是 false,那这个 frame 也仍还是 dirty 的。

2.3 FlushPage

这个函数用来将 buffer pool 内存中的 page 同步到 disk 中,较为简单。接口:

auto BufferPoolManager::FlushPageImpl(page_id_t page_id) -> bool {
  ...
}

2.4 NewPage

在 disk 中创建一个新的 page,并将其放入 buffer pool 中,返回其内存指针。

实现逻辑可以参考 FetchPage,也是需要先找打一个空闲的 frame,然后调用 disk_manager_.AllocatePage() 来分配一个新的 page 空间,并将其加载到 buffer pool 的 frame 中。

接口:

auto BufferPoolManager::NewPageImpl(page_id_t *page_id) -> Page * {
   ...
}

新的 page 内存指针通过指针参数来传递出去。

2.5 DeletePage

从 buffer pool 和 disk 中都删除掉一个 page。接口:

auto BufferPoolManager::DeletePageImpl(page_id_t page_id) -> bool {
   ...
}

实现逻辑:

  1. 先从 page table 中查找这个 page,如果没有找到,直接返回 true 即可。
  2. 如果在 page table 中找到了这个 page,先看一下它的 pin count 是否为 0,如果不为 0,表示还有人在使用这个 page,暂时还不能删除,直接返回 false 表示删除失败。
  3. 如果可以删除这个 page,那么从 page table 中移除这个 page,并调用 disk_manager_.DeallocatePage() 来把这个 page 的空间从磁盘中释放掉。
  4. 删除掉后空闲的 frame 加入到 free_list 中。

2.6 FlushAllPages

将 buffer pool 中的所有 page 都 flush 到 disk 中,实现逻辑与 FlushPage 相同。接口:

void BufferPoolManager::FlushAllPagesImpl() {
   ...
}

实验运行结果

在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CMU 15445 课程的 Project 0 是一个调试练习,旨在帮助学生熟悉调试工具和技术。在这个项目中,你将开始使用 GDB 和 Valgrind 这两个常用的调试工具,以及一些其他辅助工具。以下是一些问题和步骤,帮助你完成这个练习: 1. 你需要查看项目中提供的代码,并了解它的结构和功能,这样你才能更好地理解程序的逻辑和可能出现的 bug。 2. 接下来,你需要编译项目,并确保没有编译错误。如果出现错误,你需要修复它们,这可能需要检查一些语法错误或缺失的库。 3. 一旦成功编译项目,你就可以使用 GDB 进行调试了。GDB 是一个强大的调试器,可以帮助你找出程序中的错误。你可以使用 GDB 来单步执行代码、设置断点、查看变量的值等等。通过使用 GDB,你可以逐步查看代码运行的路径,并找出程序崩溃或产生错误的原因。 4. 在使用 GDB 进行调试时,你可以通过设置断点来暂停程序的执行,并查看变量的值和程序的状态。你可以使用“break”命令在程序中设置断点,并通过“run”命令启动程序。当程序到达这个断点时,它会停止执行,你可以使用“print”命令查看变量的值,或者“step”命令逐步执行代码。 5. 另一个常用的调试工具是 Valgrind。Valgrind 可以帮助你检测内存泄漏和错误的访问方式。你可以使用“valgrind”命令来运行程序,并查看 Valgrind 的输出。它会告诉你有关程序中任何潜在问题的信息,例如未初始化的变量、访问越界等。 6. 最后,当你发现 bug 并修复它们后,可以运行各种测试用例来验证程序的正确性。测试用例可以帮助你确定程序是否按预期工作,并且在修改代码后,它们可以帮助你确保你的修复没有引入新的错误。 通过完成 CMU 15445 项目 0 的调试练习,你将掌握一些重要的调试技巧和工具,这对于进一步开发和调试软件应用程序将非常有用。希望上述步骤和建议对你有所帮助,祝你顺利完成这个项目!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值