CMU15445 (Fall 2019) 之 Project#1 - Buffer Pool 详解
前言
这个实验有两个任务:时钟替换算法和缓冲池管理器,分别对应 ClockReplacer 和 BufferPoolManager 类,BufferPoolManager 会用 ClockReplacer 挑选被换出的页,并通过 DiskManager 将换出的页写到数据库文件中。下面介绍这两个类的实现过程。
代码实现
如果直接克隆 Bustub 仓库,得到的是 fall 2021 的实验代码,对于 fall 2019,可以将 commit 切换至 5972018: Fix typo in type.cpp(#66)。但是这样引入一个坑,就是需要将 build_support/gtest_CMakeLists.txt.in 的内容改为:
cmake_minimum_required(VERSION 3.8)
project(googletest-download NONE)
include(ExternalProject)
ExternalProject_Add(googletest
GIT_REPOSITORY git@github.com:google/googletest.git
GIT_TAG main
SOURCE_DIR "${CMAKE_BINARY_DIR}/googletest-src"
BINARY_DIR "${CMAKE_BINARY_DIR}/googletest-build"
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
TEST_COMMAND ""
)
这里主要修改了 GIT_TAG 为 main,因为 googletest 仓库似乎将 master 分支重命名为 main 了。
ClockReplacer 类
项目主页对该类的实现方式做出了一点介绍:
The size of the
ClockReplaceris the same as buffer pool since it contains placeholders for all of the frames in theBufferPoolManager. However, not all the frames are considered as in theClockReplacer. TheClockReplaceris initialized to have no frame in it. Then, only the newly unpinned ones will be considered in theClockReplacer. Adding a frame to or removing a frame from a replacer is implemented by changing a reference bit of a frame. The clock hand initially points to the placeholder of frame 0. For each frame, you need to track two things: 1. Is this frame currently in theClockReplacer? 2. Has this frame recently been unpinned (ref flag)?In some scenarios, the two are the same. For example, when you unpin a page, both of the above are true. However, the frame stays in the
ClockReplaceruntil it is pinned or victimized, but its ref flag is modified by the clock hand.
简单翻译一下,就是 ClockReplacer 类内部维护了一个 frame 的集合,集合大小和缓冲池的大小一致。由于缓冲池中的某些 frame 正在被别的线程访问,这些 frame 的 pin count (等于访问该帧的线程数量)会大于 0,此时这些 frame 不允许被换出,换个角度来说,就是这些 frame 不在 ClockReplacer 维护的集合中。对于可以被换出的 frame,它必须满足两个条件:
pin count为 0,即该帧在ClockReplacer中。一旦某个帧的pin count大于零,就要被移出ClockReplacer(调用ClockReplacer::Pin)reference bit为false,即该帧最近没被访问过。对于pin count刚变成 0 而被加入ClockReplacer的帧而言,由于它刚被访问过,所以其reference bit为true(调用ClockReplacer::Unpin)
至于时钟替换算法的过程,其实就是按顺序从 frame 集合中挑选出一个满足上述换出条件的过程。为了维护时钟指针的位置并保证线程安全,需要添加一个时钟指针成员 clock_hand_ 和一个读写锁 mutex_,帧集合 frames_ 的每个元素代表该帧是否在 ClockReplacer 中及其 reference bit:
/**
* ClockReplacer implements the clock replacement policy, which approximates the Least Recently Used policy.
*/
class ClockReplacer : public Replacer {
public:
/**
* Create a new ClockReplacer.
* @param num_pages the maximum number of pages the ClockReplacer will be required to store
*/
explicit ClockReplacer(size_t num_pages);
/**
* Destroys the ClockReplacer.
*/
~ClockReplacer() override;
bool Victim(frame_id_t *frame_id) override;
void Pin(frame_id_t frame_id) override;
void Unpin(frame_id_t frame_id) override;
size_t Size() override;
private:
frame_id_t clock_hand_ = 0;
std::vector<std::tuple<bool, bool>> frames_;
std::shared_mutex mutex_;
};
各个方法的定义如下,里面使用了 std::lock_guard 以保证代码是异常安全的:
ClockReplacer::ClockReplacer(size_t num_pages) {
for (size_t i = 0; i < num_pages; ++i) {
frames_.push_back(std::make_tuple(false, false));
}
}
ClockReplacer::~ClockReplacer() = default;
bool ClockReplacer::Victim(frame_id_t *frame_id) {
if (Size() == 0) {
return false;
}
std::lock_guard<std::shared_mutex> lock(mutex_);
while (true) {
auto &[contains, ref] = frames_[clock_hand_];
if (contains) {
if (ref) {
ref = false;
} else {
*frame_id = clock_hand_;
contains = false;
return true;
}
}
clock_hand_ = (clock_hand_ + 1) % frames_.size();
}
}
void ClockReplacer::Pin(frame_id_t frame_id) {
assert(static_cast<size_t>(frame_id) < frames_.size());
std::lock_guard<std::shared_mutex> lock(mutex_);
auto &[contains, ref] = frames_[frame_id];
contains = false;
ref = false;
}
void ClockReplacer::Unpin(frame_id_t frame_id) {
assert(static_cast<size_t>(frame_id) < frames_.size());
std::lock_guard<std::shared_mutex> lock(mutex_);
auto &[contains, ref] = frames_[frame_id];
contains = true;
ref = true;
}
size_t ClockReplacer::Size() {
std::shared_lock<std::shared_mutex> lock(mutex_);
size_t size = 0;
for (auto &[contains, ref] : frames_) {
size += contains;
}
return size;
}
在终端输入命令:
mkdir build
cd build
cmake ..
make clock_replacer_test
./test/clock_replacer_test
测试结果如下:

BufferPoolManager 类
这里将互斥锁换成了读写锁,用于保护 page_table_、pages_ 和 free_list_,同时引入了一个辅助函数 GetVictimFrameId():
class BufferPoolManager {
// 省略部分代码
protected:
/**
* select a victim frame from the free list or replacer.
* @return the frame id, INVALID_PAGE_ID if the victim could not be found
*/
frame_id_t GetVictimFrameId();
/** This latch protects shared data structures. We recommend updating this comment to describe what it protects. */
std::shared_mutex latch_;
};
BufferPoolManager 类要求我们实现五个函数:
FetchPageImpl(page_id)NewPageImpl(page_id)UnpinPageImpl(page_id, is_dirty)FlushPageImpl(page_id)DeletePageImpl(page_id)FlushAllPagesImpl()
下面会一个个实现上述函数。
FetchPageImpl(page_id)
该函数实现了缓冲池的主要功能:向上层提供指定的 page。缓冲池管理器首先在 page_table_ 中查找 page_id 键是否存在:
- 如果存在就根据
page_id对应的frame_id从缓冲池pages_取出page - 如果不存在就通过
GetVictimFrameId()函数选择被换出的帧,该函数首先从free_list_中查找缓冲池的空位,如果没找到空位就得靠上一节实现的ClockReplacer选出被换出的冤大头
具体代码如下:
Page *BufferPoolManager::FetchPageImpl(page_id_t page_id) {
// 1. Search the page table for the requested page (P).
std::lock_guard<std::shared_mutex> lock(latch_);
Page *page;
// 1.1 If P exists, pin it and return it immediately.
auto it = page_table_.find(page_id);
if (it != page_table_.end()) {
page = &pages_[it->second];
if (page->pin_count_++ == 0) {
replacer_->Pin(it->second);
}
return page;
}
// 1.2 If P does not exist, find a replacement page (R) from either the free list or the replacer.
// Note that pages are always found from the free list first.
frame_id_t frame_id = GetVictimFrameId();
if (frame_id == INVALID_PAGE_ID) {
return nullptr;
}
// 2. If R is dirty, write it back to the disk.
page = &pages_[frame_id];
if (page->IsDirty()) {
disk_manager_->WritePage(page->page_id_, page->data_);
}
// 3. Delete R from the page table and insert P.
page_table_.erase(page->GetPageId());
page_table_[page_id] = frame_id;
// 4. Update P's metadata, read in the page content from disk, and then return a pointer to P.
disk_manager_->ReadPage(page_id, page->data_);
page->update(page_id, 1, false);
replacer_->Pin(frame_id);
return page;
}
frame_id_t BufferPoolManager::GetVictimFrameId() {
frame_id_t frame_id;
if (!free_list_.empty()) {
frame_id = free_list_.front();
free_list_.pop_front();
} else {
if (!replacer_->Victim(&frame_id)) {
return INVALID_PAGE_ID;
}
}
return frame_id;
}
上述代码中还用了一个 Page::update 辅助函数,用于更新 page 的元数据:
/**
* update the meta data of page
* @param page_id the page id
* @param pin_count the pin count
* @param is_dirty is page dirty
* @param reset_memory whether to reset the memory of page
*/
void update(page_id_t page_id, int pin_count, bool is_dirty, bool reset_memory = false) {
page_id_ = page_id;
pin_count_ = pin_count;
is_dirty_ = is_dirty;
if (reset_memory) {
ResetMemory();
}
}
NewPageImpl(page_id)
该函数在缓冲池中插入一个新页,如果缓冲池中的所有页面都正在被线程访问,插入失败,否则靠 GetVictimFrameId() 计算插入位置:
Page *BufferPoolManager::NewPageImpl(page_id_t *page_id) {
// 0. Make sure you call DiskManager::AllocatePage!
std::lock_guard<std::shared_mutex> lock(latch_);
// 1. If all the pages in the buffer pool are pinned, return nullptr.
if (free_list_.empty() && replacer_->Size() == 0) {
*page_id = INVALID_PAGE_ID;
return nullptr;
}
// 2. Pick a victim page P from either the free list or the replacer. Always pick from the free list first.
frame_id_t frame_id = GetVictimFrameId();
if (frame_id == INVALID_PAGE_ID) {
*page_id = INVALID_PAGE_ID;
return nullptr;
}
// 3. Update P's metadata, zero out memory and add P to the page table.
Page *page = &pages_[frame_id];
if (page->IsDirty()) {
disk_manager_->WritePage(page->page_id_, page->data_);
}
*page_id = disk_manager_->AllocatePage();
page_table_.erase(page->GetPageId());
page_table_[*page_id] = frame_id;
// 需要把 dirty bit 设置为 false 才能通过 IsDirty 测试用例
page->update(*page_id, 1, true, true);
// 4. Set the page ID output parameter. Return a pointer to P.
return page;
}
DeletePageImpl(page_id)
该函数从缓冲池和数据库文件中删除一个 page,并将其 page_id 设置为 INVALID_PAGE_ID:
bool BufferPoolManager::DeletePageImpl(page_id_t page_id) {
// 0. Make sure you call DiskManager::DeallocatePage!
std::lock_guard<std::shared_mutex> lock(latch_);
// 1. search the page table for the requested page (P).
// If P does not exist, return true.
auto it = page_table_.find(page_id);
if (it == page_table_.end()) {
return true;
}
// 2. If P exists, but has a non-zero pin-count, return false. Someone is using the page.
Page &page = pages_[it->second];
if (page.pin_count_ > 0) {
return false;
}
// 3. Otherwise, P can be deleted. Remove P from the page table, reset its metadata and return it to the free list.
disk_manager_->DeallocatePage(page_id);
page_table_.erase(page_id);
page.update(INVALID_PAGE_ID, 0, false, true);
free_list_.push_back(it->second);
return true;
}
UnpinPageImpl(page_id, is_dirty)
该函数用以减少对某个页的引用数 pin count,当 pin_count 为 0 时需要将其添加到 ClockReplacer 中:
bool BufferPoolManager::UnpinPageImpl(page_id_t page_id, bool is_dirty) {
std::lock_guard<std::shared_mutex> lock(latch_);
auto it = page_table_.find(page_id);
if (it == page_table_.end()) {
return false;
}
Page &page = pages_[it->second];
if (page.pin_count_ <= 0) {
return false;
}
// add page to replacer when the pin count is 0
page.is_dirty_ |= is_dirty;
if (--page.pin_count_ == 0) {
replacer_->Unpin(it->second);
}
return true;
}
FlushPageImpl(page_id)
如果缓冲池的 page 被修改过,需要将其写入磁盘以保持同步:
bool BufferPoolManager::FlushPageImpl(page_id_t page_id) {
// Make sure you call DiskManager::WritePage!
std::shared_lock<std::shared_mutex> lock(latch_);
auto it = page_table_.find(page_id);
if (it == page_table_.end()) {
return false;
}
// write page to disk if it's dirty
Page &page = pages_[it->second];
if (page.IsDirty()) {
disk_manager_->WritePage(page_id, pages_[it->second].data_);
page.is_dirty_ = false;
}
return true;
}
FlushAllPagesImpl()
该函数将缓冲池中的所有 page 写入磁盘:
void BufferPoolManager::FlushAllPagesImpl() {
// You can do it!
std::lock_guard<std::shared_mutex> lock(latch_);
for (size_t i = 0; i < pool_size_; ++i) {
Page &page = pages_[i];
if (page.page_id_ != INVALID_PAGE_ID && page.IsDirty()) {
disk_manager_->WritePage(i, page.data_);
page.is_dirty_ = false;
}
}
}
测试
在终端输入指令:
cd build
make buffer_pool_manager_test
./test/buffer_pool_manager_test
测试结果如下:

总结
该实验考察了学生对并发和 STL 的掌握程度,由于注释中列出了实现步骤(最搞的是 You can do it! 注释),所以代码写起来也比较顺畅,以上~~
CMU15445 (Fall 2019) 之 Project#1 - Buffer Pool 详解的更多相关文章
- CMU15445 (Fall 2019) 之 Project#4 - Logging & Recovery 详解
前言 这是 Fall 2019 的最后一个实验,要求我们实现预写式日志.系统恢复和存档点功能,这三个功能分别对应三个类 LogManager.LogRecovery 和 CheckpointManag ...
- CMU15445 (Fall 2019) 之 Project#2 - Hash Table 详解
前言 该实验要求实现一个基于线性探测法的哈希表,但是与直接放在内存中的哈希表不同的是,该实验假设哈希表非常大,无法整个放入内存中,因此需要将哈希表进行分割,将多个键值对放在一个 Page 中,然后搭配 ...
- CMU15445 (Fall 2019) 之 Project#3 - Query Execution 详解
前言 经过前面两个实验的铺垫,终于到了给数据库系统添加执行查询计划功能的时候了.给定一条 SQL 语句,我们可以将其中的操作符组织为一棵树,树中的每一个父节点都能从子节点获取 tuple 并处理成操作 ...
- Protocol Buffer技术详解(语言规范)
Protocol Buffer技术详解(语言规范) 该系列Blog的内容主体主要源自于Protocol Buffer的官方文档,而代码示例则抽取于当前正在开发的一个公司内部项目的Demo.这样做的目的 ...
- Protocol Buffer技术详解(数据编码)
Protocol Buffer技术详解(数据编码) 之前已经发了三篇有关Protocol Buffer的技术博客,其中第一篇介绍了Protocol Buffer的语言规范,而后两篇则分别基于C++和J ...
- Protocol Buffer技术详解(Java实例)
Protocol Buffer技术详解(Java实例) 该篇Blog和上一篇(C++实例)基本相同,只是面向于我们团队中的Java工程师,毕竟我们项目的前端部分是基于Android开发的,而且我们研发 ...
- Protocol Buffer技术详解(C++实例)
Protocol Buffer技术详解(C++实例) 这篇Blog仍然是以Google的官方文档为主线,代码实例则完全取自于我们正在开发的一个Demo项目,通过前一段时间的尝试,感觉这种结合的方式比较 ...
- CMU-15445 LAB1:Extendible Hash Table, LRU, BUFFER POOL MANAGER
概述 最近又开了一个新坑,CMU的15445,这是一门介绍数据库的课程.我follow的是2018年的课程,因为2018年官方停止了对外开放实验源码,所以我用的2017年的实验,但是问题不大,内容基本 ...
- iOS学习——iOS项目Project 和 Targets配置详解
最近开始学习完整iOS项目的开发流程和思路,在实际的项目开发过程中,我们通常需要对项目代码和资料进行版本控制和管理,一般比较常用的SVN或者Github进行代码版本控制和项目管理.我们iOS项目的开发 ...
随机推荐
- Java语言学习day35--8月10日
今日内容介绍1.集合2.Iterator迭代器3.增强for循环4.泛型 ###01集合使用的回顾 *A:集合使用的回顾 *a.ArrayList集合存储5个int类型元素 public static ...
- 踹掉后端,前端导出Excel!
前言 导出Excel文件这个功能,通常都是在后端实现返回前端一个下载链接,但有时候我们只想导出前端页面上已经有了的数据,不想再调后端导出接口浪费服务器资源,学习本文demo例子,我们踹掉后端,直接在前 ...
- Maven中央仓库地址大全
1.默认的Maven中央仓库 在Maven安装目录下,找到:/lib/maven-model-builder-${version}.jar 打开该文件,能找到超级POM:\org\apache\ma ...
- jsp第三次作业
1.在jsp页面中使用include动态标记加载音频 <%@ page language="java" import="java.util.*" page ...
- mmdetection 批量执行测试脚本
在终端执行该脚本,传入所有的测试路径,每一个model的结果文件夹里面有一个best文件夹存放着其训练时最高mAP对应的权重,名字为best.pth dir=$(ls -l $1 |awk '/^d/ ...
- VSCode 前端常用插件集合
Visual Studio Code 是由微软开发的一款免费.跨平台的文本编辑器.由于其卓越的性能和丰富的功能,它很快就受到了大家的喜爱. 但工欲善其事必先利其器,以下是本人为前端开发收集的常用的vs ...
- javaScript深拷贝和浅拷贝简单梳理
在了解深拷贝和浅拷贝之前,我们先梳理一下: JavaScript中,分为基本数据类型(原始值)和复杂类型(对象),同时它们各自的数据类型细分下又有好几种数据类型 基本数据类型 数字Number 字符串 ...
- Java面试题资料合集
Java面试题资料合集 2021年最新版--Java+最常见的+200++面试题汇总+答案总结汇总.pdf BIO,NIO,AIO,Netty面试题 35道.pdf Dubbo面试题 47道.pdf ...
- MySQL事务基本使用
目录 1 事务概述 1.1 事务的支持情况 1.2 事务的特性 1.3 事务的状态 2 使用事务 2.1 显示事务 2.2 隐藏事务 3 事务隔离等级 3.1 数据并发问题 3.2 事务隔离等级介绍 ...
- vue - vue基础/vue核心内容
终于算是顺利进入vue了,确实也只有学了过后才知道,之前三过vue而不学,确实是对的,现在进来了一点都不后悔,ajax.node.js.promise.webpack.git等等确实是除了三大基础外还 ...