Project1 算是进入正式的项目了,做下来感觉整体难度还是挺高的(本人在实现的过程中还没有考虑细粒锁还是花费了大量的时间,有时间还可以优化一下),因为此博客一般就只作为我自己的笔记,再加上2022fall早已结束,所以过程中就贴上源代码。

关于本门课程,由于本人英语听力水平实在有限,所以看的是B站一个中科院老师对于该课程的解读: https://www.bilibili.com/video/BV1bQ4y1Y7iT

好,接下来进入正题

project1 一共有三个任务:

  • 可扩展哈希表(Extendible Hash Table)
  • LRU-K 置换策略(LRU-K Replacement Policy)
  • 缓冲池管理(Buffer Pool Manager)

其中缓冲池的实现会用到前两个

可扩展哈希表(Extendible Hash Table)

首先是我们为什么要引入可扩展哈希表?传统的静态哈希表在储存空间不足时进行扩展会非常麻烦,同时发生碰撞后的处理往往也会导致查找时间的增长。对于动态哈希表例如拉链法实现的哈希表,当某个哈希值对应的元素特别多时,查找的时间复杂度由O(1)退化为遍历的O(n)。
举个极端的例子,如果哈希函数是不管输入是什么都映射为 0,那么就和在第 0 位存储一个链表无异。如何设计散布更加均匀的哈希函数是优化的另一个方向,一种方法是当检测到某个桶中的元素过多时对表进行扩展。扩展最简单的做法是直接将哈希表的长度(桶数)翻倍,再将哈希函数的值域由 [0,n) 改为 [0,2n),然后对所有存储的元素重新算一次哈希值分布到不同的桶中。

这种方法的缺点很明显:如果哈希表中已经存储了大量的元素,因为要对所有元素重算哈希值,扩展的过程会有巨大的计算量,导致一次突发的大延迟。实际上,进行扩展时,可能仅仅是某一个桶出现了拉链很长的状况,其它桶的余量还很充足。于是,出现了可扩展哈希表(Extendible Hash Table)的方案,其将哈希得到的下标与桶改为非一对一映射,并引入全局深度(Global Depth)和局部深度(Local Depth)的概念,实现扩展时只需对达到容量的那一个桶进行分裂,解决了以上问题。

可扩展哈希表的具体原理就不在此赘述了,可以参考下之前B站链接老师的讲解或者下面这篇博客:https://blog.csdn.net/MelroseLbt/article/details/129329316 (这篇博客里有一定的地方没讲清楚,有迷糊可以看一下15445的作业,做一下就懂了)

Bucket类

首先我们要实现ExtendibleHashTable 类的内联类 Bucket 类的结构,Bucket就是可扩展哈希表中的桶,Bucket 使用 std::list 存储元素键值对,我们主要实现三个函数 Find,Remove 和 Insert:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Bucket {
public:
explicit Bucket(size_t size, int depth = 0);

/** @brief Check if a bucket is full. */
inline auto IsFull() const -> bool { return list_.size() == size_; }

/** @brief Get the local depth of the bucket. */
inline auto GetDepth() const -> int { return depth_; }

/** @brief Increment the local depth of a bucket. */
inline void IncrementDepth() { depth_++; }

inline auto GetItems() -> std::list<std::pair<K, V>> & { return list_; }

/* @brief Find the value associated with the given key in the bucket.
*/
auto Find(const K &key, V &value) -> bool;

/* @brief Given the key, remove the corresponding key-value pair in the bucket.
*/
auto Remove(const K &key) -> bool;

/*
* @brief Insert the given key-value pair into the bucket.
* 1. If a key already exists, the value should be updated.
* 2. If the bucket is full, do nothing and return false.
*/
auto Insert(const K &key, const V &value) -> bool;

private:
size_t size_;
int depth_; // local_depth
std::list<std::pair<K, V>> list_;
};

