CMU15445 2022 project1: 缓存池(可扩展哈希+LRU-K)

项目中涉及到的C++语法

C++11中尾返回类型

auto Find(const K &key, V &value) -> bool; 是 C++11 中的函数声明方式,采用了 尾返回类型(trailing return type) 的语法。
在函数声明中,auto 表示函数的返回类型由尾部的 -> bool 指定。
这样写的好处是,当返回类型较复杂时,代码会更加清晰。
函数返回类型是布尔值(bool)。

explicit 的作用

当一个构造函数标记为 explicit 时,编译器不允许通过单参数的构造函数进行隐式类型转换。只有显式调用构造函数时,才能创建对象。
MyClass obj = 42; 会隐式调用构造函数 MyClass(int value)。
将构造函数声明为 explicit 后,编译器禁止 MyClass obj = 42; 这种隐式转换。
必须显式调用构造函数 MyClass obj(42);。

STL中list的使用

list不支持随机访问[],但可以通过迭代器遍历。

在C++标准模板库(STL)中,std::list 是一个双向链表容器,适用于频繁的插入和删除操作。
std::list<int> myList;
std::list<int> myList = {1, 2, 3, 4, 5};
std::list<int> myList(5, 10); // 5个元素,每个值为10
myList.push_back(6);
myList.push_front(0);
//插入到指定位置
auto it = myList.begin(); // 指向第一个元素
std::advance(it, 2);     // 移动到第3个位置
myList.insert(it, 99);   // 在第3个位置插入99
删除所有匹配的元素:
myList.remove(99); // 删除值为99的所有元素
//排序
myList.sort();
myList.sort([](int a, int b) { return a > b; }); // 降序排序

myList.reverse();
myList.clear();
//合并两个链表
std::list<int> otherList = {7, 8, 9};
myList.merge(otherList); // 合并后otherList变为空

分配内存

Bucket first_bucket = new Bucket; 不正确,因为 new Bucket 返回的是指针。
根据需求选择合适的方法:
栈上分配:Bucket first_bucket;。
堆上分配:Bucket* first_bucket = new Bucket();,记得 delete。
使用智能指针:std::shared_ptr 或 std::unique_ptr。

如果要在堆上分配内存,但不想手动管理其生命周期,使用智能指针
std::shared_ptr<Bucket> first_bucket = std::make_shared<Bucket>();
// 或
std::unique_ptr<Bucket> first_bucket = std::make_unique<Bucket>();

定义和声明

在 C++ 中,定义和声明是两个重要的概念,它们在语法和功能上有所不同。理解它们的区别对于编写模块化代码和处理编译器的错误至关重要。

  1. 声明(Declaration)
    声明告诉编译器某个变量、函数或类的存在及其基本信息(如类型、参数等),但不会为它分配内存或提供实现。使编译器了解某些符号(变量、函数、类)的存在,以便在使用它们时不会报错。
    特点
  • 仅提供“接口”信息。
  • 不生成代码,只是让编译器知道某个实体的类型或存在。
  • 通常出现在头文件或 .h 文件中。
  1. 定义(Definition)
    定义为变量、函数或类提供具体的实现。这不仅让编译器知道它的存在,还会为变量分配内存,或为函数提供具体实现。

特点

  • 生成代码或分配内存。
  • 变量定义会分配存储空间。
  • 函数定义提供函数体。
  • 类定义提供成员变量和方法的实现。

override关键字

auto Remove(const K &key) -> bool override;
override 是 C++11 引入的关键字,标记该函数是重写基类的虚函数。
提高代码的可读性,让其他开发者一眼就能看出这是一个重写函数。

C++ 风格的 Cast

static_cast<size_t>强制类型转换

int x = 5;
double y = static_cast<double>(x); // C++ style cast

const int x = 5;
int* y = const_cast<int*>(&x); // 去掉 const

位运算

// 创建一个掩码,最低的n位是1,其他位是0
size_t mask = (1 << n) - 1;

// 使用按位与运算获取最低n位
size_t lowest_n_bits = key_index & mask;
1 << n 将 1 左移 n 位,得到一个值,其最低的 n 位是 1,其他位是 0。例如,当 n = 5 时,1 << 5 的结果是 32,即二进制为 100000。
(1 << n) - 1 会将掩码的最低 n 位设为 1,其他位设为 0。例如,当 n = 5 时,掩码是 11111,即二进制为 31。
key_index & mask 会将 key_index 的最低 n 位与掩码进行按位与运算,保留最低的 n 位,其他位设置为 0。

