前言

去年暑假完成了 CMU15-445 Fall 2019 的四个实验,分别对应下述博客:

今年打算接着完成 Fall 2020 的四个实验,同时解读一下课程组写好的那一部分代码,比如数据存储和页面布局的代码,加深自己对数据库系统的理解。

环境搭建

在 GitHub 上新建一个私有仓库,命名为 CMU15445-Fall2020,然后将官方仓库克隆到本地:

  1. git clone git@github.com:cmu-db/bustub.git ./cmu15445-fall2020
  2. cd cmu15445-fall2020

目前官方的代码应该更新到 Fall2023 了,需要回滚到 Fall2020,并将代码传到自己的远程仓库:

  1. git reset --hard 444765a
  2. git remote rm origin
  3. git remote add origin git@github.com:zhiyiYo/cmu15445-fall2020.git #添加自己仓库作为远程分支
  4. git push -u origin main

实验环境为 Ubuntu20.04 虚拟机,所以执行下述代码安装依赖包:

  1. sudo build_support/packages.sh

和去年一样,因为 googletest 仓库将 master 分支重命名为 main 了,所以需要将 build_support/gtest_CMakeLists.txt.in 的内容改为:

  1. cmake_minimum_required(VERSION 3.8)
  2. project(googletest-download NONE)
  3. include(ExternalProject)
  4. ExternalProject_Add(googletest
  5. GIT_REPOSITORY git@github.com:google/googletest.git
  6. GIT_TAG main
  7. SOURCE_DIR "${CMAKE_BINARY_DIR}/googletest-src"
  8. BINARY_DIR "${CMAKE_BINARY_DIR}/googletest-build"
  9. CONFIGURE_COMMAND ""
  10. BUILD_COMMAND ""
  11. INSTALL_COMMAND ""
  12. TEST_COMMAND ""
  13. )

最后编译一下,如果编译成功就说明环境搭建完成:

  1. mkdir build
  2. cd build
  3. cmake ..
  4. make

缓存池

由于磁盘读写速度远慢于内存,所以数据库会在内存中开辟一块连续空间,用于存储最近访问的页,这块空间称为缓存池。执行引擎不会直接从磁盘读取页,而是向缓存池要。如果缓存池中没有想要的页,就会从磁盘读入到池中,然后返回给执行引擎。页内数据更新后也不会立即写入磁盘,而是打上了一个 Dirty 标志位并暂存在缓存池中,等到时机成熟再写入。

缓冲池的本质是一个数组,只能存一定数量的页。如果执行引擎想要的 Page 不在缓存池中,且缓存池已满,这时候需要从中踢出一个页来腾出空间给新 Page,被踢出的 Dirty 页需要被保存到磁盘中来保证数据一致性。需要指出的是,不是任何 Page 都能被换出,那些正在被使用的页不能换出,而判断一个页是否正被使用的依据是 Page 内部保存的 Pin/Reference 计数器,只要计数器的值大于 0,就说明至少有一个线程在使用它。

缓冲池内部维护着一个 page_idframe_id 的映射表,用来指出页和内部数组索引的映射关系。同时内部还有一个互斥锁来保证并发安全,对缓存池的增删改查都需要上锁。

实验要求

任务 1:LRU Replacement Policy

Fall2019 要求实现的是时钟替换算法,而 Fall2020 则改成了 LRU 替换算法,实现方式一般使用双向链表 + 哈希表,C艹 可以直接用标准库中的 std::liststd::unordered_map。双向链表中存放允许被换出的 frame_id,哈希表中存 frame_id 及其对应的双向链表迭代器,这样可以实现 \(O(1)\) 复杂度的读写。链表的表头处存放最近访问的 frame_id,而尾处则是距离上次访问时间最远的的 frame_id

  1. class LRUReplacer : public Replacer {
  2. public:
  3. /**
  4. * Create a new LRUReplacer.
  5. * @param num_pages the maximum number of pages the LRUReplacer will be required to store
  6. */
  7. explicit LRUReplacer(size_t num_pages);
  8. ~LRUReplacer() override;
  9. /**
  10. * Remove the victim frame as defined by the replacement policy.
  11. * @param[out] frame_id id of frame that was removed, nullptr if no victim was found
  12. * @return true if a victim frame was found, false otherwise
  13. */
  14. bool Victim(frame_id_t *frame_id) override;
  15. /**
  16. * Pins a frame, indicating that it should not be victimized until it is unpinned.
  17. * @param frame_id the id of the frame to pin
  18. */
  19. void Pin(frame_id_t frame_id) override;
  20. /**
  21. * Unpins a frame, indicating that it can now be victimized.
  22. * @param frame_id the id of the frame to unpin
  23. */
  24. void Unpin(frame_id_t frame_id) override;
  25. /** @return the number of elements in the replacer that can be victimized */
  26. size_t Size() override;
  27. private:
  28. size_t num_pages_;
  29. std::list<frame_id_t> list_;
  30. std::unordered_map<frame_id_t, std::list<frame_id_t>::iterator> map_;
  31. std::shared_mutex mutex_;
  32. };

