[已滿分]CMU數據庫(15-445)實驗1-BufferPoolManager


0. 關於環境搭建請看

https://www.cnblogs.com/JayL-zxl/p/14307260.html

好了時隔超久我來填坑了,這次重構代碼和修復之前博客中的一些錯誤。主要是為了復習數據庫嘿嘿,防止誤導大家和我自己復習的時候出現問題。

1. Task1 LRU REPLACEMENT POLICY

0. 任務描述

這個任務要求我們實現在課堂上所描述的LRU算法最近最少使用算法。

你需要實現下面這些函數。請確保他們都是線程安全的。

  • 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.

關於LockLathes的區別請看下文。

https://stackoverflow.com/questions/3111403/what-is-the-difference-between-a-lock-and-a-latch-in-the-context-of-concurrent-a/42464336#42464336

1. 實現

其實這個任務還是蠻簡單的。你只需要清楚什么是最近最少使用算法即可。

LRU 算法的設計原則是:如果一個數據在最近一段時間沒有被訪問到,那么在將來它被訪問的可能性也很小。也就是說,當限定的空間已存滿數據時,應當把最久沒有被訪問到的數據淘汰。

這個題我熟啊。leetcode上有原題。而且要求在o(1)的時間復雜度實現這一任務。

https://leetcode-cn.com/problems/lru-cache/

為了實現在O(1)時間內進行查找。因此我們可以用一個hash表。而且我們要記錄一個時間戳來完成記錄最近最少使用的塊是誰。這里我們可以用list來實現。

如果我們訪問了鏈表中的一個元素。就把這個元素放在鏈表頭部。這樣放在鏈表尾部的元素一定就是最近最少使用的元素。

為了讓插入和刪除均為O(1)我們可以用鏈表來實現。

這里對於pinunpin操作實際上對於了task2。我們為什么需要pin。書上給了我們答案。下面我們也進行了分析

1.1 數據結構設計

  std::mutex latch;  // thread safety
  int capacity;      // max number of pages LRUReplacer can handle
  std::list<frame_id_t> lru_list;
	std::unordered_map<frame_id_t, std::list<frame_id_t>::iterator> lruMap;

這里我們用了鏈表 + hash表。主要是為了刪除和插入均為0(1)的時間復雜度。引入hash表就是可以根據frame_id快速找到其在list中對應的位置。否則的話你需要遍歷鏈表這就不是o(1)了

1.2 Victim 函數實現

注意這里必須要加鎖,以防止並發錯誤。

  1. 如果沒有可以犧牲的頁直接返回false
  2. 如果有的話選擇在鏈表尾部的頁。remove它即可。這里的刪除涉及鏈表和hash表兩個數據結構的刪除
bool LRUReplacer::Victim(frame_id_t *frame_id) {
  // 選擇一個犧牲frame
  latch.lock();
  if (lruMap.empty()) {
    latch.unlock();
    return false;
  }

  // 選擇列表尾部 也就是最少使用的frame
  frame_id_t lru_frame = lru_list.back();
  lruMap.erase(lru_frame);
  // 列表刪除
  lru_list.pop_back();
  *frame_id = lru_frame;
  latch.unlock();
  return true;
}

1.3 pin 函數實現

注意這里必須要加鎖,以防止並發錯誤。

  1. pin函數表示這個frame被某個進程引用了
  2. 被引用的frame不能成為LRU算法的犧牲目標,所以這里把它從我們的數據結構中刪除
void LRUReplacer::Pin(frame_id_t frame_id) {
  // 被引用的frame 不能出現在lru list中
  latch.lock();

  if (lruMap.count(frame_id) != 0) {
    lru_list.erase(lruMap[frame_id]);
    lruMap.erase(frame_id);
  }

  latch.unlock();
}

1.4 unpin 函數實現

注意這里必須要加鎖,以防止並發錯誤。

  1. 先看一下這個頁是否在可替換鏈表中
  2. 如果它不存在的話。則需要看一下當前鏈表是否還有空閑位置。如果有的話則直接加入
  3. 如果沒有則需要移除鏈表尾部的節點知道有空余位置
