CMU15-445 Buffer Pool

LRU替换策略

此组件负责跟踪缓冲池中的页面使用情况。在src/include/buffer/lru_replacer.h中实现名为LRUReplacer的子类,并在src/buffer/lru_replacer.cpp中实现其相应的实现文件。LRUReplacer扩展了包含函数规范的抽象类Replacersrc/include/buffer/Replacer.h)。
LRUReplacer的最大页数与缓冲池的大小相同,因为它包含BufferPoolManager中所有帧的占位符。然而,在任何给定的时刻,并非所有的帧都被认为是在LRUReplacer中。LRUReplacer初始化时没有帧。然后,只有新未固定的帧才会被视为在LRUReplacer中。
需要实现以下方法:
Victim(frame_id_t*):删除最早加入replacer的页帧,并返回True。如果Replacer为空,则返回False。
Pin(frame_id_t):它应该从LRUReplacer中删除包含固定页面的帧。
Unpin(frame_id_t):当页面的pin_count变为0时,调用这个方法页帧添加到LRUReplacer
Size():此方法返回LRUReplacer中当前的帧数。

  1. 数据结构

    LRU替换策略需要在空间满时将最久没被使用的页替换出去,选择用双向链表List存储没被使用的页,队首存储的是最久没被使用的页,队尾则是最近刚被放弃使用的页,正在使用的页不被存储在List里。当需要使用没被使用的页时,可以从List删除该页的结点。为了提高删除的速度,可以用map存储每个页对应结点的迭代器,实现O(1)时间复杂度的快速删除。

    private:
      // TODO(student): implement me!
      /** 缓冲区中页的数量 */
      size_t num_pages_;
      /** 存放可以被换出的帧号 */
      std::list<frame_id_t> lru_frame_list_;
      /** 保存每个可换出帧对应的list里的迭代器 */
      std::unordered_map<frame_id_t, std::list<frame_id_t>::iterator> iterator_table_;
      /** 保护共享数据frame_list和iterator_table*/
      std::mutex latch_;
    
  2. Victim函数

    调用这个函数表示需要选择一个页被替换出去,删除队首的结点和map中相应的迭代器即可。当没有没被使用的页时,需要返回false。为了多线程并发时程序仍然正确,需要加锁。

    bool LRUReplacer::Victim(frame_id_t *frame_id) {
      latch_.lock();
      if (lru_frame_list_.empty()) {
        latch_.unlock();
        return false;
      }
      iterator_table_.erase(lru_frame_list_.front());
      *frame_id = lru_frame_list_.front();
      lru_frame_list_.pop_front();
      latch_.unlock();
      return true;
    }
    
  3. Pin函数

    当没被使用的页需要重新使用时,该页就不能被替换出去,从List和Map中删除该页就行。为了多线程并发时程序仍然正确,需要加锁。

    void LRUReplacer::Pin(frame_id_t frame_id) {
      latch_.lock();
      if (iterator_table_.find(frame_id) == iterator_table_.end()) {
        latch_.unlock();
        return;
      }
      auto iterator = iterator_table_[frame_id];
      lru_frame_list_.erase(iterator);
      iterator_table_.erase(frame_id);
      latch_.unlock();
    }
    
  4. Unpin函数

    当一个页pin_count变成0,表示该页不需要被使用了,可以被替换出去,所以将该页加到List的队尾。

    注意:如果一个页已经在List时,调用unpin函数时不需要把它追加到队尾,什么都不要做。

    为了多线程并发时程序仍然正确,需要加锁。

    void LRUReplacer::Unpin(frame_id_t frame_id) {
      latch_.lock();
      if (iterator_table_.find(frame_id) != iterator_table_.end() || lru_frame_list_.size() > num_pages_) {
        latch_.unlock();
        return;
      }
      lru_frame_list_.push_back(frame_id);
      auto iterator = lru_frame_list_.end();
      --iterator;
      iterator_table_[frame_id] = iterator;
      latch_.unlock();
    }
    

缓冲池管理器实例