取数据类型最大值

size_t victim_timestamp=std::numeric_limits<size_t>::max();

多线程

多线程安全的背景
在哈希表这种数据结构中,多个线程可能同时进行插入、删除、查找等操作。如果没有适当的同步机制,可能会导致以下问题:

数据竞争(Data Race):多个线程同时修改同一个数据结构,导致数据不一致。
不一致的视图:一个线程在读取数据时,另一个线程修改了数据,导致读取到的值不可靠。

为了避免这些问题,通常使用锁(如 std::mutex)来同步线程之间的操作。例如,在 GetNumBuckets() 中加锁,确保在获取当前桶的数量时,其他线程无法修改哈希表的状态,从而保证返回值的一致性。
latch_ 这个 std::mutex 是 ExtendibleHashTable 的成员,它保护整个哈希表的数据结构,因此,当前代码锁的是整个哈希表实例。

//latch_ 是一个 std::mutex 对象,用来保护对哈希表内部状态(如 num_buckets_)的访问。
//std::scoped_lock 是一种 RAII 风格的锁,它会在作用域结束时自动解锁
std::scoped_lock<std::mutex> lock(latch_);
return GetNumBucketsInternal();

锁的策略
如果多个线程可能并发修改哈希表的数据(如插入、删除),这些操作就需要加锁。
读取操作如果会读取到共享数据(如桶的数量、某个桶的内容等),也需要加锁。
不同的加锁策略可以优化性能,比如细粒度锁或读写锁,视具体的应用场景和并发需求而定。

哈希策略
mask 的作用是将哈希值限制在某一范围内。在你的代码中,mask 是通过 1 << global_depth_ 计算的,并减去 1,这样得到的 mask 具有 global_depth_ 个最低位是 1,其他位是 0。这意味着:

std::hash()(key) & mask 只保留哈希值的最低 global_depth_ 位,抛弃其他高位。这是为了确保哈希表的索引是根据 global_depth_ 动态调整的,使得哈希表能够正确地管理桶的划分

可扩展哈希

主要类与成员

ExtendibleHashTable 类:
模板参数 K 和 V 分别代表键和值的类型。
继承自 HashTable<K, V>,是具体的哈希表实现。
包括管理全局深度(global_depth_)、桶大小(bucket_size_)以及桶数量(num_buckets_)的私有成员。

  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
  1. Find:
  • 寻找指定键对应的值。
  • 如果找到,返回 true,并通过参数返回值。
  • 使用 IndexOf(key) 来定位键对应的桶索引。
  1. Insert:

插入键值对,如果键已存在,更新值。
当桶满时,可能需要:

  • 扩展目录大小并增加全局深度。
  • 增加桶的局部深度。
  • 拆分桶并重新分配键值对。
  1. Remove:
  • 删除指定键对应的键值对。
  • 仅移除,不需要合并或缩小桶。

Bucket 类:
表示一个哈希表中的桶。
包含了与桶的深度(depth_)、存储容量(size_)以及存储数据的列表(list_)相关的逻辑。
TODO:
find: 在桶中查找键。
insert: 插入键值对(若桶满则返回 false)。
Remove: 删除键值对。

编译Debug

1.

template <typename K, typename V>
auto ExtendibleHashTable<K, V>::Bucket::Remove(const K &key) -> bool {
  // UNREACHABLE("not implemented");
  for(const auto& pair:list_){
        if(pair.first==key){ 
          list_.remove(pair);
          return true;
        }
      }
      return false;
}

代码乍一看没有问题,但是clang+clangd给出了很奇怪的报错

Replace loop by 'std::any_of()'clang-tidy(readability-use-anyofallof)

问题分析:
std::list 是双向链表:当你在 Remove 方法中使用 for (const auto& pair : list_) 时,它实际上是一个基于范围的循环,底层实现会不断解引用迭代器,这对于链表来说是可以的。但问题出现在你试图删除正在遍历的元素。