没有什么难点,照着注释敲就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
template <typename K, typename V>
auto ExtendibleHashTable<K, V>::Bucket::Find(const K &key, V &value) -> bool {
for (auto it = list_.begin(); it != list_.end(); ++it) {
if (it->first == key) {
value = it->second;
return true;
}
}
return false;
}

template <typename K, typename V>
auto ExtendibleHashTable<K, V>::Bucket::Remove(const K &key) -> bool {
for (auto it = list_.begin(); it != list_.end(); ++it) {
if (it->first == key) {
list_.erase(it);
return true;
}
}
return false;
}

template <typename K, typename V>
auto ExtendibleHashTable<K, V>::Bucket::Insert(const K &key, const V &value) -> bool {
for (auto it = list_.begin(); it != list_.end(); ++it) {
if (it->first == key) {
it->second = value;
return true;
}
}
if (!IsFull()) {
list_.emplace_back(key, value);
return true;
}
return false;
}

ExtendibleHashTable类

1
2
3
4
5
int global_depth_;    // The global depth of the directory
size_t bucket_size_; // The size of a bucket
int num_buckets_; // The number of buckets in the hash table
mutable std::mutex latch_;
std::vector<std::shared_ptr<Bucket>> dir_; // The directory of the hash table

以上是ExtendibleHashTable类的成员变量,注意dir_是桶的指针数组,因为可扩展哈希表中会有多个指针指向同一个桶,所以用 shared_ptr

获取键对应的哈希值下标函数,源代码已经给出了:

1
2
3
4
5
6
template <typename K, typename V>
auto ExtendibleHashTable<K, V>::IndexOf(const K &key) -> size_t {
int mask = (1 << global_depth_) - 1;
// eg: gd = 2 , 1 << 2 = 100 , 100 -1 = 011 , this is we want
return std::hash<K>()(key) & mask;
}

首先我们自己先实现一个创建桶的函数:

1
2
3
auto ExtendibleHashTable<K, V>::NewBucket(int local_depth) -> std::shared_ptr<Bucket> {
return std::make_shared<Bucket>(bucket_size_, local_depth);
}

在该类中,我们也主要实现Find,Remove 和 Insert这三个函数

对于Find函数,利用哈希函数得到键所对应的下标值找到对应的桶然后让桶查找即可。

1
2
3
4
5
6
template <typename K, typename V>
auto ExtendibleHashTable<K, V>::Find(const K &key, V &value) -> bool {
std::scoped_lock<std::mutex> lock(latch_);
size_t index = IndexOf(key);
return dir_[index]->Find(key, value);
}

由于Remove 时不用考虑空桶的合并收缩,因此也是找到桶然后让桶 Remove 即可:

1
2
3
4
5
6
template <typename K, typename V>
auto ExtendibleHashTable<K, V>::Remove(const K &key) -> bool {
std::scoped_lock<std::mutex> lock(latch_);
size_t index = IndexOf(key);
return dir_[index]->Remove(key);
}

