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 theReplacer
, store its contents in the output parameter and returnTrue
. If theReplacer
is empty returnFalse
.Pin(T)
: This method should be called after a page is pinned to a frame in theBufferPoolManager
. It should remove the frame containing the pinned page from theLRUReplacer
.Unpin(T)
: This method should be called when thepin_count
of a page becomes 0. This method should add the frame containing the unpinned page to theLRUReplacer
.Size()
: This method returns the number of frames that are currently in theLRUReplacer
.
關於Lock
和Lathes
的區別請看下文。
1. 實現
其實這個任務還是蠻簡單的。你只需要清楚什麼是最近最少使用演算法即可。
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 函式實現
注意這裡必須要加鎖,以防止併發錯誤。
- 如果沒有可以犧牲的頁直接返回false
- 如果有的話選擇在連結串列尾部的頁。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 函式實現
注意這裡必須要加鎖,以防止併發錯誤。
- 如果這個頁存在則直接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 函式實現
注意這裡必須要加鎖,以防止併發錯誤。
- 先看一下這個頁是否在可替換連結串列中
- 如果它不存在的話。則需要看一下當前連結串列是否還有空閒位置。如果有的話則直接加入
- 如果沒有則需要移除連結串列尾部的節點知道有空餘位置
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_id
和frame_id
2. 實現
2.1 FetchPageImpl 實現
Page *BufferPoolManager::FetchPageImpl(page_id_t page_id)
這個函式的作用就是我們要訪問一個page
。這個函式可以分為三種情況分析
- 如果該頁在緩衝池中直接訪問
- 如果該頁不在緩衝池但是緩衝池中有空閒。從
disk
中取出page
然後放入緩衝池之後在訪問 - 如果該頁不在緩衝池並且緩衝池也非空閒
- 需要找到一個犧牲頁。把它移出(判斷髒位來決定是否要寫會磁碟)
- 然後和情況2一樣。
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移出
- 如果這個page根本就不在緩衝池則直接返回
- 如果這個page 的引用計數大於0(pin_counter>0)表示我們不能返回
- 如果這個page被修改過則要寫會磁碟
- 否則正常移除就好了。(在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