接下来,您需要在系统中实现缓冲池管理器(BufferPoolManagerInstance)。BufferPoolManagerInstance负责从DiskManager获取数据库页并将其存储在内存中。当显式指示BufferPoolManagerInstance将脏页写入磁盘时,或者当它需要逐出页以为新页留出空间时,BufferPoolManagerInstance也可以将脏页写入磁盘。
为了确保您的实现与系统的其余部分正确配合,我们将为您提供一些已经填写的功能。您也不需要实现实际将数据读写到磁盘的代码(在我们的实现中称为DiskManager)。我们将为您提供该功能。
系统中的所有内存页都由页对象表示。BufferPoolManagerInstance不需要理解这些页面的内容。但是,作为系统开发人员,了解页面对象只是缓冲池中内存的容器,因此并不特定于唯一页面,这一点很重要。也就是说,每个页面对象都包含一个内存块,DiskManager将使用该内存块作为位置来复制从磁盘读取的物理页面的内容。BufferPoolManagerInstance将重用相同的页面对象来存储在磁盘上来回移动的数据。这意味着在系统的整个生命周期中,同一页面对象可能包含不同的物理页面。页面对象的标识符(Page_id)跟踪它包含的物理页面;如果页面对象不包含物理页面,则其页面id必须设置为无效的页面id
每个页面对象还维护一个计数器,用于“固定”该页面的线程数。不允许您的BufferPoolManagerInstance释放已固定的页面。每个页面对象还跟踪它是否脏。您的工作是记录页面在取消固定之前是否已修改。BufferPoolManagerInstance必须将脏页的内容写回磁盘,然后才能重用该对象。
您的BufferPoolManagerInstance实现将使用您在此分配的前面步骤中创建的LRUReplacer类。它将使用LRUReplacer跟踪访问页面对象的时间,以便在必须释放一个帧以腾出空间从磁盘复制新的物理页面时,决定退出哪个对象。

需要实现以下函数:

  • FetchPgImp(page_id)

    Fetch the requested page from the buffer pool.

  • UnpinPgImp(page_id, is_dirty)

    Unpin the target page from the buffer pool.

  • FlushPgImp(page_id)

    Flushes the target page to disk.

  • NewPgImp(page_id)

    Creates a new page in the buffer pool.

  • DeletePgImp(page_id)

    Deletes a page from the buffer pool.

  • FlushAllPagesImpl()

    Flushes all the pages in the buffer pool to disk.

下面两个代码段列出了BufferPoolManagerInstance中定义的数据成员和构造函数。构造函数分配了pool_size个page,这些page对象(物理页)被重复利用,来存储不同的逻辑页。free_list中存放的是目前空闲(没有逻辑页映射到此页)的物理页页帧,构造函数将pages_ 数组的下标存入free_list中,因为当然开始时物理页都是没被映射的。因为逻辑页的个数会远大于物理页的个数,所以需要用replacer寻找可以替换的物理页,就是把原来的物理页内容写回磁盘,把新的逻辑页的内容从磁盘读出来。逻辑页会在不同的时间映射到不同的物理页,用page_table建立逻辑页页号和物理页页帧的映射。disk_manager_用来根据逻辑页号来存取数据。互斥锁latch用来保证所有的操作都是线程安全的。其他数据成员和task3相关(除了log_manager)。

/** Number of pages in the buffer pool. */
  const size_t pool_size_;
  /** How many instances are in the parallel BPM (if present, otherwise just 1 BPI) */
  const uint32_t num_instances_ = 1;
  /** Index of this BPI in the parallel BPM (if present, otherwise just 0) */
  const uint32_t instance_index_ = 0;
  /** Each BPI maintains its own counter for page_ids to hand out, must ensure they mod back to its instance_index_ */
  std::atomic<page_id_t> next_page_id_ = instance_index_;

  /** Array of buffer pool pages. */
  Page *pages_;
  /** Pointer to the disk manager. */
  DiskManager *disk_manager_ __attribute__((__unused__));
  /** Pointer to the log manager. */
  LogManager *log_manager_ __attribute__((__unused__));
  /** Page table for keeping track of buffer pool pages. */
  std::unordered_map<page_id_t, frame_id_t> page_table_;
  /** Replacer to find unpinned pages for replacement. */
  Replacer *replacer_;
  /** List of free pages. */
  std::list<frame_id_t> free_list_;
  /** This latch protects shared data structures. We recommend updating this comment to describe what it protects. */
  std::mutex latch_;