重点是Insert函数,Insert函数的逻辑如下:

  • 首先判断表中是否已经有了该键,有则更新其值
  • 如果没有,则要判断该键所对应的桶是否满了,如果没满直接插入即可,否则:
    • 先判断local_depth是否等于global_depth_,如果相等则需要将目录dir_扩容成原来的两倍,并且将后半部分扩容部分的指针依次指向前半部分所指的shared_ptr
    • 创建两个新桶,将旧桶里的元素分发到这两个新桶中
    • 调整dir_中的指针指向这两个新桶
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
template <typename K, typename V>
void ExtendibleHashTable<K, V>::Insert(const K &key, const V &value) {
std::scoped_lock<std::mutex> lock(latch_);
size_t index = IndexOf(key);

// If a key already exists, the value should be updated.
V val; // no use,just for calling Bucket::Find()
if (dir_[index]->Find(key, val)) {
dir_[index]->Insert(key, value);
return;
}

// If the bucket is full
while (dir_[index]->IsFull()) {
int local_depth = dir_[index]->GetDepth();
// local_depth == global_depth_ , double dir size
if (local_depth == global_depth_) {
size_t dir_size = dir_.size();
dir_.reserve(2 * dir_size);
std::copy_n(dir_.begin(), dir_size,
std::back_inserter(dir_)); // Copy the shared_ptr directly from the first half to the second half
++global_depth_; // that's why we need to updata index = IndexOf(key) in the end of the while loop
}

// make two new buckets
auto b0 = NewBucket(local_depth + 1);
auto b1 = NewBucket(local_depth + 1);
++num_buckets_; // only add 1 , because the old Bucket will be replaced by the new Bucket
int local_mask = 1 << local_depth;
/*Since the elements belong to the same bucket, the original index (k) must be
the same. Therefore, we only need to compare the 'local_depth' at that position
*/

// redistribute items from old bucket
for (auto &[k, v] : dir_[index]->GetItems()) {
size_t item_hash = std::hash<K>()(k); // ExtendibleHashTable::indexOf() use std::hash<K>()
if (static_cast<bool>(item_hash & local_mask)) {
b1->Insert(k, v);
} else {
b0->Insert(k, v);
}
}
// adjust dir_ points to the old bucket
for (size_t i = (std::hash<K>()(key) & (local_mask - 1)); i < dir_.size(); i += local_mask) {
if (static_cast<bool>(i & local_mask)) {
dir_[i] = b1;
} else {
dir_[i] = b0;
}
}
// updata index of the incoming item after split
index = IndexOf(key);
}
dir_[index]->Insert(key, value);
}

测试

之前贴的博客,博主写了几个测试样例,可以加进去测一下

1
2
make extendible_hash_table_test -j2
./test/extendible_hash_table_test

LRU-K 置换策略(LRU-K Replacement Policy)

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。可以在做该项目之前先完成力扣的146. LRU 缓存,该题中采用双向链表和哈希表来完成该机制。

但是传统的LRU算法存在一定的缺陷:缓存污染问题。例如 Cache 容量是 3,存有较常访问的 [A,B,C],此时有一个访问序列 D,E,F 到来,就会把 ABC 全踢掉,即使 DEF 以后可能再也不会出现。

为了解决这个缺陷,LRU-K算法应运而生。LRU-K算法在LRU算法的基础上,增加了缓存项在缓存中存在的时间,即K值,在该实验中我们用缓存页的访问次数代替存在时间,即hit_count。我们使用两个队列来实现LRU-K算法,一个队列history_list_储存hit_count < k 的页面,另一个队列cache_list_储存hit_count >= k 的页面。在遇到需要淘汰页面时,首先利用FIFO策略淘汰history_list_队首的页面,如果history_list_队列没有可淘汰的页面,则利用LRU策略淘汰cache_list_中的页面。为了达到O(1)的时间效率,我们需要用unordered_map来存储页和他在队列中的迭代器的映射关系。同时,在淘汰页面的过程中我们要注意evictable_,是否可淘汰的问题。

综上,我们构造LRUKReplacer的类成员如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// add this struct to record the status of the frame
struct FrameEntry {
size_t hit_count_{0};
bool evictable_{false};
std::list<frame_id_t>::iterator pos_;
};
std::list<frame_id_t> history_list_; // hit_count_ < k , replace use FIFO
std::list<frame_id_t> cache_list_; // hit_count_ >= k , only when historyList is empty replace use LRU
std::unordered_map<frame_id_t, FrameEntry> entries_;

size_t curr_size_{0}; // cache size
size_t replacer_size_; // frame_id need to be smaller than replacer_size_
size_t k_;
std::mutex latch_;

注意在这里,evictable_初始化为false,笔者则是这里初始化为true,在task3 buffer pool出现了bug,并且不易于理清逻辑。

curr_size_实际上是指可以被替换的页的数量

LRUKReplacer 实现

