Buffer Pool Manager
- 通关记录
- Task1 LRU-K Replacement Policy
- Task2 Disk Scheduler
- Task3 Buffer Pool Manager
- FlushPage
- FlushAllPages
- UnpinPage
- NewPage
- FetchPage
- DeletePage
- Optimizations
CMU-15445汇总
本文对应的project版本为CMU-Fall-2023的project1
由于Andy要求,本博客只提供思路,不会公开任何代码
通关记录
目前的rank还比较低,lru-k后续会优化下
Task1 LRU-K Replacement Policy
LRU-K
与LRU
一样,都属于缓冲区的页面置换算法,不同的是,LRU-K
考虑的是页面的之前第K
次访问时间戳与当前时间戳的差(若不足K
次则优先驱逐,若有多个访问不足K
次的页面,则按LRU
规则驱逐),而LRU
考虑的是页面最近一次访问时间戳与当前时间戳的差。一个具体的例子如下图所示,假设K=3
,buffer
大小为3,访问序列为1 2 3 2 3 2 3 1 1 4
,当访问4
时,会将1
驱逐掉,图中当页面访问不足K
次时,时间戳为最新访问时间戳,否则为前第K
次的时间戳(蓝色数字为时间戳)。
Task1的要求是实现src/buffer/lru_k_replacer.cpp
文件中的如下几个函数:
Evict(frame_id_t* frame_id)
:使用LRU-K
算法驱逐一个frame
,并使用参数返回frame_id
;返回值类型为bool
,若驱逐成功返回true
,若当前可驱逐frame
数量为0,则返回false
。RecordAccess(frame_id_t frame_id)
:记录给定frame
的访问历史Remove(frame_id_t frame_id)
:将给定frame
从buffer
中移除SetEvictable(frame_id_t frame_id, bool set_evictable)
:设置frame
为可驱逐或不可驱逐,若设置前后该属性不一致,则需要修改类内维护的可驱逐frame
数量属性Size()
:返回当前可驱逐的frame
数量
个人建议的实现顺序为Size
->SetEvictable
->RecordAccess
->Evict
->Remove
主要的难点在于RecordAccess
与Evict
中各frame
的LRU-K
信息维护与驱逐算法实现,目前我实现的版本为暴力版本,即在RecordAccess
为每个frame
维护一个大小为K
的访问时间戳队列;在Evict
中,遍历所有可驱逐的frame
,找出LRU-K timestamp
最小的那个,将其驱逐。(这个做法明显太慢了,后续优化一下)
Task2 Disk Scheduler
此部分需要实现一个简单的磁盘调度器,接收BufferPoolManager
发来的读写磁盘请求放入一个请求队列中;然后启动一个新线程,不断从请求队列中获取请求,根据请求类型调用对应DiskManager
的读写函数进行磁盘读写。主要实现文件src/storage/disk/disk_scheduler.cpp
中的两个函数:
Schedule(DiskRequest r)
:接收请求并放入请求队列StartWorkerThread()
:线程函数,从请求队列中获取新请求,并根据请求类型调用磁盘读写函数
个人建议的实现顺序为Schedule
->StartWorkerThread
不需要考虑队列的线程安全性,已经有一个Channel
类帮忙实现了生产者消费者模型;关于std::promise
的用法,可参考disk_scheduler_test.cpp文件。
Task3 Buffer Pool Manager
这一部分需要实现一个BufferPoolManager
,结合前两部分实现的LRU-K Replacer
与Disk Scheduler
进行缓冲区的管理与物理页的开辟、获取与释放。BufferPoolManager
类维护的数据结构中包含一个Page
类的数组(在BufferPoolManager
的构造函数中会开辟空间),数组中的每个元素即为一个一个的frame
。Page
类主要包含以下几个成员:
page_id_t page_id_
:表示该frame
指向的物理页面idchar* data_
:用于储存对应物理页面中的真实数据int pin_count_
:故名思意,该frame
的pin count
,表示被多少个进程pin
住,当pin_count_
不为0时,该frame
不能被驱逐bool is_dirty_
:该frame
是否被写过,如果被写过则需要将内容写回磁盘
在这个任务中,我们需要实现文件src/buffer/buffer_pool_manager.cpp
中的以下方法:
FetchPage(page_id_t page_id)
UnpinPage(page_id_t page_id, bool is_dirty)
FlushPage(page_id_t page_id)
NewPage(page_id_t* page_id)
DeletePage(page_id_t page_id)
FlushAllPages()
个人建议的实现顺序为:FlushPage
->FlushAllPages
->UnpinPage
->NewPage
->FetchPage
->DeletePage
接下来我介绍下我每个函数的实现思路
FlushPage
功能:将给定的缓存页写回磁盘,无视is_dirty_
标志
参数:page_id
表示给定的想要flush到磁盘的物理页id
返回值:bool
,若page_id
不存在,返回false;否则true
实现:BufferPoolManager
类中需要维护一个page_table_
,存放着每个已分配物理页对应的frame
,通过page_table_
查找到给定page_id
对应的frame_id
,然后调用Disk Scheduler
提供的scheduler
方法发出写请求即可,最后清空frame
的is_dirty_
标志。
FlushAllPages
功能:将所有有效的缓存页写回磁盘,无视is_dirty_
标志
参数:无
返回值:void
实现:遍历整个buffer
的所有frame
,若frame
上的page_id
不为空,则使用与FlushPage
类似方法写回磁盘即可。
UnpinPage
功能:将指定的物理页对应的frame
的pin_count
减1
参数:page_id
表示给定的物理页id,is_dirty
表示在pin住此frame
时是否发生了写操作
返回值:bool
,若指定的物理页id不存在,返回false;否则true
实现:首先根据page_id
找到对应的frame
,将frame
的pin_count
减1,此时若pin_count
为0,还需调用LRU-K Replacer
提供的SetEvictable
函数设置frame
的状态为可驱逐。然后,根据is_dirty
参数设置frame
的is_dirty_
成员即可。
NewPage
功能:找到一个空闲的frame
,新分配一个物理页,并将该物理页的内容读取到刚找到的这个frame
中
参数:page_id_t *page_id
分配的物理页id以参数形式返回
返回值:Page*
表示新分配的物理页最终存放的frame
地址
实现:找到空闲的frame
首先从一个free list
里面找,如果free list
为空表明现在的buffer
已满,需要使用LRU-K Replacer
驱逐一个frame
并将其作为新物理页的承载体(若该frame
的is_dirty_
为true
,那么需要先将frame
中的内容写回对应的物理页)。接着,调用AllocatePage
函数分配新物理页,并设置frame
中的相关成员即可。最后,需要调用LRU-K Replacer
提供的RecordAccess
函数记录下访问历史,并调用SetEvictable
函数pin住该frame
。
FetchPage
功能:给定物理页id,获取该物理页所对应的frame
参数:page_id
表示给定的物理页id
返回值:Page*
表示给定物理页所在的frame
地址
实现:首先从page_table_
中寻找给定物理页对应的frame
,若未找到,则需要驱逐缓存页,这一块的处理与NewPage
函数中驱逐的处理一致,代码可以复用;若找到则进行下一步。然后,将最终确定的frame
做记录访问历史与pin住操作,也与NewPage
中的操作一致。
DeletePage
功能:给定物理页id,将物理页对应的frame
从buffer
中删除
参数:page_id
表示给定的物理页id
返回值:bool
,物理页不在buffer
中或删除成功则返回true;物理页存在且对应的frame
仍然被pin住(pin_count
不为0)时,则返回false
实现:查找物理页对应的frame
,将page_id
从page_table_
中移除,调用LRU-K Replacer
的Remove
函数将frame_id
从buffer
中移除,并将frame_id
加入free_list_
中,重置frame
的成员,最后调用DeallocatePage
将物理页回收