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