Specification
正式进入proj1, 需要实现使用lru
替换策略的并发安全的bufferpool manager
;
The first programming project is to implement a buffer pool in your storage manager.
Your implementation will need to be thread-safe.
task1 LRU replacement policy
Victim(T*)
: Remove the object that was accessed the least recently compared to all the elements being tracked by theReplacer
, store its contents in the output parameter and returnTrue
. If theReplacer
is empty returnFalse
.Pin(T)
: This method should be called after a page is pinned to a frame in theBufferPoolManager
. It should remove the frame containing the pinned page from theLRUReplacer
.Unpin(T)
: This method should be called when thepin_count
of a page becomes 0. This method should add the frame containing the unpinned page to theLRUReplacer
.Size()
: This method returns the number of frames that are currently in theLRUReplacer
.
阅读相关信息:
首先**LRUReplacer
extends the abstract Replacer
class**,
因此先阅读Replacer.h
这个类:
这是一个抽象类, 提供接口
virtual bool Victim(frame_id_t *frame_id) = 0;
- 定义了buffer 满的时候该换出哪一页, 结果通过传入的参数
frame_id
指针返回, 其中类型frame_id_t
是int32_t
; - 如果没有
victim
(比如所有page都碰巧被pin
住了)就给frame_id
赋为nullptr, 并且返回false; - 如果找到了
victim
, 就让这个int32_t
指针指向frame的id, 不是page的id!!!并且返回true;
- 定义了buffer 满的时候该换出哪一页, 结果通过传入的参数
virtual void Pin(frame_id_t frame_id) = 0;
- 当一个
frame
上的page
(frame中的内容就是disk上的page内容的copy)正在被访问的时候, 我们应该把这个frame
pin住, 也就是说我正在访问, 你别把我这页换出了, 不然就出错了;
- 当一个
virtual void Unpin(frame_id_t frame_id) = 0;
- 当这个
frame
没有在被使用了, 那么如果你buffer pool
满了就可以把我这个frame当作换出的一个candidate;
- 当这个
virtual size_t Size() = 0;
- 返回
replacer
中可以被victim的candiate的数量, 所以一开始replacer的size应该是0, 随着使用并且出现unpin, 这时候这个frame
的id
就会被加入replacer
, 作为可以被victim
的candidate
;
- 返回
继续阅读LruReplacer
发现他引入了/common/config.h
头文件
阅读config.h
可见主要定义了一些类型:
Invalid
的id都是-1
;- 一页的大小是
4096B
也就是4KB
和sqlite
一样; BUFFER_POOL_SIZE
为10, 应该是buffer pool有10个frame的空间
;- log buffer没弄懂, 应该和这一个proj无关
- bucket_size是extendible hashtable的桶的数目
- 然后定义了
frame, page, txn, log seq number
的类型都是int32_t
; slot_offset_t
是指的tuple的起始位置;- 然后最顶上三个是一些功能性的定义, 包括
环检测周期
,是否开启log
,log刷盘的周期
;
阅读完毕, 开始实现
- 头文件定义一些member variables:
1 | size_t lruCapacity; // maxSize lruReplacer need to store |
- 具体实现:
1 |
|
目前这个实现num_pages
完全没作用, 不知道是不是出错了, 测试也无法运行测试. 先写第二个任务看看;
task2 BUFFER POOL MANAGER
The BufferPoolManager
is responsible for fetching database pages from the DiskManager
and storing them in memory. The BufferPoolManager
can also write dirty pages out to disk when it is either explicitly instructed to do so or when it needs to evict a page to make space for a new page.
与磁盘打交道的代码已经帮我们写好了, 不需要管;
each Page
object contains a block of memory that the DiskManager
will use as a location to copy the contents of a physical page that it reads from disk. The BufferPoolManager
will reuse the same Page
object to store data as it moves back and forth to disk. This means that the same Page
object may contain a different physical page throughout the life of the system. 再次强调: 内存中的page 和 磁盘中的page不同
The Page
object’s identifer (page_id
) keeps track of what physical page it contains, 当一个page未没有包含磁盘page的时候, page_id应该设为invalid_page_id
即-1;
每个内存中的page还维护了一个pin_count
来记录有多少个线程正在使用这个page, 即pin住了; bufferpool manager
显然不应该free掉一个pin_count > 0
的page; 此外还维护了一个脏位, 当一个page被victim了, 如果dirty_flag, 那必须要把这一页的内容写入磁盘;
会用到task1实现的lru_replacer
来决定evict哪个page;
For FetchPageImpl,you should return NULL if no page is available in the free list and all other pages are currently pinned.
FlushPageImpl should flush a page regardless of its pin status.
阅读相关代码
导入了page.h
阅读/storage/page/page.h
:
friend class BufferPoolManager;
和porj1相关的大概就是这部分, 主要需要注意的是:
page_id
: unique identifier, 当page中没有实际包含data的时候(即不是某个physical page的copy) 设为INVALID_PAGE_ID
pin_count
表示有多个线程正在使用这一页, 当计数将为0的时候就可以加入lru_replacer
中了is_dirty
: 表示这一页是否被修改过, 初步设想是在lru决定换出这一页的时候, 查询这个flag, 如果true, 那么应该写入磁盘;data_
: 一页大小为4096字节即4KB;
导入了disk_manager.h
阅读storage/disk/disk_manager.h:
- DiskManager takes care of the allocation and deallocation of pages within a database. It performs the reading and writing of pages to and from disk, providing a logical file layer within the context of a database management system.
- 使用数据库文件的文件名来构造一个disk_manager:
explicit DiskManager(const std::string &db_file);
- 与proj1相关的函数签名:
1 | /** |
需要实现的函数:
FetchPageImpl(page_id)
: 从buffer pool
取出page_id
对应的页NewPageImpl(page_id)
: 创建一个新的pageUnpinPageImpl(page_id, is_dirty)
: unpin一个page, 注意传入了脏flagFlushPageImpl(page_id)
: 将page_id对应的页写入磁盘DeletePageImpl(page_id):
将page_id这页从buffer pool删除FlushAllPagesImpl():
将buffer pool中所有页写入磁盘
这个task需要先厘清关系, 否则会错误频出, 以下是我写的过程中的发现:
page table
是由unordered_map
实现的, 负责mapping
page_id_t
到frame_id_t
, 这里需要注意的一点是page_id
是由disk_manager->AllocatePage()
生成的, 官方提供的实现是简单的把一个atomic
的id从0一直往上加, 所以每个page_id
都唯一得代表了一个物理磁盘上的page
, 内存中的page
只是把磁盘上的page
读入内存中的frame
中的copy!!!- 当需根据
page_id
修改一个在内存中的page
, 我们需要通过page table
把page_id
map到frame_id
, 因为在buffer pool manager中分配连续的Page *pages_ = new Page[max_pool_size]的空间, 所以我们需要的页的指针的就是pages + page_table[page_id]; - 上一个
lru_replacer
提供的pin 和 unpin接口其实很垃圾, 不符合他的名字, 应该是向lru_replacer中remove和insert的功能;
1 | //===----------------------------------------------------------------------===// |
在实现过程中, 遇到的第一个大问题就是死锁:
- 本来这些需要实现的接口是可以互相调用的, 但是这就存在一个问题:
- 这些接口函数都需要获取
latch
来实现线程安全; - 一个函数比如newPage获取了latch后调用flushPage;
- flushPage中第一行代码也是获取latch, 显然无法获得, 就会死锁
解决办法:
- 一开始网上乱查了个
adopt_lock
, 结果发现这个只能让程序跑起来, 错误百出; - 可重用锁不考虑, 被陈硕疯狂diss;
- 新增了一个辅助函数,
GetVictim()
, 意思就是我需要一个page的空间的时候, 你必须要给我搞这样一块空间来, 不管是从free_list还是lru_replacer; - 当需要写文件到磁盘的时候不调用flushPage, 直接disk_manager操作;
- 辅助函数中不用获取latch: 因为调用辅助函数的函数caller一定已经在第一行代码拿到latch了, 而整个class只有一个latch, 所以不会有别的函数能够调用这个辅助函数, 所以线程安全;
以上代码本地测试通过, 官方网站测试还没学会怎么用, build失败.
官方网站满分, 可以放心写下一个部分了(修改了一个bug, 就是当我要拿的页存在页表中, 这页也可能在lru中!!!所以fetchPage如果pin_count == 0, 一定要调用replacer->unpin);
;