前言

这个实验有两个任务:时钟替换算法和缓冲池管理器,分别对应 ClockReplacerBufferPoolManager 类,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_TAGmain,因为 googletest 仓库似乎将 master 分支重命名为 main 了。

ClockReplacer 类

项目主页对该类的实现方式做出了一点介绍:

The size of the ClockReplacer is the same as buffer pool since it contains placeholders for all of the frames in the BufferPoolManager. However, not all the frames are considered as in the ClockReplacer. The ClockReplacer is initialized to have no frame in it. Then, only the newly unpinned ones will be considered in the ClockReplacer. 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 the ClockReplacer? 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 ClockReplacer until it is pinned or victimized, but its ref flag is modified by the clock hand.

简单翻译一下,就是 ClockReplacer 类内部维护了一个 frame 的集合,集合大小和缓冲池的大小一致。由于缓冲池中的某些 frame 正在被别的线程访问,这些 framepin count (等于访问该帧的线程数量)会大于 0,此时这些 frame 不允许被换出,换个角度来说,就是这些 frame 不在 ClockReplacer 维护的集合中。对于可以被换出的 frame,它必须满足两个条件:

  • pin count 为 0,即该帧在 ClockReplacer 中。一旦某个帧的 pin count 大于零,就要被移出 ClockReplacer(调用 ClockReplacer::Pin
  • reference bitfalse,即该帧最近没被访问过。对于 pin count 刚变成 0 而被加入 ClockReplacer 的帧而言,由于它刚被访问过,所以其 reference bittrue(调用 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 详解的更多相关文章

  1. CMU15445 (Fall 2019) 之 Project#4 - Logging & Recovery 详解

    前言 这是 Fall 2019 的最后一个实验,要求我们实现预写式日志.系统恢复和存档点功能,这三个功能分别对应三个类 LogManager.LogRecovery 和 CheckpointManag ...

  2. CMU15445 (Fall 2019) 之 Project#2 - Hash Table 详解

    前言 该实验要求实现一个基于线性探测法的哈希表,但是与直接放在内存中的哈希表不同的是,该实验假设哈希表非常大,无法整个放入内存中,因此需要将哈希表进行分割,将多个键值对放在一个 Page 中,然后搭配 ...

  3. CMU15445 (Fall 2019) 之 Project#3 - Query Execution 详解

    前言 经过前面两个实验的铺垫,终于到了给数据库系统添加执行查询计划功能的时候了.给定一条 SQL 语句,我们可以将其中的操作符组织为一棵树,树中的每一个父节点都能从子节点获取 tuple 并处理成操作 ...

  4. Protocol Buffer技术详解(语言规范)

    Protocol Buffer技术详解(语言规范) 该系列Blog的内容主体主要源自于Protocol Buffer的官方文档,而代码示例则抽取于当前正在开发的一个公司内部项目的Demo.这样做的目的 ...

  5. Protocol Buffer技术详解(数据编码)

    Protocol Buffer技术详解(数据编码) 之前已经发了三篇有关Protocol Buffer的技术博客,其中第一篇介绍了Protocol Buffer的语言规范,而后两篇则分别基于C++和J ...

  6. Protocol Buffer技术详解(Java实例)

    Protocol Buffer技术详解(Java实例) 该篇Blog和上一篇(C++实例)基本相同,只是面向于我们团队中的Java工程师,毕竟我们项目的前端部分是基于Android开发的,而且我们研发 ...

  7. Protocol Buffer技术详解(C++实例)

    Protocol Buffer技术详解(C++实例) 这篇Blog仍然是以Google的官方文档为主线,代码实例则完全取自于我们正在开发的一个Demo项目,通过前一段时间的尝试,感觉这种结合的方式比较 ...

  8. CMU-15445 LAB1:Extendible Hash Table, LRU, BUFFER POOL MANAGER

    概述 最近又开了一个新坑,CMU的15445,这是一门介绍数据库的课程.我follow的是2018年的课程,因为2018年官方停止了对外开放实验源码,所以我用的2017年的实验,但是问题不大,内容基本 ...

  9. iOS学习——iOS项目Project 和 Targets配置详解

    最近开始学习完整iOS项目的开发流程和思路,在实际的项目开发过程中,我们通常需要对项目代码和资料进行版本控制和管理,一般比较常用的SVN或者Github进行代码版本控制和项目管理.我们iOS项目的开发 ...

随机推荐

  1. Em 和 Rem 的基本使用

    1. Em html 结构 <div id="box-1"> <h3>Box One</h3> <p> Lorem ipsum do ...

  2. SpringBoot 三层架构 Controller、Service、Dao作用和关系详解

    首先创建一个springboot项目. model层 model层也叫pojo层或者entity层,个人比较喜欢pojo层. 一般数据库的一张表对应一个pojo层,并且表中所有字段都在pojo层都一一 ...

  3. python学习-Day33

    目录 今日内容详细 socket socket套接字简介 socket模块 服务端 客户端 通信循环 服务端 客户端 链接循环 半连接池 概念 产生半连接的两种情况 黏包问题 多次发送被并为一次 TC ...

  4. apparmor 源码分析

    这里不对apparmor做介绍,记录一下源码分析过程. 初始化 static int __init apparmor_init(void) -> security_add_hooks(appar ...

  5. OpenStack平台镜像优化

    在使用打快照方式制作镜像后,镜像的大小会变得非常大,比如一个基础的CentOS镜像大小为400M左右,但是使用打快照方式制作的镜像大小会有1个G左右,具体的大小还要根据安装的东西来实际情况实际分析. ...

  6. SQL多表多字段比对方法

    目录 表-表比较 整体思路 找出不同字段的明细 T1/T2两表ID相同的部分,是否存在不同NAME 两表的交集与差集:判断两表某些字段是否相同 两表的交集与差集:找出T2表独有的id 字段-字段比较 ...

  7. HCNP Routing&Switching之链路聚合

    前文我们了解了MSTP相关话题,回顾清参考https://www.cnblogs.com/qiuhom-1874/p/16268682.html:今天我们来聊一聊链路聚合相关话题: 链路聚合是链路高可 ...

  8. 零基础学Java第二节(运算符、输入、选择流程控制)

    本篇文章是<零基础学Java>专栏的第二篇文章,文章采用通俗易懂的文字.图示及代码实战,从零基础开始带大家走上高薪之路! 第一章 运算符 1.1 算术运算符的概述和用法 运算符 对常量和变 ...

  9. 组织:SAE

    美国汽车工程师学会(SocietyofAutomotiveEngineers),美国及世界汽车工业(包括航空和海洋)有重要影响的学术团体.简称SAE.已有90余年的历史.该学会实行会员制,约有会员69 ...

  10. Java高并发-概念

    一.为什么需要并行 业务要求 http处理多个客户端请求 java虚拟机启动多个线程 进程开销比线程大的多 性能 多线程在多核系统比单线程要好的多 摩尔定律失效 二.几个重要概念 2.1 同步和异步 ...