Task1:实现LRU算法
在这个task中我们要实现的是lru_replacer这个文件,这个文件我们需要实现三个函数
Victim(frame_id_t *frame_id)
Pin(frame_id_t frame_id)
Unpin(frame_id_t frame_id)
乍一看,好像每一个都和LRU没什么关系。
那我们先解释一下每一个函数名吧:
Vitcim:受害者;Pin:大头钉(应该是固定);Unpin:那就应该是取消固定
解释完更加迷惑了,别急,我们先来简单回顾一下LRU页面置换算法:
LRU置换算法即最近最久未使用
顾名思义我们每次淘汰掉的页面是最近最久没有使用的页面。
那现在我们再来看一下函数名,好像我们可以把vitcim理解为淘汰的页面。
好,那剩余的两个呢?
上面我们说到应该把pin和unpin理解为固定和取消固定,但是我们所理解的lru置换算法中好像没有这两个功能吧。
让我们看到数据库系统概念的13.5.1.2(原书第7版)被钉住的块
在一个进程或线程从缓冲块中读取数据之前,确保此块不会被移出是很重要的。
所以这个pin函数应该是钉住一个页框不让其被换出,unpin函数是取消固定这个页框。
好了现在我们梳理完了各个函数的作用是什么,那么我们应该如何实现呢?
力扣的第146题也是lru的实现,可以提前去写一下。
不管你有没有写这道题,我们要实现lru都需要找到最近最久未使用的那个页面。那我们怎么能找到最近最久未使用的页面呢?或者说,我们应该用哪种数据结构去找呢?
我们可以用线性表来保存页面,线性表又包含顺序表、链表、栈和队列。但是这个lab的要求是在O(1)的时间内找到页面,我们都知道栈和队列都是操作受限的,所以不合适。那么顺序表和链表呢?我们在移动元素的时候顺序表肯定不能在O(1)的时间内完成。但是在O(1)的时间内找元素呢?链表怎么实现?
这个时候就需要用到另一种数据结构了——哈希表。
std::list<frame_id_t> frames_;
std::unordered_map<frame_id_t, std::list<frame_id_t>::iterator> pos_;
std::mutex mtx_; //互斥锁
现在已经做完前期准备了,让我们向task1发起总攻!
我们先来看vitcim
bool LRUReplacer::Victim(frame_id_t *frame_id)
我们每次删除最近最久未使用的页面,也即是链表的第一个节点。当然,如果链表为空,肯定就没办法去置换。
我们注意到传入的参数是指针类型,也即是当前淘汰的页面要被调用这个函数的线程使用,所以我们应该把置换出来的页框要返回给此线程。同时要在链表和哈希表中抹去这个页框,因为我们不知道这个页框存放的是哪一个页面。
bool LRUReplacer::Victim(frame_id_t *frame_id) {
std::lock_guard<std::mutex> lock(mtx_);
if (frames_.empty()) {
return false;
}
*frame_id = frames_.front();
frames_.pop_front();
pos_.erase(*frame_id);
return true;
}
下边我们来看pin和unpin
固定页框,翻译一下:不允许被置换掉。
这样看来我们怎么才能不让其被置换掉呢?也就是我们不让调用vitcim的进程获得这个页框呢?
现在回到vitcim的逻辑,我们发现,每次置换掉的都是在链表首部的,所以只要我们在链表中就有可能被置换掉,对吧。
所以呢,我们怎么办?我们让vitcim找不到我们这个页框,也就是我们直接在链表和哈希表中删除我们这个页框。
同理,我们在unpin(解除固定)的时候再将其加入到链表中
void LRUReplacer::Pin(frame_id_t frame_id) {
std::lock_guard<std::mutex> lock(mtx_);
if (pos_.count(frame_id) == 0) {
return;
}
frames_.erase(pos_[frame_id]);
pos_.erase(frame_id);
}
void LRUReplacer::Unpin(frame_id_t frame_id) {
std::lock_guard<std::mutex> lock(mtx_);
if (pos_.count(frame_id) != 0) {
return;
}
frames_.push_back(frame_id);
pos_[frame_id] = prev(frames_.end());
}
写到这突然发现还有一个Size()方法,这个就比较简单了
直接返回链表的size()就可以了。
Task2:buffer_pool_manager_instance(数据库管理实例)
在写这个之前,我们要先了解几个概念:页面(page)、页框(frame)、页面号(page_id)、页框号(frame_id)
这个更多的会用到页面这个概念,这个lab已经为我们封装好了一个页面类,我们先来看一下这个页面类给我们封装了哪些成员方法已经哪些成员变量
成员方法的命名都是浅显易懂的,直接看成员变量:
data_[PAGE_SIZE]:用来写数据,用不到;
Page_id:页面号,初始值应该是INVALID_PAGE_ID;
Pin_count:有多少进程或线程在使用这个页面,当pin_count变为0的时候调用UnPin;
Is_dirty:该页面有没有被修改过,如果被修改过,要写回磁盘(写回磁盘的方法已经被封装好了,我们不用去了解,直接调用就行);
Rwlatch:读写锁。
好了,我们在看一下我们要写的文件里面有什么我们要了解的
pages_:指向页面类的指针,被初始化为一个数组,大小为pool_size;
Disk_manager:指向磁盘管理,不用管,直接调用就行;
Log_manager:指向日志管理,不用管,直接用就行;
Page_table_:实现页面号到页框号的转换;
Replacer_:指向task1那个类,直接调用;
Free_list_:存储没被使用的页面的链表;
现在我有一个问题,pages_数组和free_list链表是通过什么来访问的?我们现在已知的有页号和页框号。
现在回来看这个问题感觉有点弱智,但当时我确实有这个疑问。
我们来回忆一下页面和页框的定义:
页面是将进程或线程分成的逻辑上的定义;
页框是将操作系统内存分成的逻辑上的定义。
那么我们使用的肯定是页框啊,对吧。因为要在内存中找到他们。所以用页框号来索引。
那么我们如果只知道页号呢?
你忘了page_table了吗,实现映射的。
然后我们将free_list初始化
// Initially, every page is in the free list.
for (size_t i = 0; i < pool_size_; ++i) {
free_list_.emplace_back(static_cast<int>(i));
}
下面来完成我们要写的几个成员方法:
先从简单的开始入手
bool BufferPoolManagerInstance::FlushPgImp(page_id_t page_id)
void BufferPoolManagerInstance::FlushAllPgsImp()
这两个是最简单的,前一个是将某一页写磁盘,后一个是将所有页写盘
对于前一个,我们需要判断这个页号对应的页面在不在内存中,也就是在page_table中能不能找到,并且还要判断这个页号是不是非法的。这都是为了健壮性。
还有就是注意并发。
bool BufferPoolManagerInstance::FlushPgImp(page_id_t page_id) {
// Make sure you call DiskManager::WritePage!
std::lock_guard<std::mutex> lck(latch_);
if (page_id == INVALID_PAGE_ID || page_table_.count(page_id) == 0) {
return false;
}
frame_id_t frame_id = page_table_[page_id];
Page *page_t = &pages_[frame_id];
if (page_t->IsDirty()) {
disk_manager_->WritePage(page_t->GetPageId(), page_t->GetData());
}
return true;
}
void BufferPoolManagerInstance::FlushAllPgsImp() {
// You can do it!
std::lock_guard<std::mutex> lck(latch_);
for (auto temp : page_table_) {
Page *page_t = &pages_[temp.second];
if (page_t->IsDirty()) {
disk_manager_->WritePage(page_t->GetPageId(), page_t->GetData());
}
}
}
你可能会好奇,这里写完盘之后为什么不将其的is_dirty修改为false;
因为我们每次删除一个页面或者的时候就已经
接下来再来看
Page *BufferPoolManagerInstance::NewPgImp(page_id_t *page_id)
下面的注释其实已经说的很明白了,现在让我们忽略这个注释,回到我们刚才的成员变量以及task1上完成的成员方法,我们可以从哪里获得一个新页面呢?
对!Free_list链表,那如果没有空闲的呢?
没错Vitcim方法(LRU)
所以当free_list中没有数据的时候并且LRU没有置换的时候,就没办法申请新页面了。
否则就从中申请一个。
申请到的页面记得初始化,并且将其加入page_table的映射关系中。
Page *BufferPoolManagerInstance::NewPgImp(page_id_t *page_id) {
// 0. Make sure you call AllocatePage!
// 1. If all the pages in the buffer pool are pinned, return nullptr.
// 2. Pick a victim page P from either the free list or the replacer. Always pick from the free list first.
// 3. Update P's metadata, zero out memory and add P to the page table.
// 4. Set the page ID output parameter. Return a pointer to P.
std::lock_guard<std::mutex> lck(latch_);
if (replacer_->Size() == 0 && free_list_.empty()) {
return nullptr;
}
frame_id_t new_frame_id;
if (free_list_.empty()) {
replacer_->Victim(&new_frame_id);
Page *vic = &pages_[new_frame_id];
if (vic->IsDirty()) {
disk_manager_->WritePage(vic->GetPageId(), vic->GetData());
}
page_table_.erase(vic->GetPageId());
} else {
new_frame_id = free_list_.front();
free_list_.pop_front();
}
*page_id = AllocatePage();
Page *new_page = &pages_[new_frame_id];
// Page *new_page = new Page;
// 省略初始化
// pages_[new_frame_id] = *new_page;
// 上述为什么不行?
new_page->ResetMemory();//提供好的成员函数,直接调用就行
new_page->is_dirty_ = false;
new_page->page_id_ = *page_id;
new_page->pin_count_++;
replacer_->Pin(new_frame_id);
page_table_.emplace(new_page->GetPageId(), new_frame_id);
return new_page;
}
我在写的时候有上述注释的疑问,不过现在已经解决了。
因为这个类中没有重载=这个操作符,编译器没见过这种情况,不知道怎么办了。
Page *BufferPoolManagerInstance::FetchPgImp(page_id_t page_id)
请求一个页面。
我们现在只有页号,需要用page_table去找到其页框号,在内存中找到则直接返回即可,并将其更新。
如果发现不在内存,看一眼能不能从空闲链表中或者LRU中获得一个。找到的话就讲其更新。
朋友讲从空闲链表或者LRU中获取一个页面封装成一个成员函数,我认为这样是很有必要的,更加的模块化,我在完成的时候并没有使用,而是每次用就重新写一遍,费时费力。
在请求页面的时候,我们肯定是要用到这个页面,所以我们获取页面之后要将其Pin起来,并更新其pin_count
Page *BufferPoolManagerInstance::FetchPgImp(page_id_t page_id) {
// 1. Search the page table for the requested page (P).
// 1.1 If P exists, pin it and return it immediately.
// 1.2 If P does not exist, find a replacement page (R) from either the free list or the replacer.
// Note that pages are always found from the free list first.
// 2. If R is dirty, write it back to the disk.
// 3. Delete R from the page table and insert P.
// 4. Update P's metadata, read in the page content from disk, and then return a pointer to P.
std::lock_guard<std::mutex> lck(latch_);
Page *fet_page = nullptr;
//如果这个页面存在,则直接返回就可以了;因为此时有线程在使用该页面,所以不允许其被换出,则应调用pin
if (page_table_.count(page_id) != 0) {
frame_id_t fet_frame_id = page_table_[page_id];
// replacer_->Pin(page_id);
replacer_->Pin(fet_frame_id);
fet_page = &pages_[fet_frame_id];
fet_page->pin_count_++;
return fet_page;
}
frame_id_t fet_frame_id; //接受返回的那个页框号
if (replacer_->Victim(&fet_frame_id)) {
fet_page = &pages_[fet_frame_id];
} else if (!free_list_.empty()) {
fet_frame_id = free_list_.front();
free_list_.pop_front();
fet_page = &pages_[fet_frame_id];
} else {
return nullptr;
}
if (fet_page->IsDirty()) {
disk_manager_->WritePage(fet_page->GetPageId(), fet_page->GetData());
}
page_table_.erase(fet_page->GetPageId());
// page_table_.emplace(page_id, fet_frame_id);
page_table_[page_id] = static_cast<frame_id_t>(fet_page - pages_);
//页面的初始化
fet_page->ResetMemory();
fet_page->is_dirty_ = false;
fet_page->page_id_ = page_id;
fet_page->pin_count_++;
disk_manager_->ReadPage(page_id, fet_page->data_);
return fet_page;
}
在project1中,一些难的方法已经把实现过程给上了,照着写就可以了。
bool BufferPoolManagerInstance::DeletePgImp(page_id_t page_id)
这个也挺简单的,我们知道页框在free_list,page_table_,LRU都有用到,所以不要忘记更新
还有就是删除页面的时候也不要忘记将其回到默认值,写回磁盘。
还有两种特殊情况:当前页面不在内存、当前页面被固定了。
记住这些就可以写了。
bool BufferPoolManagerInstance::DeletePgImp(page_id_t page_id) {
// 0. Make sure you call 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.
std::lock_guard<std::mutex> lck(latch_);
if (page_table_.count(page_id) == 0) {
return true;
}
Page *req_page = &pages_[page_table_[page_id]];
if (req_page->GetPinCount() != 0) {
return false;
//该页面当前已经被固定,有人在用
}
//从哈希表中删除
if (req_page->IsDirty()) {
disk_manager_->WritePage(req_page->GetPageId(), req_page->GetData());
}
req_page->ResetMemory();
req_page->is_dirty_ = false;
req_page->pin_count_ = 0;
req_page->page_id_ = INVALID_PAGE_ID;
free_list_.push_back((page_table_[page_id]));
replacer_->Pin(page_table_[page_id]);//Pin函数就是把当前页面从LRU队列中删除,还记得吗?
page_table_.erase(req_page->GetPageId());
DeallocatePage(req_page->GetPageId());
return true;
}
bool BufferPoolManagerInstance::UnpinPgImp(page_id_t page_id, bool is_dirty)
一个线程不再引用一个页了, 将其pin_count减少1, 并通知manager是否该页已经脏了,
有任何异常情况(例如pin_count已经为0了)就返回false, 记得pin_count变为0时调用LRU的unpin
传入的is_dirty只是当前进程有没有修改它,所以当前页面是否为脏的标准是当前页面修改与否以及这个页面原本是不是脏的。
bool BufferPoolManagerInstance::UnpinPgImp(page_id_t page_id, bool is_dirty) {
// 对pin_count的访问肯定要上锁
std::lock_guard<std::mutex> lck(latch_);
if(page_table_.count(page_id)==0){
return false;//当前没有这个页面
}
if (pages_[page_table_[page_id]].pin_count_ == 0) {
return false;
}
Page *temp = &pages_[page_table_[page_id]];
temp->is_dirty_ = is_dirty||temp->IsDirty();
temp->pin_count_--;
if (pages_[page_table_[page_id]].pin_count_ == 0) {
replacer_->Unpin(page_table_[page_id]);
}
return true;
}
其实task2也没有想象中那么难,对吧。不要太低估自己!
Task3:并行缓冲池管理器
这个超级简单,只需要调用task2的内容即可。
不过有两点在我写的时候困扰着我,写完之后也没能很好的理解。昨天经过讨论,现已得出结论。
先说成员变量。成员变量其实按照官网的来就行,还要声明一个指向缓冲管理实例的容器。模版类真是个好东西。泛型编程真是个好东西。
为什么要声明这个容器呢?因为官网中说了
ParallelBufferPoolManager is a class that holds multiple BufferPoolManagerInstances.
ok现在我的问题就出现了:
1、官网中下述的索引是干什么的?
When the ParallelBufferPoolManager is first instantiated it should have a starting index of 0. Every time you create a new page you will try every BufferPoolManagerInstance, starting at the starting index, until one is successful. Then increase the starting index by one.
2、为什么要用父类指针指向子类对象,而不是直接用子类指针。
这个其实就是多态,使得程序更加健壮。
ok让我们来考虑一下这个索引是干什么的。
其实就是我们每次循环都是从1号缓冲管理器开始的对吧,如果没有索引的话,我们只有将1号缓冲管理器中的页面用完了才会去看下一个,并且我每次都要先看一眼1号缓冲管理器。所以索引就是用来增加并发的。让每个管理器都可以工作。
这里只放这个成员方法的实现:
Page *ParallelBufferPoolManager::NewPgImp(page_id_t *page_id) {
// create new page. We will request page allocation in a round robin manner from the underlying
// BufferPoolManagerInstances
// 1. From a starting index of the BPMIs, call NewPageImpl until either 1) success and return 2) looped around to
// starting index and return nullptr
// 2. Bump the starting index (mod number of instances) to start search at a different BPMI each time this function
// is called
Page *page = nullptr;
for (uint32_t i = 0; i < num_instances_; ++i) {
if ((page = BPM_instances[(i + instance_index) % num_instances_]->NewPage(page_id)) != nullptr) {
break;
}
}
instance_index += 1; //初始值为0
return page;
}
补充C++相关知识
1、内联函数:http://t.csdn.cn/mNreg
2、多态:http://t.csdn.cn/kxLpu
3、unordered_map:https://en.cppreference.com/w/cpp/container/unordered_map