CMU 15445 Buffer Pool

运行测试以及格式化和提交

vscode 提供的 cmake 插件貌似可以直接点击最下面的 button 来直接构建、运行。
或者也可以手动构建运行

构建和运行

make lru_k_replacer_test -j$(nproc)
./test/lru_k_replacer_test

make buffer_pool_manager_test -j$(nproc)
./test/buffer_pool_manager_test

make page_guard_test -j$(nproc)
./test/page_guard_test 

格式化

make format
make check-lint
make check-clang-tidy-p1

写在前面

直接去写 task 1,发现如果知道上面 BFM 是如何被访问,以及访问页命中和不命中时具体做的事情,那么就好写多了

所以需要考虑下面这些事情

  1. 读取一个页
    • 在 buffer 里面
    • 不在 buffer 里面
  2. 写一个页
    • 在 buffer 里面
    • 不在 buffer 里面

task 1 实现 LRU-k

  • Evivt, 从缓冲里淘汰掉一个页
    • 当缓冲满的时候
  • RecordAcess, 记录下这个访问的页的页 id 以及时间戳
    • 当一个页被访问时,需要进行 pin,一般这个操作位于 pin 之后
  • Remove, 清空一个页的历史访问记录
    • 当一个页被从 BFM 中删除时才调用
  • SetEvictable, 设置一个页的状态为可以被删除或者不可以被删除,显然当pin的计数为0时,可以调用设置为可以被删除。
  • Size, 返回当前的可淘汰的页的数量,
  1. 淘汰的策略, 翻译过来就是
    • 所有的页都被访问了超过 k 次,那么选一个页,它的往前数 k 个访问记录(倒数第 k 次访问)的时间戳最早,淘汰掉这个页。
    • 如果有页没有被访问超过 k 次,那么在这些访问次数小于 k 的页里面优先选一个淘汰,选其中的具有最早的时间戳的页进行淘汰(就是 FIFO)
    • 所以网上有些关于 LRU-K 解释说是要有两个队列。

Backward k-distance is computed as the difference in time between current timestamp and the timestamp of kth previous access.A frame with fewer than k historical accesses is given +inf as its backward k-distance. When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).

task 2 实现 Buffer Pool Manager

首先我们已经有了一个 LRU-K,这是一个逻辑的存储管理,其次 Buffer Pool Manager,在构造的时候已经帮我们分配了一个 pool_size 的空间。我们要做的是怎么管理这个 pool_size 大小的空间(LRU-K)和物理内存页之间的映射。

我们就是通过这个 bpm 来管理我们的实际物理空间,具体替换的算法是上一个 task 实现的替换算法。

其中 frame 对应的是这个 pool_size 大小内存空间里的 Page,page_id 则是对应于物理内存页。

对于每个页的 metadata,我尝试使用页的 latch 去保护,对于 buffer 的数据结构,尝试使用 buffer 的 latch 去保护。

  • NewPage,在 buffer 里面创建一个新的 page;首先看有没有空的页,没有空的看是不是 buffer 里面所有页都在被 access,否则以 LRU-K 策略替换出去一个页。内存中的 page 使用 Page ​对象进行抽象,buffer pool 无需关心 Page 当中的内容,Page 当中包含一块内存,对应一个物理页,之后将对应的内存上的内容写入到硬盘上,一个 Page ​对应一个物理页,通过 page_id ​进行表述,如果该 Page 没有对应的物理页,则 page_id 为 INVALID_PAGE_ID​
    • 每次 NewPage 或者 FetchPage(没有命中),会从磁盘读一个页,然后会初始化 pin_count 为 1,防止这个刚刚被从磁盘读入的页,立即被换出。(后来又想了一下,感觉没有这个初始的 pin_count 初始化为 1,可能也是可以的吧)
  • FlushPage,将一个 buffer 里面的页写回 disk,不管这个页是不是脏的,然后修改 metadata,自己这里对 metadata 的修改都使用了写锁。
  • FlushAllPage
    • 将 buffer 里的所有页都写回 disk。需要注意的是,我自己的一开始的实现是多次调用 FlushPage,而在FlushPage内部对每个Page加锁上锁,需要考虑比如想要按顺序[1, 2, 3]写回 disk,在时刻 1 写 页 1,此时如果有线程对页 2,3 修改,然后在将 2,3 写回 disk,这样是不是合法的。(感觉要么这样做,要么一开始一下获取所有页的写锁)。
    • 之后的做法是获取 bpm 的锁,上锁,对每个页进行 flush,但我感觉正确的做法应该是先持有这个全局的 latch,然后获取所有页的 latch,当获取到所有页的 latch 之后,释放这个全局的 latch,然后进行 flush,flush 一个页释放一个页的锁。
  • FetchPage,实现和 NewPage 差不多,只有一些小区别在于命中时的处理。

一些注意的点

  • UnpinPageNewPage是相互对应的,new 一个 page 相当于 pin 了一次,pin_count 为 1,unpin 则重置 pin_count--, 当最后一个人访问结束后,还需要调用 UnpinPage,将这个 page 加入 free_list_,同理 FetchPageUnpinPage之间也类似。(这个可以通过 buffer_pool_manager_test.cpp 这个文件里的测试用例观察得到),那这里其实就与下面的 PageGuard 实现有关系其实。

bug

  1. 在构造函数里手动 new 了若干个 page 的空间以及一些 latch_,在析构函数中忘记手动释放导致内存泄漏

task 3 Read/Write Page Guards

Page Guard 存在意义是当一个读线程或者写线程访问一个 page 时或者不再使用这个 page 时,需要手动 pin 或者 unpin,使用 page_guard (页的守护),来自动完成这一过程。这有点类似智能指针,多个用户使用或者不再使用(跳出作用域),use_count() 也会自动减一。

leadboard test


ref

  1. https://zhuanlan.zhihu.com/p/644160340
posted @   o0yo  阅读(309)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示