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 是如何被访问,以及访问页命中和不命中时具体做的事情,那么就好写多了
所以需要考虑下面这些事情
- 读取一个页
- 在 buffer 里面
- 不在 buffer 里面
- 写一个页
- 在 buffer 里面
- 不在 buffer 里面
task 1 实现 LRU-k
Evivt
, 从缓冲里淘汰掉一个页- 当缓冲满的时候
RecordAcess
, 记录下这个访问的页的页 id 以及时间戳- 当一个页被访问时,需要进行
pin
,一般这个操作位于pin
之后
- 当一个页被访问时,需要进行
Remove
, 清空一个页的历史访问记录- 当一个页被从 BFM 中删除时才调用
SetEvictable
, 设置一个页的状态为可以被删除或者不可以被删除,显然当pin
的计数为0时,可以调用设置为可以被删除。Size
, 返回当前的可淘汰的页的数量,
- 淘汰的策略, 翻译过来就是
- 所有的页都被访问了超过 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 一个页释放一个页的锁。
- 将 buffer 里的所有页都写回 disk。需要注意的是,我自己的一开始的实现是多次调用
FetchPage
,实现和NewPage
差不多,只有一些小区别在于命中时的处理。
一些注意的点
UnpinPage
和NewPage
是相互对应的,new 一个 page 相当于 pin 了一次,pin_count 为 1,unpin 则重置 pin_count--, 当最后一个人访问结束后,还需要调用UnpinPage
,将这个 page 加入 free_list_,同理FetchPage
和UnpinPage
之间也类似。(这个可以通过 buffer_pool_manager_test.cpp 这个文件里的测试用例观察得到),那这里其实就与下面的PageGuard
实现有关系其实。
bug
- 在构造函数里手动 new 了若干个 page 的空间以及一些 latch_,在析构函数中忘记手动释放导致内存泄漏
task 3 Read/Write Page Guards
Page Guard 存在意义是当一个读线程或者写线程访问一个 page 时或者不再使用这个 page 时,需要手动 pin 或者 unpin,使用 page_guard (页的守护),来自动完成这一过程。这有点类似智能指针,多个用户使用或者不再使用(跳出作用域),use_count() 也会自动减一。
leadboard test
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通