CMU15-445 Project1
Part I LRU Replacement Policy
类的设计
Replacer作为基类,LRU_Replacer作为继承类,实现基本功能:
Victim、Pin、Unpin、Size。
链表+Hash table来实现。Hash table存frame_id到链表上元素指针的映射。这里通过存指针来实现直接定位链表上的元素。链表的实现可以是作用域中的另外一个struct。一开始要有两个sentinel指针head和tail。
函数及其功能
Victim(frame_id_t *)
1 | auto LRUReplacer::Victim(frame_id_t *frame_id) -> bool { |
这个函数就是直接把链表首元素返回,同时在hash table中删除即可。
犯了一个错:如果要对指针指向的值进行修改,一定要先对指针进行解引用,如果对指针本身指向的地址更改,可以不用解引用,如果要对指针本身指向的值进行更改,并且要让这个更改传递下去,需要定义为指针的引用
Pin(frame_id_t frame_id)
1 | void LRUReplacer::Pin(frame_id_t frame_id) { |
就是直接从LRU队列中将这个元素给删掉。
Unpin(frame_id_t frame_id)
1 | void LRUReplacer::Unpin(frame_id_t frame_id) { |
一开始要判断LRU队列是不是满的,如果是满的,就要把队首的给删了。
设计要点
1.类中动态分配了内存,所以析构函数一定要考虑到类中所有的成员变量!对应着delete。这里要遍历链表进行delete:
1 | LRUReplacer::~LRUReplacer() { |
2.Latch需要在每个外部函数中加上。在加锁之前先分析清楚代码层次结构,如果外部加了锁内部就不要再继续加锁了。
3.对Pin和Unpin表示的意义:单看LRU这一部分不能理解的很清楚,先查阅整个项目整体的设计,需要设计哪些函数,每个函数用来干什么。
Part II BufferPoolManager
设计逻辑
这一部分和LRU这一部分联系起来。先要详细阅读任务书中的要实现的函数的定义、作用,记一下类中的成员变量的作用。做成一个文档列出来方便查阅,不急着写。
1.BufferPool上存储page的位置叫做frame。一开始所有的frame都是空的,空的frame_id都存在free_list中。从外层操作BufferPool的逻辑是,如果我们要分配一个新的page,那么就先看free_list上有没有空的frame,如果有就直接把page放到对应的frame上,否则从LRU中找。
2.LRU和BufferPool交互的逻辑:LRU队列上存的都是已经有page的frame,并且这些frame现在并没有被任何线程操作。也就是说,一开始LRU队列是空的,每当我们在BufferPool中对一个frame上的page操作完后,我们就在LRU层次上对他Unpin,那么就说明这个frame是可以被替换的。如果一个线程对这个frame要进行操作了,我们就在LRU层次上对这个frame Pin,那么这个frame就不能被替换了。
3.BufferPool上的成员操作的都是frame,因此需要有一个map将给定的page_id映射到frame_id上。
4.设计每一个函数的逻辑应该是这个函数的操作步骤–>LRU应该做什么。即上层应该考虑到下层应该做什么操作,想清楚再写。
5.不仅仅是LRU,对磁盘的读写操作也是一个下层。什么时候应该写回磁盘,需要在写每个函数前弄清楚。
函数及其功能
NewPgImp(page_id_t *page_id) -> Page *
1 | auto BufferPoolManagerInstance::NewPgImp(page_id_t *page_id) -> Page * { |
1.如果所有的frame都在被其他线程使用,无法分配也无法替换,失败。
2.如果能找到一个frame存新分配的page,就分配/替换。
3.修改新分配的page的元数据,确保所有的元数据都要修改到,即使本身不需要修改。这里新分配的page是默认被占用的,因此要在LRU上Pin住。
bool findFrame(frame_id_t *frame_id)
1 | bool BufferPoolManagerInstance::findFrame(frame_id_t *frame_id) { |
当我们要把一个page从frame上拿出时(可能是直接扔掉,也可能是要拿到上层进行一些修改操作),要写回磁盘。
FlushPgImp(page_id_t page_id) -> bool
1 | auto BufferPoolManagerInstance::FlushPgImp(page_id_t page_id) -> bool { |
特别注意这个函数和上一个函数都不要加锁。因为这两个函数在类中属于低层次,并不会直接被类外的函数直接调用,而一定是被类中的其他函数间接调用,如果加锁就会造成死锁问题。
BufferPoolManagerInstance::FetchPgImp(page_id_t page_id) -> Page *
1 | auto BufferPoolManagerInstance::FetchPgImp(page_id_t page_id) -> Page * { |
要点就是要Pin住和写回磁盘。
BufferPoolManagerInstance::DeletePgImp(page_id_t page_id) -> bool
1 | auto BufferPoolManagerInstance::DeletePgImp(page_id_t page_id) -> bool { |
如果一个page正在被使用,就不能删除。
UnpinPgImp(page_id_t page_id, bool is_dirty) -> bool
1 | auto BufferPoolManagerInstance::UnpinPgImp(page_id_t page_id, bool is_dirty) -> bool { |
设计要点
1.其实每个函数做的事情就是那么几步,自身怎么做、底层怎么做。
2.加锁的问题,先要搞清楚类中的层次,然后是类的每个成员的定义。
3.难点:Page不支持拷贝构造,因此要么用一个vector的emplace_back,要么用一个指针指向数组中的某个位置,对这个指针进行操作,也就是先默认对象数组中的所有元素都是初始化好了的。
PART III Parallel Buffer Pool Manager
设计逻辑
1.Parallel Buffer Pool Manager和Buffer Pool Manager都是Buffer Manager的子类,这里可以直接调用上一部分写的Buffer Pool中的函数,只需要在当前类中用一个数组存Buffer Pools即可。但是因为两个类是兄弟关系,如果要用到Buffer Pool Manager类中的方法,就需要声明友元。
2.一个不太好的设计:其实这里最好是用一个数组存Buffer Pool Manager的实例,但是受限于任务要求,这里我存了指针的指针数组,因此在操作指针的时候容易出问题:是否需要对指针本身进行修改?是否需要对指针指向的内容进行更改等等。而且一旦这样声明了,就必须遍历整个数组一个个delete对象,然后再来delete整个指针数组。
函数及其功能
NewPgImp(page_id_t *page_id) -> Page *
1 | auto ParallelBufferPoolManager::NewPgImp(page_id_t *page_id) -> Page * { |
1.在Parallel Buffer Pool Manager中维护一个start_index,采用轮询分配的方法,先看start_index的这个Buffer Pool能否分配,如果不行就跳到下一个,直到走完一个圈。每次分配新的page都要让start_index++。
2.一个大坑就是:因为之前存的是指针数组,当我取出这个数组中的一个指针时,我是要对这个数组中的元素进行更改的,也就是要对取出来的这个指针进行更改,因此需要声明为指针的引用。
设计要点
1.对于下层类,用什么数据结构来存。最好是直接存实例。
2.关于锁,下层的函数其实都上锁了,但是这里依然要上锁,因为涉及到对当前类中的成员的修改。
总结
1.先通读整个项目需求,搞清楚不同层次之间的联系、每个函数的定义。
2.从下往上设计,用单独的文档写出每个成员变量和成员函数的API。
3.每个类的析构函数,对着成员变量一个个进行检验。
4.写成员函数的时候,先考虑对本层做什么,再考虑对下层做什么,先想清楚再写。
5.锁最后再来加。