CMU數據庫(15-445)Lab1-BufferPoolManager

0. 關於環境搭建請看

http://www.javashuo.com/article/p-upeakuwf-oa.htmlhtml

1. Task1 LRU REPLACEMENT POLICY

0. 任務描述

這個任務要求咱們實如今課堂上所描述的LRU算法最近最少使用算法。node

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

  • 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的區別請看下文。c++

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 數據結構設計

struct Node{
  Node(frame_id_t v) :value(v) {}
  frame_id_t value;
  std::shared_ptr<Node> left;
  std::shared_ptr<Node> right;
};

這裏咱們用了雙向鏈表。主要是爲了刪除和插入均爲0(1)的時間複雜度

1.2 輔助函數設置

這裏咱們須要兩個輔助函數removeinsert

這裏的headtail爲頭節點和尾節點。這樣寫可以減小對於邊界條件判斷。在構造函數內咱們進行初始化

LRUReplacer::LRUReplacer(size_t num_pages) {
  head.reset(new Node(-1));
  tail.reset(new Node(-1));
  capacity=num_pages;
  head->right=tail;
  tail->left=head;
}

關於頭節點和尾節點的做用能夠參考下文。

https://blog.csdn.net/qq_41809589/article/details/86550994

insert函數負責把一個節點插入到鏈表頭部。

void LRUReplacer::insert(std::shared_ptr<Node> node) {
  if (node == nullptr) {
    return;
  }
  node->right = head->right;
  node->left = head;
  head->right->left=node;
  head->right=node;
  hash[node->value] = node;
  size++;
}

remove函數負責把一個節點從鏈表中移除

bool LRUReplacer::remove(const frame_id_t &value) {
  auto iter=hash.find(value);
  if(iter==hash.end())return false;
  auto  node=iter->second;
  node->right->left=node->left;
  node->left->right=node->right;
  hash.erase(value);
  size--;
  return true;
}

1.3 Victim 函數實現

注意這裏必需要加鎖,以防止併發錯誤。

  1. 若是沒有能夠犧牲的頁直接返回false
  2. 若是有的話選擇在鏈表尾部的頁。remove它便可
bool LRUReplacer::Victim(frame_id_t *frame_id) {
  std::scoped_lock lru_clk{lru_mutex};
  if (hash.empty()) {
    return false;
  }
  auto id = tail->left;
  remove(id->value);
  *frame_id = id->value;
  return true;
}

1.4 pin 函數實現

注意這裏必需要加鎖,以防止併發錯誤。

  1. 若是這個頁存在則直接remove(由於這個時候它的pin_couter=0
void LRUReplacer::Pin(frame_id_t frame_id) {
  std::scoped_lock lru_clk{lru_mutex};
  if(hash.count(frame_id))remove(frame_id);
}

1.5 unpin 函數實現

注意這裏必需要加鎖,以防止併發錯誤。

  1. 先看一下這個頁是否在可替換鏈表中
  2. 若是它不存在的話。則須要看一下當前鏈表是否還有空閒位置。若是有的話則直接加入
  3. 若是沒有則須要移除鏈表尾部的節點知道有空餘位置
void LRUReplacer::Unpin(frame_id_t frame_id) {
  std::scoped_lock lru_clk{lru_mutex};
  auto iter=hash.find(frame_id);
  if(iter==hash.end()){
    if (hash.size() >= capacity) {
      // need to remove item
      while (hash.size() >= capacity) {
       auto p=tail->left;
       remove(p->value);
      }
    }
    auto newNode = std::make_shared<Node>(frame_id);
    insert(newNode);

  }
}

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(偏移量) 實際上就是數組尋址

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

2. 實現

2.1 FetchPageImpl 實現

Page *BufferPoolManager::FetchPageImpl(page_id_t page_id)

這個函數的做用就是咱們要訪問一個page。這個函數能夠分爲三種狀況分析

  1. 若是該頁在緩衝池中直接訪問
  2. 若是該頁不在緩衝池可是緩衝池中有空閒。從disk中取出page而後放入緩衝池以後在訪問
  3. 若是該頁不在緩衝池而且緩衝池也非空閒
    • 須要找到一個犧牲頁。把它移出(判斷髒位來決定是否要寫會磁盤)
    • 而後和狀況2同樣。

2.2 UnpinPageImpl 實現

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

函數定義如上。這裏的is_dirty主要是對於兩種狀況

  • 狀況一。對於讀操做而言is_dirty=false
  • 狀況二。對於寫操做而言is_dirty=true

這個函數就是若是咱們這個線程已經完成了對這個頁的操做。咱們須要unpin如下

  1. 若是這個頁的pin_couter>0咱們直接--

  2. 若是這個頁的pin _couter==0咱們須要給它加到Lru_replacer中。由於沒有人引用它。因此它能夠成爲被替換的候選人

2.3 FlushPageImpl 實現

bool BufferPoolManager::FlushPageImpl(page_id_t page_id)

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

  1. 首先找到這一個頁在緩衝池之中的位置
  2. 寫入磁盤

2.4 NewPageImpl 實現

Page *BufferPoolManager::NewPageImpl(page_id_t *page_id)

分配一個新的page。

  1. 若是緩衝池有空閒位置。則直接放進緩衝池
  2. 不然的話。若是有頁能夠被犧牲掉。則犧牲它,把咱們的新頁放進去
  3. 不然失敗

2.5 DeletePageImpl 實現

bool BufferPoolManager::DeletePageImpl(page_id_t page_id)

這裏是要咱們把緩衝池中的page移出

  1. 若是這個page根本就不在緩衝池則直接返回
  2. 若是這個page 的引用計數大於0(pin_counter>0)表示咱們不能返回
  3. 若是這個page被修改過則要寫會磁盤
  4. 不然正常移除就行了。(在hash表中erase)

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. 測試

cd build
 make buffer_pool_manager_test
 ./test/buffer_pool_manager_test

相關文章
相關標籤/搜索