Lab1 - Buffer Pool Manager
实验指导书
-
构建一个新的面向磁盘的存储管理器,这样的存储管理器假定数据库的主要存储位置在磁盘上。
-
在存储管理器中实现缓冲池。缓冲池负责将
pag
从主存到磁盘来回移动。允许 DBMS 支持大于系统可用内存量的数据库。缓冲池的操作对系统中的其他部分是透明的。例如,系统使用其唯一标识符 (page_id_t
)向缓冲池请求页面,但它不知道该页面是否已经在内存中,或者系统是否必须从磁盘中检索它。 -
实现需要是线程安全的。多个线程将同时访问内部数据结构,因此需要确保临界区受到
latches
的保护。 -
需要在存储管理器中实现以下两个任务:
(1)LRU 替换原则
(2)缓冲池管理器
-
任务 1 - LRU 替换策略
(1)该组件负责跟踪缓冲池中的页面使用情况。将在 src/include/buffer/lru_replacer.h 中实现一个新的子类
LRUReplacer
,它相应的实现文件在 src/buffer/lru_replacer.cpp 。LRUReplacer
继承了抽象类Replacer
(src/include/buffer/replacer.h)。(2)
LRUReplacer
的大小与BufferPoolManager
相同,因为在BufferPoolManager
中包含了所有frames
的placeholders
。 但是,并非所有frames
都被视为LRUReplacer
. 将LRUReplacer
被初始化为有没有frames
。然后,LRUReplacer
将只考虑新的没有划分的那些。(3)需要实现课程中讨论的
LRU
策略。您将需要实现以下方法:Victim(T*)
:Replacer
跟踪与所有元素相比最近访问次数最少的对象并删除,将其删除页号存储在输出参数中,并返回True
。如果Replacer
为空则返回False
。Pin(T)
:在将page
固定到BufferPoolManager
中的frame
之后,应该调用此方法。它应该从LRUReplacer
中删除固定包含固定page
的frame
。Unpin(T)
:当页面的引用计数变为 0 时,应该调用此方法。这个方法应该将未包含固定page
的frame
添加到LRUReplacer
。(注意,需要判断是否超出了内存大小,如果超过了,则删除较新的页面,然后再添加。)Size()
:这个方法返回当前在LRUReplacer
中的页面数。
-
任务 2 - 缓冲池管理器
- 接下来,需要在系统中实现缓冲池管理器 (
BufferPoolManager
)。该BufferPoolManager
负责从DiskManager
中读取数据库页并将它们存储在内存中。BufferPoolManager
还可以在明确指示或需要为新页腾出空间时,将脏页写入磁盘。 - 为了确保实现与系统的其余部分一起正常工作,提供了一些已经填写好的功能。也不需要实现实际读写数据到磁盘的代码(在给出的实现中称为
DiskManager
)。 - 系统中所有的内存页面都由
Page
对象表示。BufferPoolManager
并不需要了解这些页的内容。但是,作为系统开发人员,重要的是要理解Page
对象只是缓冲池中用于存储内存的容器,因此不是特定于唯一的页面。也就是说,每个Page
对象都包含一个内存块,DiskManager
将它用作复制从磁盘读取的page
内容的位置。当它来回移动到磁盘时,BufferPoolManager
将重用相同的Page
对象来存储数据。这意味着相同的Page
在系统的整个生命周期中可能包含不同的物理页面。Page
对象的标识符 (page_id
) 跟踪它所包含的物理页面,如果Page
对象不包含物理页,则必须将其page_id
设置为INVALID_Page_id
。 - 每个
Page
对象还维护了一个计数器,用于表示 “固定” 该页面的线程数。BufferPoolManager
不允许释放被 ”固定“ 的页面。每个Page
对象还跟踪它标记的脏页。我们需要判断页面在解除绑定之前是否被修改过。BufferPoolManager
必须将dirty Page
的内容写回磁盘,然后才能重用该对象。 BufferPoolManager
的实现将使用上述步骤中创建的LRUReplacer
类。它将使用LRUReplacer
来跟踪Page
对象被访问的时间,以便在必须释放frame
来腾出空间给从磁盘复制的新物理页时决定删除哪个对象。- 需要实现头文件(src / compress / buffer_pool_pool_pool.h)中定义的在源文件 (src / buffer / buffer_pool_manager.cpp) 中的下列函数:
FetchPageImpl(page_id)
NewPageImpl(page_id)
UnpinPageImpl(page_id, is_dirty)
FlushPageImpl(page_id)
DeletePageImpl(page_id)
FlushAllPagesImpl()
- 对于
FetchPageImpl
,如果空闲列表中没有页面可用,并且所有其他页面当前都被固定,则应该返回NULL
。不管FlushPageImpl
的引用状态如何,都应该刷新页面。
-
测试
-
LRUReplacer
:test/buffer/lru_replacer_test.cpp
-
BufferPoolManager
:test/buffer/buffer_pool_manager_test.cpp
-
本地测试:
// 别忘了删除测试中的 disabled $ mkdir build $ cd build $ make lru_replacer_test $ ./test/lru_replacer_test $ mkdir build $ cd build $ make buffer_pool_manager_test $ ./test/buffer_pool_manager_test // 检查 $ cd build $ make format $ make check-lint $ make check-clang-tidy
-
-
提交:
-
src/include/buffer/lru_replacer.h
-
src/buffer/lru_replacer.cpp
-
src/include/buffer/buffer_pool_manager.h
-
src/buffer/buffer_pool_manager.cpp
-
打包
// 下面的写在一行,空格分隔。最好是写个bash脚本,比较方便。 zip project1.zip src/include/buffer/lru_replacer.h src/buffer/lru_replacer.cpp src/include/buffer/buffer_pool_manager.h src/buffer/buffer_pool_manager.cpp
-
设计思路
LRU(Least-Recently Used)
的策略:- 数据一般是存储在磁盘上的,当我们需要读取一个数据的时候,会首先把它加载到内存中,然后返回给客户端。
- 因为一般内存比磁盘小,容量有限,不可能同时存储那么多的数据。因此,内存会时常把暂时不需要的页面换回磁盘中,等到调用的时候再次置换回来。
LRU
正是页面置换算法的一种,最近最少使用的策略,它有一个潜在的假设,如果某个页的数据被访问过一次,那么下次再被访问的机率也就更高。
LRU
的思路:- 首先,页面需要使用一个数据结构来存储,考虑到它需要不断的插入和删除,特别是需要头插和尾删,因此使用双向链表是比较方便的。另外,可以增加一个哈希表来加快查找和定位的速度(O(1))。综合考虑,使用双向链表 + Hash Table 来完成比较合适。
- 双向链表负责维护一个页面的集合,数据按照从最近使用过的到最近没有使用的来排序。因此,每当更新某个页面的时候,都应该把它放在双向链表的头部,即使是某个页面已经出现在链表中,也要拿出来,重新放置。每当置换的时候,都应该从双向链表的尾部置换页面,取出最近没有使用的页面。
- Hash Table 负责维护一个从页面到链表节点的映射。它的作用有两个,一个是方便查找某个页面当前是否在这个链表当中。另一个是能够快速定位到当前页面在链表中的位置,方便删除操作。
Clock
策略:Clock
也是页面置换算法的一种。Clock
置换算法分为两种,一种是简单的置换算法,与LRU
算法类似。另一种是改进型的,相比于前一种,减少了磁盘IO,性能更加高效。- 简单
Clock
的思想是先将内存中的所有页面想象成一个环形队列,通过维护一个访问位,每次更新的时候,如果访问位为 0,表示最近没有被访问,则可以置换;否则,将访问位置 0,继续寻找。 - 改进版的思想是,需要添加一个修改位,修改了的置 1 ,没有则置 0 。那么当要置换时,(访问位,修改位)可能的组合按优先级分为以下四种:(0,0)、(1,0)、(0,1)、(1,1)。因此,它的执行过程是:(1)循环扫描查找(0,0)。有则退出。如果访问位为1则置 0 。(2)循环扫描查找(0,1)。有则退出。如果访问位为1则置 0 。(3)重复第一步。
Clock
设计思路:- 这里只考虑简单的
Clock
设计思路。维护一个访问位数组和一个指针。每次当成环形数组循环查找当前需要被置换的页面。
- 这里只考虑简单的
Coding
C++ :std::scoped_lock 能够避免死锁的发生。它的构造函数能够自动进行上锁操作,析构函数会对互斥量进行解锁操作。能够保证线程安全。
-
根据上面
LRU
的设计思路,我们可以定义出LRUReplacer
的数据结构,如下。class LRUReplacer : public Replacer { private: // TODO(student): implement me! // 为了线程安全需要加的锁 std::mutex latch; // 这个表示 LRUReplacer 的大小,Buffer Pool大小相同。 size_t capacity; // 我们使用 双向链表 + 哈希表 的数据结构。 std::list<frame_id_t> LRUList; // 使用 unordered_map(注意加头文件),从 frame_id 映射到 Node。 std::unordered_map<frame_id_t, std::list<frame_id_t>::iterator> LRUHash; };
-
根据上面
Clock
的设计思路,我们可以定义出ClockReplacer
的数据结构,如下。class ClockReplacer : public Replacer { private: // TODO(student): implement me! // 对于每个 frame_id,都需要标记两个元素。isPin 表示当前 frame 是正在被引用。 // ref 表示当前的 frame 最近是否被使用过。 struct clockItem { bool isPin; bool ref; }; // 为了线程安全需要加的锁 std::mutex latch; // 这个表示 ClockReplacer 的大小,Buffer Pool大小相同。 size_t victim_number; // clock 指针当前所指位置。 size_t clockHand; // clockItem 数组,数组下表表示 frame_id。 std::vector<clockItem> victimArray; };
-
Replacer
:追踪page
使用情况的抽象类。类中不包含page
的具体信息,只是为Buffer Pool Manager
服务,提供了一个可以替换的frame_id
。这个类包含了四个主要的函数。这里为了方便,将两个实现LRUReplacer
ClockReplacer
根据功能写在了一起,看的时候如果不舒服可以一次看一种实现方法。class Replacer { public: Replacer() = default; virtual ~Replacer() = default; /** * Remove the victim frame as defined by the replacement policy. * @param[out] frame_id id of frame that was removed, nullptr if no victim was found * @return true if a victim frame was found, false otherwise */ virtual bool Victim(frame_id_t *frame_id) = 0; /** * Pins a frame, indicating that it should not be victimized until it is unpinned. * @param frame_id the id of the frame to pin */ virtual void Pin(frame_id_t frame_id) = 0; /** * Unpins a frame, indicating that it can now be victimized. * @param frame_id the id of the frame to unpin */ virtual void Unpin(frame_id_t frame_id) = 0; /** @return the number of elements in the replacer that can be victimized */ virtual size_t Size() = 0; };
(1)
Victim
函数:-
用来移除可以被置换的
frame
。 其中参数frame_id
是【out】参数,也就是需要将victim
的frame_id
写给调用者。而返回值是bool
类型,如果成功删除了一个最近最少使用的frame
则返回true
,否则,返回false
。 -
LRUReplacer
实现:bool LRUReplacer::Victim(frame_id_t *frame_id) { // 为了线程安全考虑,每个函数都需要加 mutex 锁。 std::scoped_lock clock_lock{latch}; // 首先根据 LRUHash 是否为空去判断是否有页面需要置换。 if (LRUHash.empty()) { // 此时没有页面需要置换, 返回 false。 return false; } // 先获取 frame_id。 更新LRUHash,删除映射关系。 *frame_id = LRUList.back(); LRUHash.erase(*frame_id); // 然后从 LRUList 的尾部删除一个最近最久未被访问过的 frame。 LRUList.pop_back(); return true; }
-
ClockReplacer
实现:bool ClockReplacer::Victim(frame_id_t *frame_id) { std::scoped_lock clock_lock{latch}; // 如果当前页面没有全部被引用,还有可以被置换的页的时候。 for (; Size() > 0; clockHand++) { // 循环查找可以被置换的页。 if (clockHand == victimArray.size()) { clockHand = 0; } // 当前页面被引用,禁止置换。 if (victimArray[clockHand].isPin) { continue; } // isPin 为 false, ref 为 true,此时要更新 ref。 if (victimArray[clockHand].ref) { victimArray[clockHand].ref = false; continue; } // 我们需要的情况,isPin、ref 都为 false。 victimArray[clockHand].isPin = true; // 返回要置换的 frame_id *frame_id = clockHand++; // 能够被置换的页面减少 victim_number--; return true; } return false; }
(2)
Pin
函数:-
Pin
一个页面,是指当调用了一个页面的时候,那么这个页面由于有了引用,则不能从缓冲区中移除,一直到当前页面的引用为 0 ,才可以。 -
LRUReplacer
实现:- 也就是说,我们的
LRUList
中维护的是可以被victem
的页面,当Pin
的时候,如果在LRUList
中,则需要从中移除。
void LRUReplacer::Pin(frame_id_t frame_id) { std::scoped_lock clock_lock{latch}; // 如果当前页面被调用,则需要把它固定在 Buffer Pool 中。也就是说,需要把当前页面从待 victim 的 list 中移除。 // 需要查看当前 frame 是否在 LRUHash 中。 if (LRUHash.count(frame_id) != 0) { // 在 LRUHash 中,则移除。 LRUList.erase(LRUHash[frame_id]); LRUHash.erase(frame_id); } }
- 也就是说,我们的
-
ClockReplacer
实现:ClockReplacer
中capacity
维护能够被置换的页面的总的数量。victimArray
维护所有页面的信息。
void ClockReplacer::Pin(frame_id_t frame_id) { std::scoped_lock clock_lock{latch}; // 如果当前页面没有被引用,则更新 isPin 为 true。 // 代表能够被置换的页面减少。 if (!victimArray[frame_id].isPin) { victimArray[frame_id].isPin = true; victim_number--; } }
(3)
Unpin
函数:-
Unpin
一个页面,是指当一个页面的引用计数为0的时候,就把当前页面放入到待置换的数据结构中。 -
LRUReplacer
实现:void LRUReplacer::Unpin(frame_id_t frame_id) { std::scoped_lock clock_lock{latch}; // 需要判断当前页面是否在待置换的 list 中。 if (LRUHash.count(frame_id) == 0) { // 若不在,由于当前页面的引用为0,则把它加入到待置换的 list 头部中。 LRUList.push_front(frame_id); LRUHash[frame_id] = LRUList.begin(); } }
-
ClockReplacer
实现:void ClockReplacer::Unpin(frame_id_t frame_id) { std::scoped_lock clock_lock{latch}; // 如果当前页面之前被引用过,当引用计数为 0 时,需要更新 isPin 为 false。 // 因为被引用过,因此 ref 此时需要更新为 true。 // 能够被置换的页面增加。 if (victimArray[frame_id].isPin) { victimArray[frame_id].isPin = false; victimArray[frame_id].ref = true; victim_number++; } }
(4)
Size
函数:-
返回一个当前待置换的
fame
数量。 -
LRUReplacer
实现:size_t LRUReplacer::Size() { return LRUList.size(); }
-
ClockReplacer
实现:size_t ClockReplacer::Size() { return victim_number; }
(5)构造函数:
-
需要补充完整构造函数。
-
LRUReplacer
实现:LRUReplacer::LRUReplacer(size_t num_pages) { capacity = num_pages; }
-
ClockReplacer
实现:ClockReplacer::ClockReplacer(size_t num_pages) { victim_number = 0; clockHand = 0; // 初始化 victimArray。isPin 最开始为 true 是因为我们把页面加载到 Buffer pool 的时候,一定是因为 page 被引用了。 // ref 为 false,因为当前这个 page 的引用还没有结束。 for (size_t i = 0; i < num_pages; i++) { victimArray.emplace_back(clockItem{true, false}); } }
-
-
Buffer Pool Manager
:对缓冲池进行管理。其主要的数据结构是一个page
数组,下标表示frame_id
。还有一个哈希表,表示从page_id
到frame_id
的映射。-
FindPage
把查找一个frame
的操作单独拿出来,方便调用。bool BufferPoolManager::FindPage(frame_id_t *replaceFrameId) { // 1. 查看 free_list_,如果有空闲,则 Buffer Pool 没有满,从前面拿一个 frameId 返回。 if (!free_list_.empty()) { *replaceFrameId = free_list_.front(); free_list_.pop_front(); return true; } // 2. Buffer Pool 满了,则寻找是否有可以被替换的页,没有则返回 false。 if (!replacer_->Victim(replaceFrameId)) { return false; } // 3. 获得当前 replaceFrameId 对应的 page。 Page *page = &pages_[*replaceFrameId]; if (page->is_dirty_) { // 4. 刷新到磁盘。 disk_manager_->WritePage(page->page_id_, page->data_); } page_table_.erase(page->page_id_); return true; }
-
FetchPageImpl
Page *BufferPoolManager::FetchPageImpl(page_id_t page_id) { // 1. Search the page table for the requested page (P). // 1.1 If P exists, pin it and return it immediately. // 1.2 If P does not exist, find a replacement page (R) from either the free list or the replacer. // Note that pages are always found from the free list first. // 2. If R is dirty, write it back to the disk. // 3. Delete R from the page table and insert P. // 4. Update P's metadata, read in the page content from disk, and then return a pointer to P. std::scoped_lock lock{latch_}; // 1. 从 table 中寻找请求的页 std::unordered_map<page_id_t, frame_id_t>::iterator iter = page_table_.find(page_id); // 1.1 请求的页存在,pin,并返回。 if (iter != page_table_.end()) { // 找到了,pin,通知 Replacer replacer_->Pin(iter->second); pages_[iter->second].pin_count_++; return &pages_[iter->second]; } // 1.2 请求的页不存在,从 free_list 或 replacer 中找到一个替换页(R)。 frame_id_t replaceFrameId = INVALID_PAGE_ID; // 2|3. 没有找到替换的页,返回。 if (!FindPage(&replaceFrameId)) { return nullptr; } // 4. Update P 的元数据。 Page *newPage = &pages_[replaceFrameId]; page_table_[page_id] = replaceFrameId; newPage->page_id_ = page_id; newPage->pin_count_ = 1; newPage->is_dirty_ = false; disk_manager_->ReadPage(page_id, newPage->data_); // 通知 replacer。 replacer_->Pin(replaceFrameId); return newPage; }
-
UnpinPageImpl
bool BufferPoolManager::UnpinPageImpl(page_id_t page_id, bool is_dirty) { std::scoped_lock lock{latch_}; // 1. 查看该页是否在 Buffer Pool 中。 std::unordered_map<page_id_t, frame_id_t>::iterator iter = page_table_.find(page_id); if (iter == page_table_.end()) { return false; } // 2. 在 Buffer Pool 中,处理 count。 frame_id_t frameId = iter->second; Page *page = &pages_[frameId]; // 2.1 已经没有引用了,直接返回。 if (page->pin_count_ <= 0) { return false; } // 需要先判断是否需要减。 page->pin_count_--; // 2.2 更新 is_dirty_ if (is_dirty) { page->is_dirty_ = true; } // 3. 通知 Replacer。 if (page->pin_count_ == 0) { replacer_->Unpin(frameId); } return true; }
-
FlushPageImpl
bool BufferPoolManager::FlushPageImpl(page_id_t page_id) { // Make sure you call DiskManager::WritePage! std::scoped_lock lock{latch_}; // 1. 查看该页是否在 Buffer Pool 中, 是否无效。 std::unordered_map<page_id_t, frame_id_t>::iterator iter = page_table_.find(page_id); if (iter == page_table_.end() || page_id == INVALID_PAGE_ID) { return false; } // 2. 刷新到磁盘。 Page *page = &pages_[iter->second]; // 不管引用状态如何,都要刷新到磁盘。 disk_manager_->WritePage(page_id, page->data_); page->is_dirty_ = false; return true; }
-
NewPageImpl
Page *BufferPoolManager::NewPageImpl(page_id_t *page_id) { // 0. Make sure you call DiskManager::AllocatePage! // 1. If all the pages in the buffer pool are pinned, return nullptr. // 2. Pick a victim page P from either the free list or the replacer. Always pick from the free list first. // 3. Update P's metadata, zero out memory and add P to the page table. // 4. Set the page ID output parameter. Return a pointer to P. std::scoped_lock lock{latch_}; // 1|2. 挑选一个 victim page frame_id_t victimFrameId = -1; if (!FindPage(&victimFrameId)) { return nullptr; } // 0. 分配一个新的页号。 *page_id = disk_manager_->AllocatePage(); // 3. 更新 page 的元数据。 Page *newPage = &pages_[victimFrameId]; newPage->page_id_ = *page_id; newPage->is_dirty_ = false; newPage->pin_count_ = 1; // 添加到 page table。 page_table_[*page_id] = victimFrameId; replacer_->Pin(victimFrameId); // 创建的新页需要写回磁盘。 disk_manager_->WritePage(*page_id, newPage->data_); return newPage; }
-
deletePageImpl
bool BufferPoolManager::DeletePageImpl(page_id_t page_id) { // 0. Make sure you call DiskManager::DeallocatePage! // 1. Search the page table for the requested page (P). // 1. If P does not exist, return true. // 2. If P exists, but has a non-zero pin-count, return false. Someone is using the page. // 3. Otherwise, P can be deleted. Remove P from the page table, reset its metadata and return it to the free list. std::scoped_lock lock{latch_}; // 1. 查看页表中 page 是否存在。 std::unordered_map<page_id_t, frame_id_t>::iterator iter = page_table_.find(page_id); if (iter == page_table_.end()) { return true; } // 2. 查看是否有非 0 的引用 Page *deletePage = &pages_[iter->second]; if (deletePage->pin_count_ > 0) { return false; } // 3. 删除 P if (deletePage->is_dirty_) { disk_manager_->WritePage(deletePage->page_id_, deletePage->data_); } // 调用删除 disk_manager_->DeallocatePage(page_id); page_table_.erase(page_id); // 重置页面元数据 deletePage->page_id_ = INVALID_PAGE_ID; deletePage->pin_count_ = 0; deletePage->is_dirty_ = false; // 更新 free_list. free_list_.push_back(iter->second); return true; }
-
FlushAllPagesImpl
void BufferPoolManager::FlushAllPagesImpl() { // You can do it! std::scoped_lock lock{latch_}; for (size_t i = 0; i < pool_size_; i++) { disk_manager_->WritePage(pages_[i].page_id_, pages_[i].data_); } }
-
-
DiskManager
主要是读写磁盘操作。-
ReadPage
从数据库文件中读取一个page
。读取指定page_id
的数据到page_data
。page_data
是一个【out】输出参数。/** * Read the contents of the specified page into the given memory area. * Read a page from the database file. * @param page_id id of the page * @param[out] page_data output buffer */ void DiskManager::ReadPage(page_id_t page_id, char *page_data) { // 获取偏移位置。 int offset = page_id * PAGE_SIZE; // check if read beyond file length if (offset > GetFileSize(file_name_)) { //.... } else { // set read cursor to offset db_io_.seekp(offset); // 读取数据到 page_data。 db_io_.read(page_data, PAGE_SIZE); //.... // if file ends before reading PAGE_SIZE int read_count = db_io_.gcount(); if (read_count < PAGE_SIZE) { //.... // 数据不够则用 0 补齐。 memset(page_data + read_count, 0, PAGE_SIZE - read_count); } } }
-
WritePage
将数据写入到磁盘中。写入指定page_id
的page_data
。/** * Write the contents of the specified page into disk file. * Flush the entire log buffer into disk. * @param log_data raw log data * @param size size of log entry */ void DiskManager::WritePage(page_id_t page_id, const char *page_data) { // 获取偏移。 size_t offset = static_cast<size_t>(page_id) * PAGE_SIZE; // set write cursor to offset num_writes_ += 1; db_io_.seekp(offset); // 将 page_data 写入. db_io_.write(page_data, PAGE_SIZE); // check for I/O error if (db_io_.bad()) { LOG_DEBUG("I/O error while writing"); return; } // needs to flush to keep disk file in sync db_io_.flush(); }
-