void LRUReplacer::Unpin(frame_id_t frame_id) {
  // 加入lru list中
  latch.lock();
  if (lruMap.count(frame_id) != 0) {
    latch.unlock();
    return;
  }
  // if list size >= capacity
  // while {delete front}
  while (Size() >= capacity) {
     frame_id_t need_del = lru_list.front();
      lru_list.pop_front();
      lruMap.erase(need_del);
  }
  // insert
  lru_list.push_front(frame_id);
  lruMap[frame_id] = lru_list.begin();
  latch.unlock();
}

2. 測試

執行下面的語句即可

 cd build
 make lru_replacer_test
 ./test/lru_replacer_test


可以發現成功通過

Task2 BUFFER POOL MANAGER

0. 任務描述

接下來,您需要在系統中實現緩沖池管理器(BufferPoolManager)。BufferPoolManager負責從DiskManager獲取數據庫頁面並將它們存儲在內存中。BufferPoolManage還可以在有要求它這樣做時,或者當它需要驅逐一個頁以便為新頁騰出空間時,將臟頁寫入磁盤。為了確保您的實現能夠正確地與系統的其余部分一起工作,我們將為您提供一些已經填寫好的功能。您也不需要實現實際讀寫數據到磁盤的代碼(在我們的實現中稱為DiskManager)。我們將為您提供這一功能。

系統中的所有內存頁面均由Page對象表示。 BufferPoolManager不需要了解這些頁面的內容。 但是,作為系統開發人員,重要的是要了解Page對象只是緩沖池中用於存儲內存的容器,因此並不特定於唯一頁面。 也就是說,每個Page對象都包含一塊內存,DiskManager會將其用作復制從磁盤讀取的物理頁面內容的位置。 BufferPoolManager將在將其來回移動到磁盤時重用相同的Page對象來存儲數據。 這意味着在系統的整個生命周期中,相同的Page對象可能包含不同的物理頁面。Page對象的標識符(page_id)跟蹤其包含的物理頁面。 如果Page對象不包含物理頁面,則必須將其page_id設置為INVALID_PAGE_ID

每個Page對象還維護一個計數器,以顯示“固定”該頁面的線程數。BufferPoolManager不允許釋放固定的頁面。每個Page對象還跟蹤它的臟標記。您的工作是判斷頁面在解綁定之前是否已經被修改(修改則把臟標記置為1)。BufferPoolManager必須將臟頁的內容寫回磁盤,然后才能重用該對象。

BufferPoolManager實現將使用在此分配的前面步驟中創建的LRUReplacer類。它將使用LRUReplacer來跟蹤何時訪問頁對象,以便在必須釋放一個幀以為從磁盤復制新的物理頁騰出空間時,它可以決定取消哪個頁對象

你需要實現在(src/buffer/buffer_pool_manager.cpp):的以下函數

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

1. 分析

1.1 為什么需要pin

其實大抵可以如下圖。

考慮這樣一種情況。一個塊被放入緩沖區,進程從緩沖區內存中讀取塊的內容。但是,當這個塊被讀取的時候,如果一個並發進程將這個塊驅逐出來,並用一個不同的塊替換它,讀取舊塊內容的進程(reader)將看到不正確的數據;如果塊被驅逐時正在寫入它,那么寫入者最終會破壞替換塊的內容。

因此,在進程從緩沖區塊讀取數據之前,確保該塊不會被逐出是很重要的。為此,進程在塊上執行一個pin操作;緩沖區管理器從不清除固定的塊(pin值不為0的塊)。當進程完成讀取數據時,它應該執行一個unpin操作,允許在需要時將塊取出。

因此我們需要一個pin_couter來記錄pin的數量。其實也就是引用計數的思想。

1.2 如何管理頁和訪問頁

一句話基地址+偏移量

page(基地值)+frame_id(偏移量) 實際上就是數組尋址

同時 DBMS 會維護一個 page table,負責記錄每個 page 在內存中的位置,以及是否被寫過(Dirty Flag),是否被引用或引用計數(Pin/Reference Counter)等元信息,如下圖所示:

