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 數據結構設計
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 函數實現
注意這里必須要加鎖,以防止並發錯誤。
- 如果沒有可以犧牲的頁直接返回false
- 如果有的話選擇在鏈表尾部的頁。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 函數實現
注意這里必須要加鎖,以防止並發錯誤。
- pin函數表示這個frame被某個進程引用了
- 被引用的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 函數實現
注意這里必須要加鎖,以防止並發錯誤。
- 先看一下這個頁是否在可替換鏈表中
- 如果它不存在的話。則需要看一下當前鏈表是否還有空閑位置。如果有的話則直接加入
- 如果沒有則需要移除鏈表尾部的節點知道有空余位置
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_id
和frame_id
2. 實現
2.1 find_replace()函數
- 如果空閑鏈表非空,則不需要進行替換算法。直接返回一個空閑frame就okay啦。這個情況是buffer pool未滿
- 如果空閑鏈表為空,則表示當前buffer pool已經滿了,這個時候必須要執行LRU算法
尋找替換frame過程
- 調用前面實現的
Victim
函數獲取犧牲幀的frame id
- 在
pages_
中找到對應的犧牲頁,如果該頁dirty則需要寫回磁盤,並且reset pin count - 然后在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
。這個函數可以分為三種情況分析
- 如果該頁在緩沖池中直接訪問並且記得把它的
pin_count++
,然后把調用Pin
函數通知replacer
- 否則調用
find_replace
函數,無論緩沖池是否有空閑,都可以獲得可用的frame_id
- 當然如果替換頁為空,擇要
- 然后建立新的
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
操作
-
如果這個頁的
pin_couter>0
我們直接-- -
如果這個頁的
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
寫入磁盤。
- 首先找到這一個頁在緩沖池之中的位置
- 寫入磁盤
// 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。
- 利用
find_replace
函數在我們的緩沖池找到合適的地方建立page_id --> frame_id的映射 - 更新 新頁的元數據
這里注意新創建的頁要寫回磁盤
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移出
- 如果這個page根本就不在緩沖池則直接返回
- 如果這個page 的引用計數大於0(pin_counter>0)表示我們不能返回
- 如果這個page被修改過則要寫回磁盤
- 否則正常移除就好了。(在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 的最下面