具体实现如下所示,可以看到 LRUReplacer 对缓冲池中存了多少页以及存了哪些页是一无所知的,它只关心能被换出的 frame_id,外界通过调用 LURReplacer::Unpin() 添加一个能被换出的 frame_id,调用 LRUReplacer::Pin() 来移除一个 frame_id

  1. LRUReplacer::LRUReplacer(size_t num_pages) : num_pages_(num_pages) {}
  2. LRUReplacer::~LRUReplacer() = default;
  3. bool LRUReplacer::Victim(frame_id_t *frame_id) {
  4. lock_guard<shared_mutex> lock(mutex_);
  5. if (Size() == 0) {
  6. return false;
  7. }
  8. *frame_id = list_.back();
  9. list_.pop_back();
  10. map_.erase(*frame_id);
  11. return true;
  12. }
  13. void LRUReplacer::Pin(frame_id_t frame_id) {
  14. lock_guard<shared_mutex> lock(mutex_);
  15. // frame 需要在缓冲池中
  16. if (!map_.count(frame_id)) {
  17. return;
  18. }
  19. auto it = map_[frame_id];
  20. map_.erase(frame_id);
  21. list_.erase(it);
  22. }
  23. void LRUReplacer::Unpin(frame_id_t frame_id) {
  24. lock_guard<shared_mutex> lock(mutex_);
  25. // 缓冲池满了不能插入新的 page,不能重复插入 page
  26. if (Size() == num_pages_ || map_.count(frame_id)) {
  27. return;
  28. }
  29. list_.push_front(frame_id);
  30. map_[frame_id] = list_.begin();
  31. }
  32. size_t LRUReplacer::Size() {
  33. return list_.size();
  34. }

在终端输入命令:

  1. mkdir build
  2. cd build
  3. cmake ..
  4. make lru_replacer_test
  5. ./test/lru_replacer_test

测试结果如下:

任务2:Buffer Pool Manager

BufferPoolManager 用于管理缓冲池,内部有一个 DiskManager 来读写磁盘数据,LRUReplacer 执行替换算法。这个类要求我们实现五个函数:

  • 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_ 中查找缓冲池的空位,如果没找到空位就得靠上一节实现的 LRUReplacer 选出被换出的冤大头

具体代码如下:


  1. Page *BufferPoolManager::FetchPageImpl(page_id_t page_id) {
  2. lock_guard<mutex> lock(latch_);
  3. // 1. Search the page table for the requested page (P).
  4. Page *page;
  5. auto it = page_table_.find(page_id);
  6. // 1.1 If P exists, pin it and return it immediately.
  7. if (it != page_table_.end()) {
  8. auto frame_id = it->second;
  9. page = &pages_[frame_id];
  10. replacer_->Pin(frame_id);
  11. page->pin_count_++;
  12. return page;
  13. }
  14. // 1.2 If P does not exist, find a replacement page (R) from either the free list or the replacer.
  15. // Note that pages are always found from the free list first.
  16. auto frame_id = GetVictimFrameId();
  17. if (frame_id == INVALID_PAGE_ID) {
  18. return nullptr;
  19. }
  20. // 2. If R is dirty, write it back to the disk.
  21. page = &pages_[frame_id];
  22. if (page->IsDirty()) {
  23. disk_manager_->WritePage(page->page_id_, page->data_);
  24. }
  25. // 3. Delete R from the page table and insert P.
  26. page_table_.erase(page->page_id_);
  27. page_table_[page_id] = frame_id;
  28. // 4. Update P's metadata, read in the page content from disk, and then return a pointer to P.
  29. disk_manager_->ReadPage(page_id, page->data_);
  30. page->update(page_id, 1, false);
  31. replacer_->Pin(frame_id);
  32. return page;
  33. }
  34. frame_id_t BufferPoolManager::GetVictimFrameId() {
  35. frame_id_t frame_id = INVALID_PAGE_ID;
  36. if (!free_list_.empty()) {
  37. frame_id = free_list_.front();
  38. free_list_.pop_front();
  39. } else {
  40. replacer_->Victim(&frame_id);
  41. }
  42. return frame_id;
  43. }

