目录
实验准备
- 官方说明:https://15445.courses.cs.cmu.edu/fall2019/project1/
- 实验环境:已打包为 Docker 镜像
docker pull registry.cn-hangzhou.aliyuncs.com/yubincloud/cmu15445:lab1
由于课程原生放出的代码中的 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_
的 sizesize_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 的重要成员变量:
std::unordered_map<page_id_t, frame_id_t> page_table_
:记录已被换入到 manager 中的所有 pages,它是一个由 page_id → \to → frame_id 的 map,这些 page 有的还在被上层程序使用,有的已经被用完。Replacer replacer_
:上一个 task 实现的 replacer,当 buffer pool 空间不足时,用来决定将哪个已经不用了的 page 换出到 disk 中。std::list<frame_id_t> free_list_
:用来记录还有哪些 frame 没有被使用的 list,当需要将一个 page 从 disk 换入到 buffer pool 中时,可以从这里取出一个空闲的 frame 放入 page。Page *pages_
:这就是 frames,一个存放 buffer pool 所有 pages 的 Page 数组。- 当有一个 frame_id 时,
pages_ + frame_id
就是所要找的 page 的指针
- 当有一个 frame_id 时,
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 * {
...
}
实现逻辑:
- 首先在 page_table 中查找有没有这个 page,有的话,pin 一下然后立刻返回即可。
- pin 一下表示它的使用者 +1,类似于引用计数。
- 如果 page_table 中找不到,说明这个 page 还不在 buffer pool 中,需要从 disk 中将这个 page 读入到 buffer pool 中,而为了完成这一步,需要先找一个空闲的 frame,找空闲 frame 的方法如下:
- 看一下 free_list 中是否还有空闲的 frame
- 如果没有的话,使用 replacer 将一个没用的 page 换出到 disk 中,从而得到一个空闲的 frame
- 如果还找不到空闲 frame 的话,直接 return false 来表示 fetch 失败即可。
- 找到空闲 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 {
...
}
实现逻辑:
- 先从 page table 中查找这个 page,如果没有找到,直接返回 true 即可。
- 如果在 page table 中找到了这个 page,先看一下它的 pin count 是否为 0,如果不为 0,表示还有人在使用这个 page,暂时还不能删除,直接返回 false 表示删除失败。
- 如果可以删除这个 page,那么从 page table 中移除这个 page,并调用
disk_manager_.DeallocatePage()
来把这个 page 的空间从磁盘中释放掉。 - 删除掉后空闲的 frame 加入到 free_list 中。
2.6 FlushAllPages
将 buffer pool 中的所有 page 都 flush 到 disk 中,实现逻辑与 FlushPage 相同。接口:
void BufferPoolManager::FlushAllPagesImpl() {
...
}