该类中我们主要需要实现四个成员函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// @brief Find the frame with largest backward k-distance and evict that frame. Only frames 
// that are marked as 'evictable_' are candidates for eviction.
auto Evict(frame_id_t *frame_id) -> bool;

// @brief Record the event that the given frame id is accessed at current timestamp.
// Create a new entry for access history if frame id has not been seen before.
void RecordAccess(frame_id_t frame_id);

// @brief Toggle whether a frame is evictable_ or non-evictable_. This function also
// controls replacer's size. Note that size is equal to number of evictable_ entries.
void SetEvictable(frame_id_t frame_id, bool set_evictable_);

// @brief Remove an evictable_ frame from replacer, along with its access history.
// This function should also decrement replacer's size if removal is successful.
void Remove(frame_id_t frame_id);

Evict()
比较简单

  • 首先判断curr_size_是否为0,如果为0,则直接返回false,否则:
    • 从队尾开始遍历history_list_,没找到就从队尾开始遍历cache_list_
      两个队列中第一个可驱逐的就是满足条件的,之所以能这样做是因为在RecordAccess()中保证了这一点。
      然后就是注意反向迭代器的删除:history_list_.erase(std::next(rit).base());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
auto LRUKReplacer::Evict(frame_id_t *frame_id) -> bool {
std::scoped_lock<std::mutex> lock(latch_);
if (curr_size_ == 0) {
return false;
}
if (!history_list_.empty()) {
for (auto rit = history_list_.rbegin(); rit != history_list_.rend(); ++rit) { // fisrt in first out
if (entries_[*rit].evictable_) {
*frame_id = *rit;
history_list_.erase(std::next(rit).base()); // erase的入参只接受正向迭代器
entries_.erase(*frame_id);
--curr_size_;
return true;
}
}
}
if (!cache_list_.empty()) {
for (auto rit = cache_list_.rbegin(); rit != cache_list_.rend();
++rit) { // in RecordAccess() ,we have insure the first frame is the latest one
if (entries_[*rit].evictable_) {
*frame_id = *rit;
cache_list_.erase(std::next(rit).base());
entries_.erase(*frame_id);
--curr_size_;
return true;
}
}
}
return false;
}

RecordAccess()

  • 首先判断frame_id是否越界,如果越界,则抛出异常
  • 然后更改++hit_count_,由于unordered_map的性质,如果entries_中没有该页,他会自动初始化一个
  • 然后判断hit_count_的值,判断是否需要移入到cache_list_中,如果已经在cache_list_中也需要将其移动到队首以保证LRU策略

注意到就算有新页进来curr_size_也不能加一,因为新页默认是不可驱逐的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void LRUKReplacer::RecordAccess(frame_id_t frame_id) {
std::scoped_lock<std::mutex> lock(latch_);
if (frame_id > static_cast<frame_id_t>(replacer_size_)) {
throw std::invalid_argument(std::string("Invalid frame_id: " + std::to_string(frame_id)));
}
// for new frames , default hit count is 0

size_t count = ++entries_[frame_id].hit_count_; // 新页在此添加进entries
if (count == 1) { // new frame
// LOG_INFO("%d NEW FRAME",frame_id);
// ++curr_size_; // bug1,不应该++,因为默认的evictable_是false
history_list_.emplace_front(frame_id);
entries_[frame_id].pos_ = history_list_.begin();
} else {
if (count == k_) { // reach k-hit , move from history_list_ to the front of cache_list_
// LOG_INFO("No.%d FRAME MOVE FROM HIST TO CACHE" , frame_id);
history_list_.erase(entries_[frame_id].pos_);
cache_list_.emplace_front(frame_id);
entries_[frame_id].pos_ = cache_list_.begin();
} else if (count > k_) { // move to the front of cache_list_ , insure the LRU
cache_list_.erase(entries_[frame_id].pos_);
cache_list_.emplace_front(frame_id);
entries_[frame_id].pos_ = cache_list_.begin();
}
}
// count < k , keep in history_list_ , do not move because the FIFO
// LOG_INFO("RECORD: %d" , frame_id);
}