上述代码中还用了一个 Page::update 辅助函数,用于更新 page 的元数据:

  1. /**
  2. * update the meta data of page
  3. * @param page_id the page id
  4. * @param pin_count the pin count
  5. * @param is_dirty is page dirty
  6. * @param reset_memory whether to reset the memory of page
  7. */
  8. void update(page_id_t page_id, int pin_count, bool is_dirty, bool reset_memory = false) {
  9. page_id_ = page_id;
  10. pin_count_ = pin_count;
  11. is_dirty_ = is_dirty;
  12. if (reset_memory) {
  13. ResetMemory();
  14. }
  15. }

NewPageImpl(page_id)

该函数在缓冲池中插入一个新页,如果缓冲池中的所有页面都正在被线程访问,插入失败,否则靠 GetVictimFrameId() 计算插入位置:


  1. Page *BufferPoolManager::NewPageImpl(page_id_t *page_id) {
  2. // 0. Make sure you call DiskManager::AllocatePage!
  3. lock_guard<mutex> lock(latch_);
  4. // 1. If all the pages in the buffer pool are pinned, return nullptr.
  5. auto frame_id = GetVictimFrameId();
  6. if (frame_id == INVALID_PAGE_ID) {
  7. return nullptr;
  8. }
  9. // 2. Pick a victim page P from either the free list or the replacer. Always pick from the free list first.
  10. auto page = &pages_[frame_id];
  11. if (page->IsDirty()) {
  12. disk_manager_->WritePage(page->page_id_, page->data_);
  13. }
  14. // 3. Update P's metadata, zero out memory and add P to the page table.
  15. *page_id = disk_manager_->AllocatePage();
  16. page_table_.erase(page->page_id_);
  17. page_table_[*page_id] = frame_id;
  18. page->update(*page_id, 1, false, true);
  19. replacer_->Pin(frame_id);
  20. // 4. Set the page ID output parameter. Return a pointer to P.
  21. return page;
  22. }

DeletePageImpl(page_id)

该函数从缓冲池和数据库文件中删除一个 page,并将其 page_id 设置为 INVALID_PAGE_ID

  1. bool BufferPoolManager::DeletePageImpl(page_id_t page_id) {
  2. // 0. Make sure you call DiskManager::DeallocatePage!
  3. lock_guard<mutex> lock(latch_);
  4. // 1. Search the page table for the requested page (P).
  5. // 1. If P does not exist, return true.
  6. auto it = page_table_.find(page_id);
  7. if (it == page_table_.end()) {
  8. return true;
  9. }
  10. // 2. If P exists, but has a non-zero pin-count, return false. Someone is using the page.
  11. auto frame_id = it->second;
  12. auto &page = pages_[frame_id];
  13. if (page.pin_count_ > 0) {
  14. return false;
  15. }
  16. // 3. Otherwise, P can be deleted. Remove P from the page table, reset its metadata and return it to the free list.
  17. disk_manager_->DeallocatePage(page_id);
  18. page_table_.erase(page.page_id_);
  19. free_list_.push_back(frame_id);
  20. page.update(INVALID_PAGE_ID, 0, false);
  21. return true;
  22. }

UnpinPageImpl(page_id, is_dirty)

该函数用以减少对某个页的引用数 pin count,当 pin_count 为 0 时需要将其添加到 LRUReplacer 中:


  1. bool BufferPoolManager::UnpinPageImpl(page_id_t page_id, bool is_dirty) {
  2. lock_guard<mutex> lock(latch_);
  3. auto it = page_table_.find(page_id);
  4. if (it == page_table_.end()) {
  5. return false;
  6. }
  7. auto frame_id = it->second;
  8. auto &page = pages_[frame_id];
  9. if (page.pin_count_ <= 0) {
  10. return false;
  11. }
  12. page.is_dirty_ |= is_dirty;
  13. if (--page.pin_count_ == 0) {
  14. replacer_->Unpin(frame_id);
  15. }
  16. return true;
  17. }

FlushPageImpl(page_id)

