http://www.javashuo.com/article/p-upeakuwf-oa.htmlhtml
這個任務要求咱們實如今課堂上所描述的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
.關於Lock
和Lathes
的區別請看下文。c++
其實這個任務仍是蠻簡單的。你只須要清楚什麼是最近最少使用算法便可。數據庫
LRU 算法的設計原則是:若是一個數據在最近一段時間沒有被訪問到,那麼在未來它被訪問的可能性也很小。也就是說,當限定的空間已存滿數據時,應當把最久沒有被訪問到的數據淘汰。數組
這個題我熟啊。leetcode
上有原題。並且要求在o(1)的時間複雜度實現這一任務。安全
https://leetcode-cn.com/problems/lru-cache/數據結構
爲了實如今O(1)時間內進行查找。所以咱們能夠用一個hash表。並且咱們要記錄一個時間戳來完成記錄最近最少使用的塊是誰。這裏咱們能夠用list
來實現。併發
若是咱們訪問了鏈表中的一個元素。就把這個元素放在鏈表頭部。這樣放在鏈表尾部的元素必定就是最近最少使用的元素。
爲了讓插入和刪除均爲O(1)咱們能夠用雙向鏈表來實現。
這裏對於pin
和unpin
操做實際上對於了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 輔助函數設置
這裏咱們須要兩個輔助函數remove
和insert
這裏的head
和tail
爲頭節點和尾節點。這樣寫可以減小對於邊界條件判斷。在構造函數內咱們進行初始化
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 函數實現
注意這裏必需要加鎖,以防止併發錯誤。
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 函數實現
注意這裏必需要加鎖,以防止併發錯誤。
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 函數實現
注意這裏必需要加鎖,以防止併發錯誤。
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); } }
執行下面的語句便可
cd build make lru_replacer_test ./test/lru_replacer_test
能夠發現成功經過
接下來,您須要在系統中實現緩衝池管理器(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 爲何須要pin
其實大抵能夠以下圖。
考慮這樣一種狀況。一個塊被放入緩衝區,進程從緩衝區內存中讀取塊的內容。可是,當這個塊被讀取的時候,若是一個併發進程將這個塊驅逐出來,並用一個不一樣的塊替換它,讀取舊塊內容的進程(reader)將看到不正確的數據;若是塊被驅逐時正在寫入它,那麼寫入者最終會破壞替換塊的內容。
所以,在進程從緩衝區塊讀取數據以前,確保該塊不會被逐出是很重要的。爲此,進程在塊上執行一個pin操做;緩衝區管理器從不清除固定的塊(pin值不爲0的塊)。當進程完成讀取數據時,它應該執行一個unpin操做,容許在須要時將塊取出。
所以咱們須要一個pin_couter
來記錄pin的數量。其實也就是引用計數的思想。
1.2 如何管理頁和訪問頁
一句話基地址+偏移量
page(基地值)+frame_id(偏移量) 實際上就是數組尋址
這裏用了hash表來實現page_table
來映射page_id
和frame_id
2.1 FetchPageImpl 實現
Page *BufferPoolManager::FetchPageImpl(page_id_t page_id)
這個函數的做用就是咱們要訪問一個page
。這個函數能夠分爲三種狀況分析
disk
中取出page
而後放入緩衝池以後在訪問2.2 UnpinPageImpl 實現
bool BufferPoolManager::UnpinPageImpl(page_id_t page_id, bool is_dirty)
函數定義如上。這裏的is_dirty
主要是對於兩種狀況
is_dirty=false
is_dirty=true
這個函數就是若是咱們這個線程已經完成了對這個頁的操做。咱們須要unpin
如下
若是這個頁的pin_couter>0
咱們直接--
若是這個頁的pin _couter==0
咱們須要給它加到Lru_replacer
中。由於沒有人引用它。因此它能夠成爲被替換的候選人
2.3 FlushPageImpl 實現
bool BufferPoolManager::FlushPageImpl(page_id_t page_id)
這個函數是要把一個page
寫入磁盤。
2.4 NewPageImpl 實現
Page *BufferPoolManager::NewPageImpl(page_id_t *page_id)
分配一個新的page。
2.5 DeletePageImpl 實現
bool BufferPoolManager::DeletePageImpl(page_id_t page_id)
這裏是要咱們把緩衝池中的page移出
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; }
cd build make buffer_pool_manager_test ./test/buffer_pool_manager_test