SetEvictable()
该函数用于调整evictable_属性,比较简单,唯一要注意的就是对于curr_size_值的影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void LRUKReplacer::SetEvictable(frame_id_t frame_id, bool set_evictable_) {
std::scoped_lock<std::mutex> lock(latch_);
if (frame_id > static_cast<frame_id_t>(replacer_size_)) {
throw std::invalid_argument(std::string("Invalid frame_id: " + std::to_string(frame_id)));
}
if (entries_.find(frame_id) == entries_.end()) {
return;
}
if (set_evictable_ && !entries_[frame_id].evictable_) {
curr_size_++;
} else if (!set_evictable_ && entries_[frame_id].evictable_) {
curr_size_--;
}
entries_[frame_id].evictable_ = set_evictable_;
}

Remove()
逻辑比较简单,看代码即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void LRUKReplacer::Remove(frame_id_t frame_id) {
std::scoped_lock<std::mutex> lock(latch_);
if (frame_id > static_cast<frame_id_t>(replacer_size_)) {
throw std::invalid_argument(std::string("Invalid frame_id: " + std::to_string(frame_id)));
}
if (entries_.find(frame_id) == entries_.end()) {
return;
}
if (!entries_[frame_id].evictable_) {
throw std::logic_error(std::string("Can't remove an inevictable_ frame: " + std::to_string(frame_id)));
}
if (entries_[frame_id].hit_count_ < k_) {
history_list_.erase(entries_[frame_id].pos_);
} else {
cache_list_.erase(entries_[frame_id].pos_);
}
--curr_size_;
entries_.erase(frame_id);
}

缓冲池管理(Buffer Pool Manager)

Buffer Pool Manager 简单来说就是充当数据库上层设施和磁盘文件间的缓冲区,类似于 Cache 在 CPU 和内存间的作用。bustub 中有 Page 和 Frame 的概念,Page 是承载 4K 大小数据的类,可以通过 DiskManager 从磁盘文件中读写,带有 page_id 编号,is_dirty 标识等信息。Frame 不是一个具体的类,而可以理解为 Buffer Pool Manager(以下简称 BPM)中容纳 Page 的槽位,具体来说,BPM 中有一个 Page 数组,frame_id 就是某个 Page 在该数组中的下标。

外界只知道 page_id,向 BPM 查询时,BPM 要确定该 Page 是否存在以及其位置,所以要维护一个 page_id 到 frame_id 的映射,这里就使用我们刚完成的 ExtendibleHashTable。为区分空闲和占用的 Page,维护一个 free_list_,保存空闲的 frame_id。初始状态,所有 Page 都是空闲的。当上层需要取一个 Page 时,如果 Page 已存在于 BP 中,则直接返回;否则需要从磁盘读取到 BP 中。此时优先取空闲的 Page,否则只能从所有已经占用的 Page 中用我们刚完成的 LRUKReplacer 决定踢出某个 Page。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @brief Creates a new BufferPoolManagerInstance.
* @param pool_size the size of the buffer pool
* @param disk_manager the disk manager
* @param replacer_k the lookback constant k for the LRU-K replacer
* @param log_manager the log manager (for testing only: nullptr = disable logging). Please ignore this for P1.
*/
BufferPoolManagerInstance(size_t pool_size, DiskManager *disk_manager, size_t replacer_k = LRUK_REPLACER_K,
LogManager *log_manager = nullptr);

/** Number of pages in the buffer pool. */
const size_t pool_size_;
/** The next page id to be allocated */
std::atomic<page_id_t> next_page_id_ = 0;
/** Bucket size for the extendible hash table */
const size_t bucket_size_ = 4;