BufferPoolManagerInstance::BufferPoolManagerInstance(size_t pool_size, uint32_t num_instances, uint32_t instance_index, DiskManager *disk_manager, LogManager *log_manager)
    : pool_size_(pool_size),
      num_instances_(num_instances),
      instance_index_(instance_index),
      next_page_id_(instance_index),
      disk_manager_(disk_manager),
      log_manager_(log_manager) {
  BUSTUB_ASSERT(num_instances > 0, "If BPI is not part of a pool, then the pool size should just be 1");
  BUSTUB_ASSERT(
      instance_index < num_instances,
      "BPI index cannot be greater than the number of BPIs in the pool. In non-parallel case, index should just be 1.");
  // We allocate a consecutive memory space for the buffer pool.
  pages_ = new Page[pool_size_];
  replacer_ = new LRUReplacer(pool_size);

  // Initially, every page is in the free list.
  for (size_t i = 0; i < pool_size_; ++i) {
    free_list_.emplace_back(static_cast<int>(i));
  }
}
获得可用物理页 GetVictimPage(frame_id_t *frame_id)
  1. 如果bpmi的free_list为空

    • 就使用replacer找到一个最久为使用的pin_conut为0物理页进行替换,脏页要被写回磁盘

    • 如果replacer中没有可以替换的帧,表示所以的物理帧都在被使用,return false

  2. free_list不为空,选择第一个物理页帧

Page *BufferPoolManagerInstance::GetVictimPage(frame_id_t *frame_id) {
    Page *page = nullptr;
    if (free_list_.empty()) {
        bool has_victim = replacer_->Victim(frame_id);
        if (!has_victim) {
            return nullptr;
        }
        page = &pages_[*frame_id];
        page_table_.erase(page->GetPageId());
        if (page->IsDirty()) {
            disk_manager_->WritePage(page->GetPageId(), page->GetData());
            page->is_dirty_ = false;
        }
    } else {
        *frame_id = free_list_.front();
        free_list_.pop_front();
        page = &pages_[*frame_id];
    }
    page->ResetMemory();
    return page;
}
FetchPgImp(page_id)

此函数功能是根据逻辑页的页号得到页指针

  1. 如果page_table中存在页号的键,表示该逻辑页存在于内存中,返回该页对应的物理页即可,需要增加pin_count,此时还需要Pin对应的物理页帧,因为可能存在一种情况。当该逻辑页映射到该物理页而并没有在使用该物理页时(pin_count = 0),该物理页被加入到replacer里,属于可以被牺牲。现在该逻辑页从新使用该物理页时,需要把该物理页从replacer中删除,因为该物理页已经不可被牺牲。
  2. 没有该页号的键表示该逻辑页不在内存中,需要把它从磁盘读入到内存。此时就需要获得一个可牺牲的(pin_count=0)的物理页,增加该物理页的pin_count,并把更新数据成员,把该逻辑页的内存从磁盘读入内存(读入到该物理页)。
Page *BufferPoolManagerInstance::FetchPgImp(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.
    Page *page = nullptr;
    frame_id_t frame_id;
    bool page_exists = false;

    latch_.lock();
    if (page_table_.find(page_id) != page_table_.end()) {
        frame_id = page_table_[page_id];
        page = &pages_[frame_id];
        replacer_->Pin(frame_id);
        page_exists = true;
    } else {
        page = GetVictimPage(&frame_id);
    }
    if (!page) {
        latch_.unlock();
        return nullptr;
    }

    page->page_id_ = page_id;
    page_table_.insert({page_id, frame_id});
    ++page->pin_count_;
    if (!page_exists) {
        disk_manager_->ReadPage(page_id, page->GetData());
    }
    latch_.unlock();
    return page;
}
UnpinPgImp(page_id, is_dirty)

函数功能是当逻辑页使用结束后减少映射的物理页的被引用数。

如果被页面数据被修改过,把物理页属性设为dirty。页面引用数减一,当引用数为0时,该页面可以被牺牲(替换)。

bool BufferPoolManagerInstance::UnpinPgImp(page_id_t page_id, bool is_dirty) {
    frame_id_t frame_id;
    latch_.lock();
    if (page_table_.find(page_id) == page_table_.end()) {
        latch_.unlock();
        return false;
    }
    frame_id = page_table_[page_id];
    Page *page = &pages_[frame_id];
    page->is_dirty_ = is_dirty || page->is_dirty_;
    if (page->GetPinCount() > 0) {
        --page->pin_count_;
    }
    if (page->GetPinCount() == 0) {
        replacer_->Unpin(frame_id);
    }
    latch_.unlock();
    return true;
}
FlushPgImp(page_id_t page_id)

函数把脏页写会磁盘

bool BufferPoolManagerInstance::FlushPgImp(page_id_t page_id) {
    // Make sure you call DiskManager::WritePage!
    // disk_manager_->WritePage()
    frame_id_t frame_id;

    latch_.lock();
    if (page_id == INVALID_PAGE_ID ||
        page_table_.find(page_id) == page_table_.end()) {
        latch_.unlock();
        return false;
    }
    frame_id = page_table_[page_id];
    if (pages_[frame_id].IsDirty()) {
        disk_manager_->WritePage(page_id, pages_[frame_id].GetData());
    }
    latch_.unlock();
    return true;
}
NewPgImp(page_id_t *page_id)