這里用了hash表來實現page_table來映射page_idframe_id

2. 實現

2.1 find_replace()函數

  1. 如果空閑鏈表非空,則不需要進行替換算法。直接返回一個空閑frame就okay啦。這個情況是buffer pool未滿
  2. 如果空閑鏈表為空,則表示當前buffer pool已經滿了,這個時候必須要執行LRU算法

尋找替換frame過程

  1. 調用前面實現的Victim函數獲取犧牲幀的frame id
  2. pages_中找到對應的犧牲頁,如果該頁dirty則需要寫回磁盤,並且reset pin count
  3. 然后在page_table中刪除對應映射關系 [page_id --> frame_id]

一定要注意2和3的順序不能顛倒、不然沒有辦法找到對應的犧牲頁

bool BufferPoolManager::find_replace(frame_id_t *frame_id) {
  // if free_list not empty then we don't need replace page
  // return directly
  if (!free_list_.empty()) {
    *frame_id = free_list_.front();
    free_list_.pop_front();
    return true;
  }
  // else we need to find a replace page
  if (replacer_->Victim(frame_id)) {
    // Remove entry from page_table
    int replace_frame_id = -1;
    for (const auto &p : page_table_) {
      page_id_t pid = p.first;
      frame_id_t fid = p.second;
      if (fid == *frame_id) {
        replace_frame_id = pid;
        break;
      }
    }
    if (replace_frame_id != -1) {
      Page *replace_page = &pages_[*frame_id];

      // If dirty, flush to disk
      if (replace_page->is_dirty_) {
        char *data = pages_[page_table_[replace_page->page_id_]].data_;
        disk_manager_->WritePage(replace_page->page_id_, data);
        replace_page->pin_count_ = 0;  // Reset pin_count
      }
      page_table_.erase(replace_page->page_id_);
    }

    return true;
  }

  return false;
}

2.2 FetchPageImpl 實現

Page *BufferPoolManager::FetchPageImpl(page_id_t page_id)

這個函數就是我們要拿到一個page。這個函數可以分為三種情況分析

  1. 如果該頁在緩沖池中直接訪問並且記得把它的pin_count++,然后把調用Pin函數通知replacer
  2. 否則調用find_replace函數,無論緩沖池是否有空閑,都可以獲得可用的frame_id
  3. 當然如果替換頁為空,擇要
  4. 然后建立新的page_table映射關系
 latch_.lock();
  std::unordered_map<page_id_t, frame_id_t>::iterator it = page_table_.find(page_id);
  // 1.1 P exists
  if (it != page_table_.end()) {
    frame_id_t frame_id = it->second;
    Page *page = &pages_[frame_id];

    //
    page->pin_count_++;        // pin the page
    replacer_->Pin(frame_id);  // notify replacer

    latch_.unlock();
    return page;
  }
  // 1.2 P not exist
  frame_id_t replace_fid;
  if (!find_replace(&replace_fid)) {
    latch_.unlock();
    return nullptr;
  }
  Page *replacePage = &pages_[replace_fid];
  // 2. write it back to the disk
  if (replacePage->IsDirty()) {
    disk_manager_->WritePage(replacePage->page_id_, replacePage->data_);
  }
  // 3
  page_table_.erase(replacePage->page_id_);
  // create new map
  // page_id <----> replaceFrameID;
  page_table_[page_id] = replace_fid;
  // 4. update replacePage info
  Page *newPage = replacePage;
  disk_manager_->ReadPage(page_id, newPage->data_);
  newPage->page_id_ = page_id;
  newPage->pin_count_++;
  newPage->is_dirty_ = false;
  replacer_->Pin(replace_fid);
  latch_.unlock();

  return newPage;

2.3 UnpinPageImpl 實現

bool BufferPoolManager::UnpinPageImpl(page_id_t page_id, bool is_dirty) 

函數定義如上。

這個函數就是如果我們這個進程已經完成了對這個頁的操作。我們需要unpin操作

  1. 如果這個頁的pin_couter>0我們直接--

  2. 如果這個頁的pin _couter==0我們需要給它加到Lru_replacer中。因為沒有人引用它。所以它可以成為被替換的候選人