/** Array of buffer pool pages. */
Page *pages_; // frame_id在这个数组中起到index的作用,可以就理解为内存中的页框(Page Frame,或页帧)
/** Pointer to the disk manager. */
DiskManager *disk_manager_ __attribute__((__unused__));
/** Pointer to the log manager. Please ignore this for P1. */
LogManager *log_manager_ __attribute__((__unused__));
/** Page table for keeping track of buffer pool pages. */
ExtendibleHashTable<page_id_t, frame_id_t> *page_table_;
/** Replacer to find unpinned pages for replacement. */
LRUKReplacer *replacer_;
/** List of free frames that don't have any pages on them. */
std::list<frame_id_t> free_list_;
/** This latch protects shared data structures. We recommend updating this comment to describe what it protects. */
std::mutex latch_;

3. BufferPoolManagerInstance 类实现

在该类中主要要实现以下几个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// @brief Create a new page in the buffer pool. Set page_id to the new page's id, or nullptr if all frames
// are currently in use and not evictable (in another word, pinned).
auto NewPgImp(page_id_t *page_id) -> Page * override;

// @brief Fetch the requested page from the buffer pool. Return nullptr if page_id needs to be fetched from the disk
// but all frames are currently in use and not evictable (in another word, pinned).
auto FetchPgImp(page_id_t page_id) -> Page * override;

// @brief Unpin the target page from the buffer pool. If page_id is not in the buffer pool or its pin count is already 0, return false.
auto UnpinPgImp(page_id_t page_id, bool is_dirty) -> bool override;

// @brief Flush the target page to disk.
// Use the DiskManager::WritePage() method to flush a page to disk, REGARDLESS of the dirty flag.
// Unset the dirty flag of the page after flushing.
auto FlushPgImp(page_id_t page_id) -> bool override;

// @brief Flush all the pages in the buffer pool to disk
void FlushAllPgsImp() override;

// @brief Delete a page from the buffer pool. If page_id is not in the buffer pool, do nothing and return true. If the
// page is pinned and cannot be deleted, return false immediately.
auto DeletePgImp(page_id_t page_id) -> bool override;

NewPgImp()
逻辑很简单,获取一个空闲页或者利用LRU-K淘汰一个页,然后利用这个页号来新建。
注意在初始化的过程中replacer_->RecordAccess(frame_id);必须在replacer_->SetEvictable(frame_id, false);之前,因为在SetEvictable中的if(entries.find(frame_id) == entries.end())判断时,此时还没有entries条目,所以会直接返回故会出bug。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
auto BufferPoolManagerInstance::NewPgImp(page_id_t *page_id) -> Page * {
std::scoped_lock<std::mutex> lock(latch_);
frame_id_t frame_id;
if (!GetAvailableFrame(&frame_id)) {
// LOG_INFO("false: %d#",frame_id);
page_id = nullptr;
return nullptr;
}
// LOG_INFO("true: %d#",frame_id);
page_id_t new_page_id = AllocatePage();
// reset page 重置内存、元数据;更新 buffer pool
pages_[frame_id].page_id_ = new_page_id;
pages_[frame_id].ResetMemory();
pages_[frame_id].pin_count_ = 0;
pages_[frame_id].is_dirty_ = false;

page_table_->Insert(new_page_id, frame_id);
// "Pin" the frame by calling replacer.SetEvictable(frame_id, false)
/* RecordAccess必须在SetEvictable之前
因为在SetEvictable中的if(entries.find(frame_id) == entries.end())
,此时还没有entries条目,所以会直接返回故会报错 */
replacer_->RecordAccess(frame_id);
replacer_->SetEvictable(frame_id, false);
pages_[frame_id].pin_count_++;
// record the access history

*page_id = new_page_id;
return &pages_[frame_id];
}