函数会生成一个新的逻辑页

  • 如果没有可以替换的物理页,pin_count全部大于0,就无法为新的逻辑页分配物理页,所以生成失败
  • 有的话,使用AllocatePage获得一个新的逻辑页号,然后就是建立逻辑页与物理页的映射,更新页面属性等等
Page *BufferPoolManagerInstance::NewPgImp(page_id_t *page_id) {
    // 0.   Make sure you call 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.
    frame_id_t frame_id;
    Page *page = nullptr;

    latch_.lock();
    page = GetVictimPage(&frame_id);
    if (!page) {
        *page_id = INVALID_PAGE_ID;
        latch_.unlock();
        return nullptr;
    }
    *page_id = AllocatePage();
    page_table_.insert({*page_id, frame_id});
    page->page_id_ = *page_id;
    page->ResetMemory();
    page->pin_count_ = 1;
    latch_.unlock();
    return page;
}
DeletePgImp(page_id_t page_id)

删除该逻辑页的映射,并把对应的物理页加入free_list中。

  • 只有当该逻辑页存在映射,才会有对应的物理页,并且对应物理页pin_count等于0才能释放该物理页,显然当别的逻辑页还在使用这个物理页时不能释放这个物理页。
  • 删除映射,写回脏页,还原页面成员属性。
  • 最重要的一点,还要把这个物理页从replacer中删去,因为可以被删除的页pin_count一定为0,所以这个页一定在replacer中。

注意:我之前没有调用replacer_->Pin(frame_id);这个函数但同样过了所有的case,但是不调用确实有问题。

如果不调用pin,考虑一种情况:

  1. 创建一个含有1个物理页的bpm
  2. new一个页面然后delete这个页面,这时free_list和replacer里就会存在相同的物理页帧
  3. 在new一个page,用到free_list中的物理页,再new一个page时,会从replacer里得到一个页帧。显然出现了错误。

不知道有没有其他人碰到过这个问题,我把测试用例贴在下面,大家可以测试一下自己的实现。

bool BufferPoolManagerInstance::DeletePgImp(page_id_t page_id) {
  // 0.   Make sure you call 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.
  frame_id_t frame_id;
  latch_.lock();
  if (page_table_.find(page_id) == page_table_.end()) {
    latch_.unlock();
    return true;
  }
  frame_id = page_table_[page_id];
  Page *page = &pages_[frame_id];
  if (page->GetPinCount() > 0) {
    latch_.unlock();
    return false;
  }
  page_table_.erase(page->GetPageId());
  if (page->IsDirty()) {
    disk_manager_->WritePage(page->GetPageId(), page->GetData());
    page->is_dirty_ = false;
  }
  DeallocatePage(page_id);
  
  // pin这个函数需要调用
  replacer_->Pin(frame_id);

  page->page_id_ = INVALID_PAGE_ID;
  free_list_.push_back(frame_id);
  latch_.unlock();
  return true;
}

// 测试代码
TEST(BufferPoolManagerInstanceTest, MyDataTest) {
  const std::string db_name = "test.db";
  const size_t buffer_pool_size = 1;
  auto *disk_manager = new DiskManager(db_name);
  // buffer pool size  = 1
  auto *bpmi = new BufferPoolManagerInstance(buffer_pool_size, disk_manager, nullptr);
  page_id_t page_id_temp;
  // allocate a page
  auto *page0 = bpmi->NewPage(&page_id_temp);
  ASSERT_EQ(0, page_id_temp);
  ASSERT_NE(nullptr, page0);
  // unpin and delete this page
  ASSERT_EQ(true, bpmi->UnpinPage(page_id_temp, false));
  ASSERT_EQ(true, bpmi->DeletePage(page_id_temp));

  // allocate a page
  page0 = bpmi->NewPage(&page_id_temp);
  ASSERT_EQ(1, page_id_temp);
  ASSERT_NE(nullptr, page0);
  
  // 这次分配应该是失败的
  page0 = bpmi->NewPage(&page_id_temp);
  ASSERT_EQ(INVALID_PAGE_ID, page_id_temp);
  ASSERT_EQ(nullptr, page0);

  disk_manager->ShutDown();
  remove("test.db");
  delete bpmi;
  delete disk_manager;
}

并行缓冲池管理器

这个task非常简单,就是在parallel_buffer_pool_manager多分配几个buffer_pool_manager_instance,把逻辑页号对bpmi的个数取模,调用bpmi数组中下标为取模结果的实例来完成任务即可,提高效率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值