bool BufferPoolManager::UnpinPageImpl(page_id_t page_id, bool is_dirty) {
  latch_.lock();
  // 1. 如果page_table中就沒有
  auto iter = page_table_.find(page_id);
  if (iter == page_table_.end()) {
    latch_.unlock();
    return false;
  }
  // 2. 找到要被unpin的page
  frame_id_t unpinned_Fid = iter->second;
  Page *unpinned_page = &pages_[unpinned_Fid];
  if (is_dirty) {
    unpinned_page->is_dirty_ = true;
  }
  // if page的pin_count == 0 則直接return
  if (unpinned_page->pin_count_ == 0) {
    latch_.unlock();
    return false;
  }
  unpinned_page->pin_count_--;
  if (unpinned_page->GetPinCount() == 0) {
    replacer_->Unpin(unpinned_Fid);
  }
  latch_.unlock();
  return true;
}

2.4 FlushPageImpl 實現

bool BufferPoolManager::FlushPageImpl(page_id_t page_id)

這個函數是要把一個page寫入磁盤。

  1. 首先找到這一個頁在緩沖池之中的位置
  2. 寫入磁盤
  // Make sure you call DiskManager::WritePage!
  auto iter = page_table_.find(page_id);
  if (iter == page_table_.end() || page_id == INVALID_PAGE_ID) {
    latch_.unlock();
    return false;
  }

  frame_id_t flush_fid = iter->second;
  disk_manager_->WritePage(page_id, pages_[flush_fid].data_);
  
  return false;

2.5 NewPageImpl 實現

Page *BufferPoolManager::NewPageImpl(page_id_t *page_id) 

分配一個新的page。

  1. 利用find_replace函數在我們的緩沖池找到合適的地方建立page_id --> frame_id的映射
  2. 更新 新頁的元數據
    這里注意新創建的頁要寫回磁盤
Page *BufferPoolManager::NewPageImpl(page_id_t *page_id) {
    latch_.lock();
  // 0.
  page_id_t new_page_id = disk_manager_->AllocatePage();
  // 1.
  bool is_all = true;
  for (int i = 0; i < static_cast<int>(pool_size_); i++) {
    if (pages_[i].pin_count_ == 0) {
      is_all = false;
      break;
    }
  }
  if (is_all) {
    latch_.unlock();
    return nullptr;
  }
  // 2.
  frame_id_t victim_fid;
  if (!find_replace(&victim_fid)) {
    latch_.unlock();
    return nullptr;
  }
  // 3.
  Page *victim_page = &pages_[victim_fid];
  victim_page->page_id_ = new_page_id;
  victim_page->pin_count_++;
  replacer_->Pin(victim_fid);
  page_table_[new_page_id] = victim_fid;
  victim_page->is_dirty_ = false;
  *page_id = new_page_id;
  // [attention]
  // if this not write to disk directly
  // maybe meet below case:
  // 1. NewPage
  // 2. unpin(false)
  // 3. 由於其他頁的操作導致該頁被從buffer_pool中移除
  // 4. 這個時候在FetchPage, 就拿不到這個page了。
  // 所以這里先把它寫回磁盤
  disk_manager_->WritePage(victim_page->GetPageId(), victim_page->GetData());
  latch_.unlock();
  return victim_page;
}

2.6 DeletePageImpl 實現

bool BufferPoolManager::DeletePageImpl(page_id_t page_id)

這里是要我們把緩沖池中的page移出

  1. 如果這個page根本就不在緩沖池則直接返回
  2. 如果這個page 的引用計數大於0(pin_counter>0)表示我們不能返回
  3. 如果這個page被修改過則要寫回磁盤
  4. 否則正常移除就好了。(在hash表中erase)
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.
  latch_.lock();

  // 1.
  if (page_table_.find(page_id) == page_table_.end()) {
    latch_.unlock();
    return true;
  }
  // 2.
  frame_id_t frame_id = page_table_[page_id];
  Page *page = &pages_[frame_id];
  if (page->pin_count_ > 0) {
    latch_.unlock();
    return false;
  }
  if (page->is_dirty_) {
    FlushPageImpl(page_id);
  }
  // delete in disk in here
  disk_manager_->DeallocatePage(page_id);
  
  page_table_.erase(page_id);
  // reset metadata
  page->is_dirty_ = false;
  page->pin_count_ = 0;
  page->page_id_ = INVALID_PAGE_ID;
  // return it to the free list
  
  free_list_.push_back(frame_id);
  latch_.unlock();
  return true;
}

