CMU 15-445 2021 Project1
缓冲池设计

课程项目链接:CMU 15-445 Project1
部分核心的代码本文不会给出(便于大家自己思考、收获)
Task 1
实现LRU (Least recently used,最近最少使用)页面置换器。由于无法预测各页面将来的使用情况,只能利用“最近的过去”作为“最近的将来”的近似,因此,LRU算法就是将最近最久未使用的页面予以淘汰。
src/include/buffer/lru_replacer.h定义了一个LRUReplacer 类,在src/buffer/lru_replacer.cpp中实现对应的方法。
您将需要实现以下方法:
- Victim(frame_id_t*):删除与跟踪的所有其他元素相比最近访问最少的对象,将其内容存储在输出参数中并返回 True。如果为空,则返回 False。
- Pin(frame_id_t):在将页面固定到框架后,BufferPoolManager 调用此方法。它应从 LRUReplacer 中删除包含固定页面的框架。
- Unpin(frame_id_t):当页面变为 0 时,应调用此方法。此方法应将包含未固定页面的框架添加到 .pin_countLRUReplacer
- Size():此方法返回当前位于 LRUReplacer 中的帧数。
Note:Page和Frame本质是一个东西。page_id是Page的唯一标识,frame_id是page在 BufferPoolManager 中page数组的索引下标,所以两者本质上都是为了找到需要的页面的标识,frame_id的设计是为了在 BufferPoolManager 中更方便查找Page。
设计成员变量
src/include/buffer/lru_replacer.h
namespace bustub {
class LRUReplacer : public Replacer {
private:
// 替换器的最大容量
size_t capacity_;
// 可替换的页面列表
std::list<frame_id_t> lists_;
// 记录列表中有哪些帧
std::unordered_map<frame_id_t, int> frame_map_;
// 并发锁
std::mutex latch_;
}
构造函数
`src/buffer/lru_replacer.cpp`
LRUReplacer::LRUReplacer(size_t num_pages) : capacity_(num_pages) {
this->latch_.lock();
this->frame_map_.clear();
this->lists_.clear();
this->latch_.unlock();
}
Victim
功能是返回受害者(需要被置换的页面),被置换的页面会在 BufferPoolManager 中被删除。
bool LRUReplacer::Victim(frame_id_t *frame_id) {
// 候选列表为空说明没有可以选择的替换页面,返回false
// 上锁
// 选择队列尾部的页面帧
// 将其剔除队列
// 删除映射记录
// 解锁
}
Pin
调用意味着使用了对应的页面,所以不能被置换器选中,所以需要删除在置换器中的记录。
void LRUReplacer::Pin(frame_id_t frame_id) {
// 列表若没有frame_id的记录则直接返回
// 若有对应记录则删除队列中的页面帧
}
Unpin
将不用的页面放入替换列表中。
void LRUReplacer::Unpin(frame_id_t frame_id) {
// 如果列表大小达到容量上限,则需要删除列表头对应的页面帧
}
Task 2
完善实现 BufferPoolManagerInstance 类
BufferPoolManagerInstance 负责从 DiskManager 中获取数据库页并将其存储在内存中。当 BufferPoolManagerInstance 明确指示它这样做时,或者当它需要逐出页面以便为新页面腾出空间时,也可以将脏页(即被更改写入的页)写出到磁盘。
为了确保实现与系统的其余部分正常工作,代码提供一些已经填写的功能。不需要实现实际读取数据并将数据写入磁盘的代码,代码已经实现了该功能。
系统中所有内存中的页面都由Page对象表示。BufferPoolManagerInstance 不需要理解这些页面的内容。但是,对于系统开发人员来说,重要的是要理解Page对象只是缓冲池中内存的容器,因此并不是特定于唯一页面的。也就是说,每个Page对象都包含一块内存,DiskManager会将其用作复制从磁盘读取的物理页面内容的位置。BufferPoolManagerInstance将在数据来回移动到磁盘时重用相同的Page对象来存储数据。这意味着在系统的整个生命周期中,同一个页面对象可能包含不同的物理页面。Page对象的标识符(Page_id)跟踪它包含的物理页面;如果Page对象不包含物理页面,则其Page_id必须设置为 INVALID_Page_id。
每个Page对象还为该页面的线程数维护一个计数器“pin_count_”。不允许您的BufferPoolManagerInstance释放已固定的页面。每个Page对象还跟踪它是否脏。您的工作是记录页面在取消固定之前是否被修改。BufferPoolManagerInstance必须将脏页的内容写回磁盘,然后才能重用该对象。
需要在源文件 (src/buffer/buffer_pool_manager_instance.cpp) 的头文件 (src/include/buffer/buffer_pool_manager_instance.h) 中实现以下函数:
- FetchPgImp(page_id)
- UnpinPgImp(page_id, is_dirty)
- FlushPgImp(page_id)
- NewPgImp(page_id)
- DeletePgImp(page_id)
- FlushAllPagesImpl()
成员变量
namespace bustub {
class BufferPoolManagerInstance : public BufferPoolManager {
...
protected:
...
// 缓冲池大小(最大可放置页面的数量)
const size_t pool_size_;
// 缓冲池管理器的数量(并发使用缓冲池会用到,否则默认一个管理器)
const uint32_t num_instances_ = 1;
// 下一个新页面的page_id
const uint32_t instance_index_ = 0;
std::atomic<page_id_t> next_page_id_ = instance_index_;
// 缓存的所有页面
Page *pages_;
// 磁盘管理器,用于读取写入磁盘的页面
DiskManager *disk_manager_ __attribute__((__unused__));
LogManager *log_manager_ __attribute__((__unused__));
// page_id到frame_id的映射表
std::unordered_map<page_id_t, frame_id_t> page_table_;
// 选出可以被替换页面的置换器
Replacer *replacer_;
// 空闲可用的frame列表
std::list<frame_id_t> free_list_;
std::mutex latch_;
}
FetchPgImp
功能:将页面从磁盘加载到内存
Page *BufferPoolManagerInstance::FetchPgImp(page_id_t page_id) {
std::lock_guard<std::mutex> guard(this->latch_);
// 1. 在缓冲中查询请求的页面
// 1.1 如果页面存在则Pin它,并返回它。
// 1.2 如果没找到页面,则查看free_list_是否有空闲的帧用于引入页面,没有则请求置换器给出置换页面,如果都失败则返回空
// 2. 如果选择的旧的被置换的页面是脏页,则再删除前需要写入磁盘
// 3. 再映射表中删除旧页,引入新页。
// 4. 更新新页面的元数据(page_id, pin_count_, data_),其中data_需要从磁盘中读取。最后返回新页对象指针。
}
UnpinPgImp
功能:表示当前进程任务不再使用此页。
Note:脏页的属性是单向的,即缓冲区的页面只能从不脏->脏,所以再更改is_dirty_属性前要进行判断。
bool BufferPoolManagerInstance::UnpinPgImp(page_id_t page_id, bool is_dirty) {
std::lock_guard<std::mutex> guard(this->latch_);
// 如果缓冲区没有此页则失败,返回false
if (this->page_table_.find(page_id) == this->page_table_.end()) {
return false;
}
frame_id_t frame_id = this->page_table_[page_id];
Page *page = &this->pages_[frame_id];
// 如果is_dirty为false则不对页面属性进行变更
if (is_dirty) {
page->is_dirty_ = is_dirty;
}
// 如果页面的依赖进程为0,说明页面已经被Unpin过了,直接返回
if (page->GetPinCount() <= 0) {
return true;
}
// 否则,页面依赖进程数减一,若为0则进行Unpin
page->pin_count_--;
if (page->GetPinCount() == 0) {
this->replacer_->Unpin(frame_id);
}
return true;
}
FlushPgImp
功能:将页面写回磁盘中 (Unpinned的页面)
bool BufferPoolManagerInstance::FlushPgImp(page_id_t page_id) {
std::lock_guard<std::mutex> guard(latch_);
// 保证page_id合法且在缓冲池中
// 重置属性
page->is_dirty_ = false;
page->pin_count_ = 0;
// 数据写入磁盘
this->disk_manager_->WritePage(page_id, page->GetData());
// 删除在置换器的页面,以免被其他任务选中替换
// 删除page_id记录,将帧放回空闲列表
return true;
}
FlushAllPgsImp
void BufferPoolManagerInstance::FlushAllPgsImp() {
// 遍历所有page
for (size_t i = 0; i < pool_size_; i++) {
this->FlushPgImp(this->pages_[i].GetPageId());
}
}
NewPgImp
分配一个新的物理页,并将page_id由参数返回
Page *BufferPoolManagerInstance::NewPgImp(page_id_t *page_id) {
std::lock_guard<std::mutex> guard(latch_);
// 1. 如果缓冲区所有的页面都在被使用,则不能新建页面,返回空
// 2. 分配找到可以使用的frame_id给新的页
frame_id_t free_frame_id;
if (this->free_list_.empty()) {
bool has_victim = this->replacer_->Victim(&free_frame_id);
// 特判:物理页对应的位置可能是空的,这时不需要写回
if (!has_victim) {
return nullptr;
}
} else {
free_frame_id = free_list_.back();
this->free_list_.pop_back();
}
// 3. 如果旧的页面是脏页,则写回磁盘;删除旧页信息
// 4. 分配新的页面,page_id通过内部特定的规则生成,初始化元数据
// 加入映射表信息,将帧设置为pinned状态
return victim_page;
}
DeletePgImp
删除指定页面
bool BufferPoolManagerInstance::DeletePgImp(page_id_t page_id) {
std::lock_guard<std::mutex> guard(this->latch_);
// 0. Make sure you call DeallocatePage!
// 1. Search the page table for the requested page (P).
// 1. 若缓冲区无此页则返回true
// 2. 若此页pin_count_不为0,说明有其他任务在使用,不能删除
// 3. 按步骤删除页面,消去记录,释放帧
}
Task 3
实现并行缓冲池管理器 ParallelBufferPoolManager。
单个缓冲池管理器实例需要采用闩锁才能确保线程安全。这可能会导致大量争用,因为每个线程在与缓冲池交互时都会争夺单个闩锁。一种可能的解决方案是系统中有多个缓冲池,每个缓冲池都有自己的闩锁。
我们使用给定的页面 ID 来确定要使用的缓冲池。如果我们有很多 BufferPoolManagerInstance,那么我们需要一些方法来将给定的页面ID映射到[0,num_instances]范围内的数字。对于这个项目,我们将使用取模运算符 page_id mod num_instances,将给定的page_id映射到正确的范围。
需要在源文件 (src/buffer/parallel_buffer_pool_manager.cpp) 的头文件 (src/include/buffer/parallel_buffer_pool_manager.h) 中实现以下函数:
- ParallelBufferPoolManager(num_instances, pool_size, disk_manager, log_manager)
- ~ParallelBufferPoolManager()
- GetPoolSize()
- GetBufferPoolManager(page_id)
- FetchPgImp(page_id)
- UnpinPgImp(page_id, is_dirty)
- FlushPgImp(page_id)
- NewPgImp(page_id)
- DeletePgImp(page_id)
- FlushAllPagesImpl()
成员变量
namespace bustub {
class ParallelBufferPoolManager : public BufferPoolManager {
...
private:
// 缓冲池集合
std::vector<BufferPoolManagerInstance *> buffer_pool_;
// 缓冲池数量
size_t num_instances_;
// 每个缓冲池的大小
size_t pool_size_;
// 下一个尝试新建页的缓冲池管理器的遍历起点(防止每次从头遍历)
size_t instance_index_;
DiskManager *disk_manager_ __attribute__((__unused__));;
LogManager *log_manager_ __attribute__((__unused__));;
std::mutex latch_;
};
}
构造函数
ParallelBufferPoolManager::ParallelBufferPoolManager(size_t num_instances, size_t pool_size, DiskManager *disk_manager,
LogManager *log_manager) {
this->num_instances_ = num_instances;
this->pool_size_ = pool_size;
this->disk_manager_ = disk_manager;
this->log_manager_ = log_manager;
this->instance_index_ = 0;
// 创建num_instances_个缓冲池放入vector
for (size_t i = 0; i < this->num_instances_; i++) {
BufferPoolManagerInstance *bpm = new BufferPoolManagerInstance(
this->pool_size_, this->num_instances_, i, this->disk_manager_, this->log_manager_);
this->buffer_pool_.push_back(bpm);
}
}
GetBufferPoolManager
用于根据page_id查找对应的缓冲池管理器
BufferPoolManager *ParallelBufferPoolManager::GetBufferPoolManager(page_id_t page_id) {
if (page_id == INVALID_PAGE_ID) {
return nullptr;
}
// 通过取模运算分配缓冲池
size_t idx = page_id % this->num_instances_;
return this->buffer_pool_[idx];
}
FetchPgImp
将页面写入磁盘
Page *ParallelBufferPoolManager::FetchPgImp(page_id_t page_id) {
std::lock_guard<std::mutex> guard(this->latch_);
// 找到对应缓冲池调用对应方法
BufferPoolManager *bpm = this->GetBufferPoolManager(page_id);
if (bpm != nullptr) {
return bpm->FetchPage(page_id);
}
return nullptr;
}
UnpinPgImp, FlushPgImp, DeletePgImp 与此类似
NewPgImp
分配新的页面
Page *ParallelBufferPoolManager::NewPgImp(page_id_t *page_id) {
std::lock_guard<std::mutex> guard(this->latch_);
// 1. 从instance_index_下标对应的缓冲池开始创建页面,成功则退出循环,若全部缓冲池遍历均不能创建,则返回空指针
Page *page = nullptr;
for (size_t i = 0; i < this->num_instances_; i++) {
BufferPoolManagerInstance *bpm = this->buffer_pool_[(instance_index_ + i) % this->num_instances_];
page = bpm->NewPage(page_id);
if (page != nullptr) {
break;
}
}
if (page == nullptr) {
return nullptr;
}
// 2. 更新instance_index_,提高每次新建页面的效率;返回新建的页面
this->instance_index_ = *page_id % num_instances_ + 1;
return page;
}
FlushAllPgsImp
void ParallelBufferPoolManager::FlushAllPgsImp() {
// flush all pages from all BufferPoolManagerInstances
for (auto bpm : this->buffer_pool_) {
bpm->FlushAllPages();
}
}

项目1
浙公网安备 33010602011771号