list_.remove(pair) 的问题:在 std::list 中,remove 会遍历整个列表并删除与 pair 匹配的所有元素。你在 for 循环中试图使用 remove 来删除元素,但 remove 本质上需要一个元素来比较,并且一旦在遍历过程中调用删除操作,list_ 内部结构可能会被改变,导致遍历发生错误。
解决方案:
使用 std::list::erase 来删除元素。erase 可以安全地删除遍历过程中找到的元素。
在删除元素时,我们不能直接用 for 循环,因为如果在循环中删除了元素,后续的元素会移动到之前的位置,从而改变迭代器的状态。正确的方法是用 erase 删除元素,并确保我们处理好迭代器。

2.

你遇到的错误提示 Non-const lvalue reference to type 'List_iterator<…>’ cannot bind to a temporary of type 'List_iterator<…>’ 是因为 std::list::erase 返回的是一个临时对象(返回值是一个新的迭代器)。在 iter = list.erase(iter) 中,iter 是一个非 const 的左值引用,而 list.erase 返回的是一个临时的右值迭代器,编译器不允许将临时右值绑定到一个非 const 的左值引用。

3.

for(size_t i=0;i<dir_.size();i++){
     dir_.push_back(dir_[i]);
      }

如果这么写,毫无疑问会导致死循环,正确的做法是先用变量保存目录的大小

size_t dir_size=dir_.size();
for(size_t i=0;i<dir_size;i++){
    dir_.push_back(dir_[i]);
      }

测试Debug

1.find和remove函数中错误地将目录大小用桶的数量来表示

运行所有禁用的测试
./test/extendible_hash_table_test --gtest_also_run_disabled_tests

第一次test
DISABLED_SampleTest只有几个样例没通过
DISABLED_ConcurrentInsertTest样例不通过集中在65 和66

进行修改:
将类ExtendibleHashTable中的Find和Remove方法进行了调整:

//将代码变更
if(static_cast<size_t>(num_buckets_)<key_index+1){
  return false;
  }
//下面的才是对的,因为会出现多个目录指针指向同一个桶的情况,dir_.size()并不等于桶的个数num_buckets_
if (key_index >= dir_.size()) {
    return false;
}

2.key_index没有重新计算

SampleTest的测试全部通过,ConcurrentInsertTest的测试依旧没有通过
说明我的代码没有处理好并发问题。
还是未能解决。
按理说我的insert函数在一开始就锁住了,同一时间只有一个线程能insert,不应该会有问题才对。

找到bug了:
确保在分裂后重新计算 key_index
在每次分裂后,重新计算 key_index 以反映最新的目录结构。这可以防止在分裂后使用旧的 key_index 导致错误的分裂。
因为key_index的计算机是基于global_depth的,当global_depth改变时,key_index也需要重新计算。

