CMU 15-445 2021 Project1

缓冲池设计
image.png

课程项目链接: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();
  }
}
posted @ 2023-08-01 14:09  Zkun6  阅读(27)  评论(0)    收藏  举报