CMU15445 lab1 - BUFFER POOL

TASK #1 - LRU REPLACEMENT POLICY

本任务为实现一个LRU页面置换策略,建立一个关于面向磁盘的数据库的基本的概念是很重要的,如下图:

截屏2022-10-01 20.45.06

从中可以看出,实际数据是持久化存储于磁盘之上的,执行引擎主要进行一些数据操作(读/写,也即对Page增删改查),而BufferPool则是介于执行引擎和磁盘之间,位于内存中,给执行引擎提供Page。由于存储器体系结构一般表现为内存容量远小于磁盘容量,因此BufferPool是无法加载整个db的所有Pages的,因此需要在合适的时机将Page写入磁盘中,LRU就决定了牺牲哪个Page(即将哪个Page写回到磁盘中),其中包含了局部性原理的思想。

在Buffer Pool中,Page是存放在frame中的,这是要注意的一个点(buffer pool就是一个能容放多个Page的vector)。

截屏2022-10-01 21.07.01

The size of the LRUReplacer is the same as buffer pool since it contains placeholders for all of the frames in the BufferPoolManager. However, not all the frames are considered as in the LRUReplacer. The LRUReplacer is initialized to have no frame in it. Then, only the newly unpinned ones will be considered in the LRUReplacer.

所要实现的接口主要是下面四个:

  • Victim(T*) : Remove the object that was accessed the least recently compared to all the elements being tracked by the Replacer, store its contents in the output parameter and return True. If the Replacer is empty return False.
  • Pin(T) : This method should be called after a page is pinned to a frame in the BufferPoolManager. It should remove the frame containing the pinned page from the LRUReplacer.
  • Unpin(T) : This method should be called when the pin_count of a page becomes 0. This method should add the frame containing the unpinned page to the LRUReplacer.
  • Size() : This method returns the number of frames that are currently in the LRUReplacer.

LRU的实现十分的简单,是经典的leetcode题,用list套一个unordered_map即可实现。

下面主要讲一下我对PinUnPin的理解:

  • Pin(T) : 将一个Page(frame)从LRU的list中剔除。即该Page(frame)被Buffer Pool所使用了,LRU不应该牺牲该页面。
  • Unpin(T) : 加入一个Page(frame)入LRU的list。即该页面Buffer Pool目前没人使用了,LRU根据策略决定该页面的去留。
  • Victim(T*) :意思很直接,LRU根据规则(最近最少使用)有选择性的牺牲一个页面(frame)。

并发的话,直接加大锁就好了。std::lock_guard是一种RAII的加锁方式,可以不用unlock(在析构的时候unlock),比较方便。给出Victim的实现方法,其他的应 Prof. Pavlo 要求就不放出来了。

bool LRUReplacer::Victim(frame_id_t *frame_id) {
  std::lock_guard<std::mutex> lock(latch_);
  if (id2iter_.empty()) {
    return false;
  }
  auto deleting_id = lru_list_.back();
  lru_list_.pop_back();
  id2iter_.erase(deleting_id);
  *frame_id = deleting_id;
  return true;
}

TASK #2 - BUFFER POOL MANAGER

第二个任务为构造一个Buffer Pool。

The BufferPoolManager is responsible for fetching database pages from the DiskManager and storing them in memory. The BufferPoolManager can also write dirty pages out to disk when it is either explicitly instructed to do so or when it needs to evict a page to make space for a new page.

实现以下几个接口:

  • FetchPageImpl(page_id)
  • NewPageImpl(page_id)
  • UnpinPageImpl(page_id, is_dirty)
  • FlushPageImpl(page_id)
  • DeletePageImpl(page_id)
  • FlushAllPagesImpl()

(其实可以先通过测试程序了解这几个接口怎么用的,然后再去实现会比较好!)

  • NewPageImpl(page_id):新建一个Page。
  • FetchPageImpl(page_id):获取一个Page。
  • UnpinPageImpl(page_id, is_dirty):解除对某个Page的使用(别的进程可能还在使用,pin_count为0的时候可以删除)
  • DeletePageImpl(page_id):删除一个Page。
  • FlushPageImpl(page_id):强制将某个Page写盘。
  • FlushAllPagesImpl():将所有Page写盘。

这个task其实本质上就是考验对下面两个点的理解,根据提示看看DiskManager 的API是比较好实现的:

  • Dirty Flag :当该flag为真时,该页被写过了,要写回磁盘。
  • Pin/Reference Counter:引用计数,当该计数为0时,将对应frame加入LRU中;当该计数不为0时,将对应frame从LRU中删除(即不参与LRU的替换)。

该task有几个坑需要注意一下:

  1. 重复UnpinPageImpl,但is_dirty标志不同。

    • 不是简单的赋值设置is_dirty标志,而是累计,即或一下。
    • page->is_dirty_ |= is_dirty;
  2. New完一个Page后,其pin_count为1,因此不要将这个Page放入LRU。

    • replacer_->Pin(fid);
  3. New完一个Page后,要立即刷盘。可能会有new完以后unpin(false)的情况,不刷盘这一页就丢失了

    • disk_manager_->WritePage(new_page->GetPageId(), new_page->GetData());
  4. 获取frame时,先从free list获取,再从lru中获取。

    • /**
       * @brief get a free page from free_list or lru_list
       *
       * @return frame_id_t frame id, -1 is error
       */
      frame_id_t BufferPoolManager::get_free_frame() {
        frame_id_t frame_id = -1;
        if (!free_list_.empty()) {
          frame_id = free_list_.front();
          free_list_.pop_front();
      
        } else {
          replacer_->Victim(&frame_id);
        }
        return frame_id;
      }
      
  5. 删除一个Page时,要保证free list和LRU中只存在一个fid,而不是两边都有。

    • replacer_->Pin(fid);
    • free_list_.emplace_back(fid);

由于是多线程的程序,可以多跑几次测试一下,通过日志排查出错的原因。

#!/usr/bin/env bash

trap 'exit 1' INT

echo "Running test $1 for $2 iters"
for i in $(seq 1 $2); do
    echo -ne "\r$i / $2"
    LOG="$i.txt"
    # Failed go test return nonzero exit codes
    $1 &> $LOG
    if [[ $? -eq 0 ]]; then
        rm $LOG
    else
        echo "Failed at iter $i, saving log at $LOG"
    fi
done

(gradescope上测试要是失败了可以直接偷测试文件,逃

若有概念不理解的可以翻翻课件

posted @ 2022-10-01 21:51  zju_cxl  阅读(127)  评论(0编辑  收藏  举报