该函数将缓冲池中的页写入磁盘以保持同步,这里不管页是否为脏,一律写入磁盘,不然并发的测试用例过不了:

  1. bool BufferPoolManager::FlushPageImpl(page_id_t page_id) {
  2. // Make sure you call DiskManager::WritePage!
  3. lock_guard<mutex> lock(latch_);
  4. auto it = page_table_.find(page_id);
  5. if (it == page_table_.end()) {
  6. return false;
  7. }
  8. auto &page = pages_[it->second];
  9. disk_manager_->WritePage(page_id, page.data_);
  10. page.is_dirty_ = false;
  11. return true;
  12. }

FlushAllPagesImpl()

该函数将缓冲池中的所有 page 写入磁盘:

  1. void BufferPoolManager::FlushAllPagesImpl() {
  2. lock_guard<mutex> lock(latch_);
  3. for (auto &[page_id, frame_id] : page_table_) {
  4. auto &page = pages_[frame_id];
  5. if (page.IsDirty()) {
  6. disk_manager_->WritePage(page_id, page.data_);
  7. page.is_dirty_ = false;
  8. }
  9. }
  10. }

测试

在终端输入指令:

  1. cd build
  2. make buffer_pool_manager_test
  3. ./test/buffer_pool_manager_test
  4. # 下面是从 gradescope 扒下来的测试用例
  5. make buffer_pool_manager_concurrency_test
  6. ./test/buffer_pool_manager_concurrency_test

测试结果如下:

总结

这个实验主要考察学生对并发和 STL 的掌握程度,由于注释中列出了实现步骤(最搞的是 You can do it! 注释),所以代码写起来也比较顺畅,以上~~

CMU15445 (Fall 2020) 之 Project#1 - Buffer Pool 详解的更多相关文章

  1. CMU15445 (Fall 2019) 之 Project#1 - Buffer Pool 详解

    前言 这个实验有两个任务:时钟替换算法和缓冲池管理器,分别对应 ClockReplacer 和 BufferPoolManager 类,BufferPoolManager 会用 ClockReplac ...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

随机推荐

  1. 关于VUE3的疑问。

    1.响应式数据的声明 中 ref 与 reactive 有什么区别? 答:参考答案 .个人理解:ref最好用来定义基本数据类型,使用时要用.value :reactive最好用来定义引用数据类型.re ...

  2. Defi开发简介

    Defi开发简介 介绍 Defi是去中心化金融的缩写, 是一项旨在利用区块链技术和智能合约创建更加开放,可访问和透明的金融体系的运动. 这与传统金融形成鲜明对比,传统金融通常由少数大型银行和金融机构控 ...

  3. 二进制安装Kubernetes(k8s)IPv4/IPv6双栈 v1.24.0

    二进制安装Kubernetes(k8s) v1.24.0 IPv4/IPv6双栈 介绍 kubernetes二进制安装 1.23.3 和 1.23.4 和 1.23.5 和 1.23.6 和 1.24 ...

  4. python之列表详解

    一组数据的集合,可以重复, 集合不可以重复 列表的定义 a=[] list(a) 常用操作 # 增加ss.append(1)#加到末尾ss.insert(0,7)#list_name.insert(i ...

  5. win32api中文在线文档

    中文文档http://www.yfvb.com/help/win32sdk/ 英文手册https://www.jb51.net/books/724576.html

  6. 在smt贴片加工中手工焊接和机器焊接的区别

    在smt贴片加工领域,都需要将电子元件贴装在pcb板表面并进行焊接的,常用的焊接方式分为两种:手动焊接和全自动机器焊接,而常用的焊接机器有回流焊机和波峰焊机,那你知道他们的区别是什么吗?安徽英特丽带你 ...

  7. MySQL(七)索引

    索引的数据结构 1 为什么使用索引 索引概述 索引(Index)是帮助MySQL高效获取数据的数据结构.是"排好序的快速查找结构",满足特定的查找算法 索引是在存储引擎中实现的,每 ...

  8. Job for nginx.service failed because the control process exited with error code.

    1. nginx启动报错: Job for nginx.service failed because the control process exited with error code. See & ...

  9. 这可能是最全面的Redis面试八股文了

    Redis连环40问,绝对够全! Redis是什么? Redis(Remote Dictionary Server)是一个使用 C 语言编写的,高性能非关系型的键值对数据库.与传统数据库不同的是,Re ...

  10. Swift下Data处理全流程:从网络下载,数模转换,本地缓存到页面使用

    Swift下将网络返回json数据转换成struct 假如网络请求返回的数据结构是一个深层嵌套的Json 首先要通过key-value取出这个json中的数据源 // 将返回的json字符串转Dict ...