前言
最近有空了,做了15-445的第一个lab。因为这个学期网上评测已经结束了,所以还没来得及在线上测试。
Project1 Buffer Pool中有三个小Task,第一个是实现可拓展哈希(网上有很多资料,我觉得也可以用C++已经封装好的容器代替)。第二个是LRU-K算法的实现,在我之前这篇博客中讨论过缓存替换策略:LRU-K算法详解及其C++实现 CMU15-445 Project#1
然后在这里主要讨论第三个Task,BufferPoolManagerInstance的实现。
题目讲解
- 在实现缓冲池之前,我们先需要知道两个概念的区别:frame和page。
- 在这里frame是指在内存中的一块区域,用于存放page。frame在内存中是固定的
- page是包含了元信息(比如是否是脏页面、page_id等)和page_data的一个类。page可以存放在frame中,也可以将数据写在disk上以便被缓存池丢掉。
属性
在类BufferPoolManagerInstance
主要有以下属性
- pool_size_
- pool_size_是缓冲池的大小,也是最多frame个数。
- Page *pages_
- 指向一个Page数组,用于存放所有的Page。
- free_list_
- 一个list,里面存放了空闲的frame_id
- 还有之前实现的哈希表、lru_k_replacer、打开了当前对应文件的disk_manager
BufferPoolManagerInstance
是class Page
的友元,可以直接访问Page的属性,我们需要用到Page以下的属性- data_是一个字符数组,用于存放数据
- page_id_
- is_dirty_ 记录是否是脏页面
- pin_count_ 记录被pin住的数量,当pin_count==0时,这个页面才能够被驱逐
方法
NewPgImp
- 功能:为当前的数据库增加一页,并且返回该页。
- 如果内存中有空闲的frame,(free_list不空)则直接把该页放到这个frame上。否则通过之前的LRU_K策略驱逐一个frame,这里如果frame中的page是脏的,需要用之后的Flush方法写回磁盘。
- 这里要注意,生成一个Page后记得把它的data_清零,然后驱逐、增加等操作都要影响page_table_和replacer_的数据,记得哈希表去掉旧的page_id并添加新的键值对。
- 新的page按照题目意思pin_count_为1,不能够被驱逐。
FetchPgImp
- FetchPgImp传入一个page_id,并返回对应页面的指针。
- 如果通过hash表查到该page在内存中,直接返回,记得LRU-K记录一下访问历史。
- FetchPgImp驱逐过程和上一个方法类似。但是要加一步:从磁盘中将之前的data读入内存
disk_manager_->ReadPage(page_id,pages_[frame_id].GetData());
注意 - 如果要fetch的页面已经在pool中了,那么要进行以下操作
- lru-k要进行record
- 手动setEvictable为false
- pincount++
- 如果不在pool中,那么也要进行以上操作,并且pincount = 1
- 上面这个卡了我很久,因为在文档里也没写具体的要求,大家写的时候注意一下
UnpinPgImp
- 如果对应页面
pin_count>0
将pin_count_--
- 当pin_count为0,设置该页面可被驱逐
FlushPgImp
- 通过disk_manager的WritePage将页面写入disk中
- 将脏页面标记设置为false.
FlushAllPgsImp
- Flush所有页面,注意这里不能写frame为空的位置
- 可以设置一个标记数组,将free_list遍历一遍找出空的frame,然后再遍历一次buffer_pool,不为空则flush。带来的时间和空间复杂度冗余都是 O ( p o o l s i z e ) O(pool size) O(poolsize),可以接受。
DeletePgImp
- 删除指定Page
代码实现
- 在这个Project中我学到了几个c++的用法。
Page *pages_; pages_ = new Page[pool_size_];
之后,pages_指向一个数组。- 在创建新page时,
pages_[frame_id]
是一个Page类型,不能通过指针创建,比如pages_[frame_id] = new Page()
是错的。也不能pages_[frame_id] = Page()
,因为Page没有拷贝方法。我们可以通过new(&pages_[free_frame]) Page();
在指定位置上构造一个新的Page。这里new里面的参数是一个地址。 - 同理,
pages_[frame_id]
不是一个指针,所以我们在删除页的时候,不能使用delete pages_[frame_id]
,可以通过pages_[frame_id].~Page()
来调用析构函数。
然后其他的代码实现按照题目要求做就行了,注意每次frame的变更对page_table_、replacer_的影响,遇到错误使用GDB或者通过IDE调试工具来调试找到问题。
关于多线程
我自己直接用的是大锁,也就是在每个函数的第一排加了一行
std::scoped_lock<std::mutex> lock(latch_);
注意如果用这种方法,那么你的函数之间是不能相互调用的,比如你在、FetchPgImp
中,准备驱逐一个脏页面,那么你是不能调用FlushPgImp
的,否则会引起死锁。同理AllocatePage()
前面也是不能加锁的,因为AllocatePage()
会被FetchPgImp
调用。
如果要写更细粒度的话,可以尝试读写锁分开,然后用Page里面的小锁。不过写的粗糙一点也能过就是了