while(true)
  {
    size_t key_index=IndexOf(key);
    if(dir_[key_index]->Insert(key,value)) {break;}

3. 地址溢出问题

dir_[key_index+(1<<(dir_[key_index]->GetDepth()-1))]=new_bucket;

需要更新指针的桶不止一个,应遍历,全部更新。

LRU-K替换策略

数据结构选型

采用了:

std::unordered_map<frame_id_t, std::list<size_t>> access_history_;//保存每个frame的访问历史
优化成了:
std::unordered_map<frame_id_t, std::deque<size_t>> access_history_;
std::unordered_set<frame_id_t> evictable_frames_;//保存可以逐出的帧集合

但是使用list双向链表来维护访问历史的话,Evict函数中,遍历evictable_frames_时,对于每个帧均要循环k次找到记录时间,平方级复杂度。
所以改成了deque。
std::list:非常适合插入和删除操作,尤其是中间位置的插入删除(通过迭代器),时间复杂度为 O(1)。但是,随机访问的性能较差,需要 O(n) 的时间来遍历。
std::deque:支持高效的随机访问(O(1)),并且在头尾插入和删除也非常高效。但是,在中间插入和删除时性能较差,需要 O(n) 的时间。
因此,如果你需要经常在容器的两端进行插入或删除操作,且不太需要随机访问元素,std::list 是一个不错的选择。如果你需要在两端进行快速插入删除,并且还需要高效的随机访问,std::deque 会是更好的选择。

操作std::liststd::vectorstd::deque
访问元素O(n)(需要遍历链表)O(1)(随机访问)O(1)(随机访问)
插入到尾部O(1)O(1)(如果没有扩容)O(1)(常数时间)
插入到头部O(1)O(n)(需要移动元素)O(1)(常数时间)
插入到中间O(1)(需要给出迭代器)O(n)(需要移动元素)O(n)(需要移动块)
删除尾部元素O(1)O(1)(常数时间)O(1)(常数时间)
删除头部元素O(1)O(n)(需要移动元素)O(1)(常数时间)
删除中间元素O(1)(需要给出迭代器)O(n)(需要移动元素)O(n)(需要移动块)
空间效率O(n)(每个节点有前后指针)O(n)(元素连续存储)O(n)(块内元素连续,块间可能不连续)
内存访问模式不连续(频繁缓存不命中)连续(较高的缓存命中率)每个块内连续,块间不连续(缓存命中率较高)
扩容时性能O(1)(链表的插入不受影响)O(n)(需要复制元素到新内存)O(n)(可能需要扩展多个块)
适用场景频繁插入删除,低访问要求高效随机访问,低插入删除要求高效头尾操作,支持随机访问

Debug

1.指针类型传参数

auto LRUKReplacer::Evict(frame_id_t *frame_id) -> bool {}

对于函数参数为指针类型,如果要赋值:

frame_id = &victim;//这样写并不对
作用:改变 frame_id 指针,使其指向 victim 的地址。
意义:仅在函数内部有效,调用者的指针不会被修改,因为传递的是指针的拷贝。

*frame_id = victim;//应该这样写
作用:将 victim 的值赋给 frame_id 所指向的内存位置。
意义:修改调用者传入的变量,使其值为 victim。

2. Evict中access_history_清除历史

没有注意在Evict函数中,逐出页时,也要把对应历史清除掉

access_history_.erase(victim);

3. 并发的bug(其实没有,是测试用例的问题)***

在提交到https://www.gradescope.com/网站进行评分时,LRUKReplacerTest.Size.ASAN (0/6)测试用例一直无法通过。
为了找出他的问题,我使用了GPT提供的测试用例。

TEST(LRUKReplacerTest, ConcurrentAccess) {
  LRUKReplacer lru_replacer(100, 2);

  auto set_evictable = [&lru_replacer](int start, int end) {
    for (int i = start; i < end; ++i) {
      lru_replacer.SetEvictable(i, true);
    }
  };

  auto remove_evictable = [&lru_replacer](int start, int end) {
    for (int i = start; i < end; ++i) {
      lru_replacer.SetEvictable(i, false);
    }
  };

  std::thread t1(set_evictable, 0, 50);
  std::thread t2(set_evictable, 50, 100);
  std::thread t3(remove_evictable, 25, 75);

  t1.join();
  t2.join();
  t3.join();

  EXPECT_EQ(lru_replacer.Size(), 50);
}

这个测试用例时而通过,时而不通过,我就以为是程序的并发出现了问题。
其实是这个测试的写法不对。

#include <iostream>
#include <shared_mutex>
#include <mutex>
#include <thread>
#include <vector>

std::shared_mutex latch_;  // 共享互斥锁
std::vector<int> shared_data(100,0);      // 共享数据

void set_evictable_true (int start,int end) {
    std::unique_lock<std::shared_mutex> lock(latch_);
    for (int i = start; i < end; ++i) {
      shared_data[i]=1;  // 将指定范围内的元素标记为可驱逐
      std::cout << "set_evictable_true: " << i << " = 1\n";  // 调试输出
    }
  };

void set_evictable_false (int start,int end) {
    std::unique_lock<std::shared_mutex> lock(latch_);
    for (int i = start; i < end; ++i) {
      shared_data[i]=0;  // 将指定范围内的元素标记为可驱逐
      std::cout << "set_evictable_false: " << i << " = 0\n";  // 调试输出
    }
  };

int main() {
    

 std::thread t1(set_evictable_true, 0, 50);
  std::thread t2(set_evictable_true, 50, 100);
  std::thread t3(set_evictable_false, 25, 75);

  t1.join();
  t2.join();
  t3.join();
  
  int num=0;
  for(int val:shared_data){
      if(val) num++;
  }
    std::cout<<num<<std::endl;
    return 0;
}

单独编写为上述文件,可以发现程序是先执行的t3线程,在执行的t1和t2线程!!!
GPT给的不一定是对的,一定要记住!!!
主要原因:线程执行顺序
在多线程编程中,线程的执行顺序是非确定性的。
由于线程调度的非确定性,导致最终结果是所有元素都被设置为 1。
正确方案:
为了避免这种竞争条件,你可以调整线程之间的执行顺序,确保线程 t3 完成其任务(即设置索引 25 到 74 为 0)之后,再执行线程 t1 和 t2。你可以通过 std::condition_variable 或其他同步机制来控制线程的执行顺序,确保每个线程的执行顺序是合理的。

  if (static_cast<size_t>(frame_id) > replacer_size_) {
    //   throw std::invalid_argument("Invalid frame_id in SetEvictable");
    return;
  }

4.

在提交到https://www.gradescope.com/网站进行评分时,LRUKReplacerTest.Size.ASAN (0/6)测试用例一直无法通过。size()计算一直有问题。
使用了GPT提供的其他的测试用例,发现在加入的page序号等于replacer的大小时,测试出错。
截取测试用例如下:

TEST(LRUKReplacerTest, EvictAllFrames) {
  LRUKReplacer lru_replacer(3, 2);
  lru_replacer.SetEvictable(1, true);
  lru_replacer.SetEvictable(2, true);
  lru_replacer.SetEvictable(3, true);
  EXPECT_EQ(lru_replacer.Size(), 3);
  }

TEST(LRUKReplacerTest, MixedOperations) {
  LRUKReplacer lru_replacer(5, 2);
  lru_replacer.SetEvictable(5, true);
  EXPECT_EQ(lru_replacer.Size(), 3);

  lru_replacer.SetEvictable(3, false);
  EXPECT_EQ(lru_replacer.Size(), 2);
  }

找到了在SetEvictable()函数中,

线程池实例

函数说明

1. NewPgImp

功能说明
NewPgImp 用于在缓冲池中创建一个新页面。如果缓冲池已满且没有可用的替换帧(即所有帧都被固定),则返回 nullptr。否则,它会:
从自由列表或替换器中选择一个替换帧。
如果选择的帧包含脏页面,则将其写回磁盘。
分配一个新的 page_id 并初始化新的页面。
更新页表和替换器。
将帧的固定计数设为 1(表示被固定)。
实现步骤

  1. 加锁:获取互斥锁以保护共享数据结构。
  2. 选择替换帧:
    优先从 free_list_ 中获取可用帧。
    如果 free_list_ 为空,则从 replacer_ 中选择一个替换帧。
  3. 检查是否有可用帧:
    如果没有可用帧,则返回 nullptr。
  4. 处理被替换的页面:
    如果选择的帧当前包含一个页面,并且该页面是脏的,则将其写回磁盘。
    从页表中移除被替换页面的映射。
  5. 分配新页面:
    调用 AllocatePage() 获取一个新的 page_id。
    读取新页面的内容(初始化为空或从磁盘读取)。
  6. 更新页面元数据:
    设置页面的 page_id。
    重置页面内容。
    设置页面为非脏。
    固定页面(设置 pin_count 为 1)。
  7. 更新页表和替换器:
    将新页面的 page_id 映射到帧。
    将帧从替换器中移除,因为它现在被固定。
    解锁并返回:释放锁并返回新页面的指针。

2. FetchPgImp

功能说明
FetchPgImp 用于从缓冲池中获取指定 page_id 的页面。如果页面不在缓冲池中,则需要:

从自由列表或替换器中选择一个替换帧。
如果替换帧中的页面是脏的,则将其写回磁盘。
从磁盘读取所需页面到替换帧中。
更新页表和替换器。
增加页面的固定计数。
如果所有帧都被固定且没有可用帧,则返回 nullptr。

实现步骤

  1. 加锁:获取互斥锁以保护共享数据结构。
  2. 查找页面:
    在 page_table_ 中查找 page_id 是否存在。
    如果存在,获取对应的帧。
    增加该页面的 pin_count。
    从 replacer_ 中移除该帧(因为它现在被固定)。
    返回页面指针。
  3. 如果页面不在缓冲池中:
    从 free_list_ 或 replacer_ 中选择一个替换帧。
    如果没有可用帧,则返回 nullptr。
  4. 处理被替换的页面:
    如果选择的帧包含脏页面,则将其写回磁盘。
    从页表中移除被替换页面的映射。
  5. 读取新页面:
    从磁盘读取 page_id 对应的页面到选择的帧中。
  6. 更新页面元数据:
    设置页面的 page_id。
    重置页面内容(从磁盘读取的数据)。
    设置页面为非脏。
    固定页面(增加 pin_count)。
  7. 更新页表和替换器:
    将新页面的 page_id 映射到帧。
    从 replacer_ 中移除帧(因为它现在被固定)。
  8. 解锁并返回:释放锁并返回页面指针。

3. UnpinPgImp

功能说明
UnpinPgImp 用于解除对指定 page_id 的页面的固定。如果页面的 pin_count 降至 0,且 is_dirty 为 true,则将页面标记为脏,并将其添加到替换器中,表示该页面可以被替换。

实现步骤

  1. 加锁:获取互斥锁以保护共享数据结构。
    查找页面:
    在 page_table_ 中查找 page_id 是否存在。
    如果不存在,返回 false。
  2. 检查 pin_count:
    如果 pin_count 已经为 0,返回 false。
  3. 更新页面元数据:
    减少页面的 pin_count。
    如果 is_dirty 为 true,将页面标记为脏。
  4. 更新替换器:
    如果 pin_count 降至 0,表示页面现在可以被替换,将其添加到 replacer_。
  5. 解锁并返回:释放锁并返回 true。

4. FlushPgImp

功能说明
FlushPgImp 用于将指定 page_id 的页面刷新到磁盘,不论该页面是否被标记为脏。刷新后,页面的 dirty 标志被取消。

实现步骤

  1. 加锁:获取互斥锁以保护共享数据结构。
  2. 查找页面:
    在 page_table_ 中查找 page_id 是否存在。
    如果不存在,返回 false。
  3. 写回磁盘:
    使用 DiskManager::WritePage 将页面数据写回磁盘。
  4. 更新页面元数据:
    将页面的 dirty 标志设为 false。
  5. 解锁并返回:释放锁并返回 true。

5. FlushAllPgsImp

功能说明
FlushAllPgsImp 用于将缓冲池中的所有页面刷新到磁盘。这包括所有脏页面和非脏页面。

实现步骤

  1. 加锁:获取互斥锁以保护共享数据结构。
  2. 遍历所有页面:
    对于每个页面,如果其 page_id 不为 INVALID_PAGE_ID,则调用 DiskManager::WritePage 将其写回磁盘。
    重置页面的 dirty 标志。
  3. 解锁:释放锁。

6. DeletePgImp

功能说明
DeletePgImp 用于从缓冲池中删除指定 page_id 的页面。如果页面被固定(pin_count > 0),则无法删除,返回 false。否则,删除页面并释放其帧。

实现步骤

  1. 加锁:获取互斥锁以保护共享数据结构。
  2. 查找页面:
    在 page_table_ 中查找 page_id 是否存在。
    如果不存在,调用 DeallocatePage(page_id) 并返回 true。
  3. 检查 pin_count:
    如果页面的 pin_count > 0,则无法删除,返回 false。
  4. 处理页面:
    如果页面是脏的,则将其写回磁盘。
    从页表中移除页面的映射。
    重置页面的元数据。
    将帧添加回 free_list_。
  5. 释放页面:
    调用 DeallocatePage(page_id) 模拟释放磁盘上的页面。
  6. 解锁并返回:释放锁并返回 true。

Debug

1.段错误

问题根源在于如下代码:

frame_id_t *new_frame_id = nullptr;
// 来到替换池中寻找是否有可逐出的帧
replacer_->Evict(new_frame_id);

new_frame_id 初始化为 nullptr: 你定义了一个指针 new_frame_id,但未分配实际内存,只是将其初始化为 nullptr。
传递空指针给 Evict: 如果 Evict 方法在内部试图通过 *new_frame_id 写入值(如设置要逐出的帧 ID),则会尝试解引用 nullptr,从而导致段错误(Segmentation Fault)。
解决方案

需要让 new_frame_id 指向一个有效的内存地址,供 Evict 写入数据。可以通过以下方式修复:
frame_id_t new_frame_id;  // 使用实际变量
if (replacer_->Evict(&new_frame_id)) {
    // 使用 new_frame_id
}

2.逻辑问题

在 NewPgImp 和 FetchPgImp 中,如果调用 Evict 成功后,代码没有正确处理 page_table_ 中旧页的移除。旧页的 (page_id, frame_id) 键值对需要在 page_table_ 中移除,否则可能会导致哈希表的键值对残留,进而引发错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值