3. 源碼解析

3.1 ResetMemory()

這個非常簡單就是一個簡單的內存分配。給我們的frame分配內存區域

3.2 ReadPage

void DiskManager::ReadPage(page_id_t page_id, char *page_data)
void DiskManager::ReadPage(page_id_t page_id, char *page_data) {
  int offset = page_id * PAGE_SIZE; //PAGE_SIZE=4kb 先計算偏移。判斷是否越界(因為文件大小有限制)
  // check if read beyond file length
  if (offset > GetFileSize(file_name_)) {
    LOG_DEBUG("I/O error reading past end of file");
    // std::cerr << "I/O error while reading" << std::endl;
  } else {
    // set read cursor to offset
    db_io_.seekp(offset); //把讀寫位置移動到偏移位置處
    db_io_.read(page_data, PAGE_SIZE); //把數據讀到page_data中
    if (db_io_.bad()) {
      LOG_DEBUG("I/O error while reading");
      return;
    }
    // if file ends before reading PAGE_SIZE
    int read_count = db_io_.gcount();
    if (read_count < PAGE_SIZE) {
      LOG_DEBUG("Read less than a page");
      db_io_.clear();
      // std::cerr << "Read less than a page" << std::endl;
      memset(page_data + read_count, 0, PAGE_SIZE - read_count); //如果讀取的數據小於4kb剩下的補0
    }
  }
}

3.3 WritePage

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);
  db_io_.write(page_data, PAGE_SIZE); //向offset處寫data
  // 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(); //刷新緩沖區
}

3.4 DiskManager 構造函數

就是獲取文件指針

DiskManager::DiskManager(const std::string &db_file)
    : file_name_(db_file), next_page_id_(0), num_flushes_(0), num_writes_(0), flush_log_(false), flush_log_f_(nullptr) {
  std::string::size_type n = file_name_.rfind('.');
  if (n == std::string::npos) {
    LOG_DEBUG("wrong file format");
    return;
  }
  log_name_ = file_name_.substr(0, n) + ".log";

  log_io_.open(log_name_, std::ios::binary | std::ios::in | std::ios::app | std::ios::out);
  // directory or file does not exist
  if (!log_io_.is_open()) {
    log_io_.clear();
    // create a new file
    log_io_.open(log_name_, std::ios::binary | std::ios::trunc | std::ios::app | std::ios::out);
    log_io_.close();
    // reopen with original mode
    log_io_.open(log_name_, std::ios::binary | std::ios::in | std::ios::app | std::ios::out);
    if (!log_io_.is_open()) {
      throw Exception("can't open dblog file");
    }
  }

  db_io_.open(db_file, std::ios::binary | std::ios::in | std::ios::out); //獲取文件指針。並且打開輸入輸出流
  // directory or file does not exist
  if (!db_io_.is_open()) {
    db_io_.clear();
    // create a new file
    db_io_.open(db_file, std::ios::binary | std::ios::trunc | std::ios::out);
    db_io_.close();
    // reopen with original mode
    db_io_.open(db_file, std::ios::binary | std::ios::in | std::ios::out);
    if (!db_io_.is_open()) {
      throw Exception("can't open db file");
    }
  }
  buffer_used = nullptr;
}

4. 測試

4.1 本地測試

 cd build
 make buffer_pool_manager_test
 ./test/buffer_pool_manager_test

4.2 cmu官網測試

后面發現原來不是cmu自己的學生也可以用它們的軟件進行測試。修改了好久同時得到了大佬的幫助。才成功實現滿分

cmu的測試網站如下 https://www.gradescope.com/courses/195440

如何使用測試網站的教程在 課程的FAQ 的最下面


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM