个人笔记
构建一个新的面向磁盘的存储管理器,这样的存储管理器假定数据库的主要存储位置在磁盘上。
在存储管理器中实现缓冲池。缓冲池负责将 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
来腾出空间给从磁盘复制的新物理页时决定删除哪个对象。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
的思路:
Clock
策略:
Clock
也是页面置换算法的一种。Clock
置换算法分为两种,一种是简单的置换算法,与 LRU
算法类似。另一种是改进型的,相比于前一种,减少了磁盘IO,性能更加高效。Clock
的思想是先将内存中的所有页面想象成一个环形队列,通过维护一个访问位,每次更新的时候,如果访问位为 0,表示最近没有被访问,则可以置换;否则,将访问位置 0,继续寻找。Clock
设计思路:
Clock
设计思路。维护一个访问位数组和一个指针。每次当成环形数组循环查找当前需要被置换的页面。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();
}