该函数中的GetAvailableFrame(&frame_id)是我添加的一个函数,用于在free_list_中获取空闲页框或者利用LRU-K淘汰一个页:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// add a function to GetAvailableFrame
auto BufferPoolManagerInstance::GetAvailableFrame(frame_id_t *avilFrameId) -> bool {
frame_id_t frame_id;
if (!free_list_.empty()) {
// LOG_INFO("free_list_: %d#",frame_id);
frame_id = free_list_.front();
free_list_.pop_front();
*avilFrameId = frame_id;
return true;
}
// no free frame , find a replacement
if (replacer_->Evict(&frame_id)) {
// LOG_INFO("replacement: %d#",frame_id);
if (pages_[frame_id].IsDirty()) {
disk_manager_->WritePage(pages_[frame_id].GetPageId(), pages_[frame_id].GetData());
pages_[frame_id].is_dirty_ = false;
}
page_table_->Remove(pages_[frame_id].GetPageId());
*avilFrameId = frame_id;
return true;
}
return false;
}

FetchPgImp()
与NewPgImp类似,如果没有这个页就从磁盘读取并在BPM中加入该页,改变LRU-k中的访问记录和evictable_

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
auto BufferPoolManagerInstance::FetchPgImp(page_id_t page_id) -> Page * {
std::scoped_lock<std::mutex> lock(latch_);
frame_id_t frame_id;
if (!page_table_->Find(page_id, frame_id)) {
if (!GetAvailableFrame(&frame_id)) {
return nullptr; // cache中没有 同时也没有frame可以获取
}
pages_[frame_id].page_id_ = page_id;
pages_[frame_id].ResetMemory();
pages_[frame_id].pin_count_ = 0;
pages_[frame_id].is_dirty_ = false;
disk_manager_->ReadPage(page_id, pages_[frame_id].data_);
page_table_->Insert(page_id, frame_id);
}
replacer_->RecordAccess(frame_id);
replacer_->SetEvictable(frame_id, false);
pages_[frame_id].pin_count_++;
return &pages_[frame_id];
}

UnpinPgImp()
看代码即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
auto BufferPoolManagerInstance::UnpinPgImp(page_id_t page_id, bool is_dirty) -> bool {
std::scoped_lock<std::mutex> lock(latch_);
frame_id_t frame_id;
// If page_id is not in the buffer pool or its pin count is already 0, return false
if (!page_table_->Find(page_id, frame_id)) {
return false;
}
if (pages_[frame_id].pin_count_ <= 0) {
return false;
}
if (--pages_[frame_id].pin_count_ == 0) {
replacer_->SetEvictable(frame_id, true);
}
if (is_dirty) {
pages_[frame_id].is_dirty_ = is_dirty;
}
return true;
}

剩下的逻辑也很简单,看代码即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
auto BufferPoolManagerInstance::FlushPgImp(page_id_t page_id) -> bool {
std::scoped_lock<std::mutex> lock(latch_);
frame_id_t frame_id;
if (!page_table_->Find(page_id, frame_id)) {
return false;
}
disk_manager_->WritePage(page_id, pages_[frame_id].data_);
pages_[frame_id].is_dirty_ = false;
return true;
}

void BufferPoolManagerInstance::FlushAllPgsImp() {
for (size_t i = 0; i < pool_size_; ++i) {
if (pages_[i].GetPageId() == INVALID_PAGE_ID) {
continue;
}
FlushPgImp(pages_[i].GetPageId());
}
}

auto BufferPoolManagerInstance::DeletePgImp(page_id_t page_id) -> bool {
std::scoped_lock<std::mutex> lock(latch_);
frame_id_t frame_id;
if (!page_table_->Find(page_id, frame_id)) {
return true;
}
if (pages_[frame_id].GetPinCount() > 0) {
return false;
}
// 在页表中删除
page_table_->Remove(page_id);
// 在replace_移除对frame_id的跟踪
replacer_->Remove(frame_id);
// 在free_list_中插入
free_list_.emplace_back(frame_id);
// 重置BPM中的页框
pages_[frame_id].page_id_ = INVALID_PAGE_ID;
pages_[frame_id].ResetMemory();
pages_[frame_id].pin_count_ = 0;
pages_[frame_id].is_dirty_ = false;

DeallocatePage(page_id);
return true;
}