记录一下 CMU 15445 项目

Write In Front

或许是因为我几乎没写过比较大且完整的项目的原因,从编程环境配置到C++语法掌握,再到整个项目组织设计模式,我已经记不得多少次拍手称赞。
期间遇到各种问题,很多时候想过要不要放弃了,但是经过一次次内心的挣扎之后,耗时一个月满分完成了这个项目。

收获很多,感谢Bustub教学组!!!

PROJECT #1 - BUFFER POOL

TASK #1 - LRU REPLACEMENT POLICY

核心概念
  • page_id

    磁盘被划分为若干块(页),每个块都用唯一的 page_id 来标识,disk_manager 就是根据 page_id 来读取和写入磁盘页

  • frame_id

    磁盘页加载到内存,内存中也需要有相应的内存页(人为划分的内存页),BufferPoolManager(BMP)管理的正是这些内存块,和磁盘类似,内存页也需要有相应的标识来唯一标记这些内存页,frame_id 就是唯一标识内存页的标记

  • lru_hash

    lru_hash 记录可以被置换出内存的那些内存页的 frame_id_t,用于在 O(1)时间查询某个 frame_id 是否可以被置换出内存

  • lru_list

    lru_list 是一个双向链表,记录能够被置换出内存的页面的frame_id, 后加入链表的元素被放到链表头,尾部则是最近最久未使用的页面对应的 frame_id。

  • max_size

    max_size 是 lru_replacer 可以管理的最大的置换页面数量,本文和 BPM 大小一致

  • Pin

    把 frame_id 对应页面剔除 lru_list, 表示该页面被线程占用,不可以被置换出去

  • Unpin

    把 frame_id 对应的页面加入 lru_list,表示该页面可以被加入lru_replacer

  • Victim

    查看 lru_replacer 是否有可以被置换出的页面,有的话则取出 frame_id

数据结构
  • 主要成员
     std::mutex mtx; // 互斥锁
     std::list<frame_id_t> lru_List; // 保存能够置换的页面信息,这里用 frame_id 标记相应页面
     std::unordered_map<frame_id_t, std::list<frame_id_t>::iterator> lru_hash; // 便于快速判断某个 frame_id 对应页面是否在 lru中
     size_t max_size; // lru_replacer 最大可以管理的置换页面数量
    
  • 主要函数
    /**
    * @brief 使用 LRU 策略选择出待从内存移除的页面对应的页帧编号(frame_id)
    *
    * @param frame_id 用于记录被替换出的页面对应的 frame_id,如果没有可被替换出的页面,则返回 false, frame_id 指向 nullptr
    * @return true 如果存在待移除的页面,返回 true
    * @return false 当不存在待移除的页面时,返回 false
    */
    bool LRUReplacer::Victim(frame_id_t *frame_id);
    
    /**
    * @brief 固定 frame_id 对应的页面,表示该页面不可以被置换,即从 replacer 中移除该页
    * 
    * @param frame_id 将要从 replacer 中移除的页对应的 frame_id
    */
    void LRUReplacer::Pin(frame_id_t frame_id);
    
    /**
    * @brief 加入一个新的可以置换的页面对应的 frame_id 到 replacer
    * 
    * @param frame_id 待加入的 frame_id
    */
    void LRUReplacer::Unpin(frame_id_t frame_id);
    
Other
  • Victim 函数为什么不把选中的 frame_id 页面移动到 lru_List 头部(最新访问的移动到头部)

    Victim 这里只用于选中 frame_id, Victim 一般搭配 Unpin 操作使用,而 Unpin 操作会把 frame_id 移动到头部,相当于把选择 frame_id 和 更新 frame_id 拆分了,由更上层进行调用

TASK #2 - BUFFER POOL MANAGER INSTANCE

核心概念
  • page_id 和 frame_id 参考 Task-1 LRUReplacer

  • BMP

    BufferPoolManager 的简称,用户管理内存虚拟的 Page 页面,实际上就是管理 Page 类型的数组 pages_。

  • free_list_

    空闲内存页面链表,刚实例化的 BPM 时,所有的页面都应该加入 free_list_, 此时 LRUReplacer 为空,没有可以置换的页面。

  • replacer_

    页面置换策略,本文使用的是 LRUReplacer,应该优先使用 free_list_内页面,如果没有空闲页面,再考虑页面置换策略。

  • Page

    为了方便管理内存页的数据、线程访问数量、数据是否修改等信息,BPM 通过 Page 对象管理内存页,每个 Page 对象对应一个内存页。Page 对象主要由 metadata 和 data[] 两部分构成,metedata 是一些元数据信息,如 page_id, pin_count、dirty等,而 data字符数组则是存储磁盘页的具体数据内容,为了方便处理,本文假设 data 大小和磁盘页大小一致。 Page 对象是可以被重用的,即每个Page 对象都可以装入不同的磁盘页面

  • pages_

    Page 类型对象的数组,是一组 Page 对象的组合

  • pin_count

    Page 的元数据之一,用于记录当前内存页被占用的线程数量。新建的 Page pin_count = 1,经过 FetchPgImp pin_count++, NewPgImp 后 pin_count = 1, UnpinPg 后 pin_count–, pin_count 为 0 后可以加入 LRUReplacer。

  • dirty

    Page 的元数据之一,用于记录当前内存页数据是否被修改(相对于磁盘数据)

  • pool_size_

    BPM 能够管理的最大内存页数量,实际上就是 pages_(Page类型) 数组的大小

  • disk_manager_

    用于执行与磁盘相关的底层操作,常用的有写入数据到磁盘WritePage 和 从磁盘读取数据 ReadPage

主要函数
  • 函数理解

    需要注意的是以下函数都是 BPM 调用的,主要是对 BPM 的理解,本质上 BPM 的以下操作都是管理的内存页面 Page 数组(除了AllocatePage、DeallocatePage、FindVictimPage),以 FetchPgImp 为例,站在系统角度,需要从主存取 page_id 对应的页面,显示查看内存是否已经调入了该页面,如果没有,再考虑从磁盘读入。

  • FetchPgImp VS NewPgImp

    • FetchPgImp是从 buffer pool 取 page_id 对应页面,如果不存在,则从磁盘调入,会把数据一起写入 buffer pool 内存页
    • NewPgImp 是从磁盘创建一个新的物理页,然后在内存 buffer pool 找到一个位置放入,因为新申请的物理页没有有效的 data。因此, NewPgImp 不需要 ReadPage 操作,只需要完成元数据更新,ResetMemory 即可
    • 主要函数
      // Fetch the requested page from the buffer pool.
      Page *FetchPgImp(page_id_t page_id);
      
      // Creates a new page in the buffer pool.
      Page *NewPgImp(page_id_t *page_id);
      
      // Deletes a page from the buffer pool.
      bool DeletePgImp(page_id_t page_id);
      
      // Unpin the target page from the buffer pool.
      bool UnpinPgImp(page_id_t page_id, bool is_dirty);
      
      // Flushes the target page to disk.
      bool FlushPgImp(page_id_t page_id);
      
      // Flushes all the pages in the buffer pool to disk.
      void FlushAllPgsImp();
      
      // Allocate a page on disk.∂
      page_id_t AllocatePage();
      
      // Deallocate a page on disk.
      void DeallocatePage(__attribute__((unused)) page_id_t page_id) 
      
      // 自定义函数,查找空闲的 buffer pool Page,优先从 free_list_查找,其次采用 LRU 策略置换页面
      bool FindVictimPage(frame_id_t *frame_id);
      

PROJECT #2 - EXTENDIBLE HASH INDEX

Task #1 PAGE LAYOUTS

HashTableDirectoryPage
  • 成员变量
    page_id_t page_id_;  // 目录页自身的页编号
    lsn_t lsn_;  // 页面的 lsm 用于 recovery
    uint32_t global_depth_{0};  // 目录全局深度
    uint8_t local_depths_[DIRECTORY_ARRAY_SIZE];  // 每个数据桶(bucket_page) 的局部深度
    page_id_t bucket_page_ids_[DIRECTORY_ARRAY_SIZE];  // 每个数据桶所在的物理页面编号
    
  • 获取镜像桶编号
    uint32_t HashTableDirectoryPage::GetSplitImageIndex(uint32_t bucket_idx) {
      int n = local_depths_[bucket_idx];  // local_depth 记录分裂过后的局部深度
      if (n == 0) {  // 整个 hash 表只剩下最后一个桶了,这个就是最初的那个深度为 0 的桶,该桶不能进行合并了
        return 0;
      }
      return (1 << (n - 1)) ^ bucket_idx;  // 希望获得由分裂前深度 +0 or +1 得到的镜像桶的 index,则需要1 <<  (n - 1)
    }
    
  • 当局部深度和全局深度相同的桶需要分裂时,增加全局深度,降低时不需要这么复杂
      /**
        * 以 global_depth = 1 grow 到 global_depth = 2 为例:
        * 0 -> b1
        * 1 -> b1
        * after grow :
        * 00 -> b1
        * 01 -> b2
        * 10 -> b1
        * 11 -> b2
        * 本质上是在 grow 前的每个 bucket_idx 的二进制表示形式最高位的再高一位加上 1, 如原先的 0 + (1 << gd) -> 10 , 1 + (1 <<
        * gd) -> 11 这样就可以保证所有的数在加上一个数后末尾的二进制表示形式总是不变的,也就使得 i + orig_max_bucket 与 i
        * 的二进制表示形式具有相同的低位二进制表示 本质上 i + orig_max_bucket  就是该桶分裂后的新桶的 bucket_idx
        */
    void HashTableDirectoryPage::IncrGlobalDepth() {
        int orig_max_bucket = 1 << global_depth_;    // 原来总的桶的数量
        for (int i = 0; i < orig_max_bucket; ++i) {  // 遍历原来的每个桶索引 bucket_idx
          bucket_page_ids_[i + orig_max_bucket] =
              bucket_page_ids_[i];  // 编号为 i 和编号为 i + orig_max_bucket 的索引指向同一个桶
          local_depths_[i + orig_max_bucket] =
              local_depths_[i];  // 编号为 i 和编号为 i + orig_max_bucket 的桶的初始时深度保持一致
        }
        global_depth_++;
      }
    
Hash Table Bucket Page
  • 成员变量

    /*
    * Bucket page format (keys are stored in order):
    *  ----------------------------------------------------------------
    * | KEY(1) + VALUE(1) | KEY(2) + VALUE(2) | ... | KEY(n) + VALUE(n)
    *  ----------------------------------------------------------------
    */
    
    // 用比特位标记某个<key, value>位置是否被占用(这主要用于linear probe hash),一旦插入过数据,即便删除也是1,Extendiable Hash 没用到
    // 0 if tombstone/brand new (never occupied), 1 otherwise.
    char occupied_[(BUCKET_ARRAY_SIZE - 1) / 8 + 1];
    // 标记某个位置是否已经插入数据
    char readable_[(BUCKET_ARRAY_SIZE - 1) / 8 + 1];
    MappingType array_[0];  // 零长度数组 :occupied_ 和 readable_ 剩下的空间都用于数组
    
  • 实现细节

    • readable_ 为 1 表示该位置为有效的键值对,0表示该位置为 0

    • occupied_ 为 0 表示该位置从未被使用过,1 表示该位置被占据过,当键值对删除时,occupied_值仍然为 1 ,该位不表示空与否,Extensible hash 其实可以不需要,这是linear probe hash 中锁使用的,tombstone的设计主要是为了在开放寻址法中防止探测中断

    • readable_ 设置位,采用 BitMap 思想实现

      readable 为 char 类型,每个单位占 8bit ,我们可以把 readable—— 数组看成 r 行 8 列的数组,每个 bit,当数字 n 出现时,对应 n / 8 行 和 n % 8 列的数应该置为 1。当数组变成 int(32-bit) 类型时候,列数则为 32

      // set bucket_idx
      int r = bucket_idx / 8;
      int c = bucket_idx % 8;
      readable_[r] |= (1 << c);  // 其他位置不变,相应位置设置为 1
      
      // clear bucket_idx
      int r = bucket_idx / 8;
      int c = bucket_idx % 8;
      readable_[r] &= (~(1 << c)); // 其他位置不变,相应位置设置为 0
      
    • MappingType array_[0] 采用零长度数组实现,整个 Bucket 类除去 occupied_ & readable_ 数组,其余空间全部用于这个数组

    • Bitmap 简介

  • 获取桶内某个键对应的值

    template <typename KeyType, typename ValueType, typename KeyComparator>
    bool HASH_TABLE_BUCKET_TYPE::GetValue(KeyType key, KeyComparator cmp, std::vector<ValueType> *result) {
      for (size_t i = 0; i < BUCKET_ARRAY_SIZE; ++i) {  // 遍历当前桶(页)所有 <key, value>
        if (IsReadable(i) &&
            !cmp(KeyAt(i), key)) {  // 如果该位置为有效键值对且该位置的键和传入的 key 相当 ,那么加入该位置的值到 result
          result->emplace_back(ValueAt(i));
        }
      }
      return !result->empty();  // 如果存在至少一个 key 对应的 value,则返回 true, 否则返回 false
    }
    
  • 插入键值对到桶中,可以重复键,但是不可以重复键值对

    template <typename KeyType, typename ValueType, typename KeyComparator>
    bool HASH_TABLE_BUCKET_TYPE::Insert(KeyType key, ValueType value, KeyComparator cmp) {
      // 1. 检查待插入的键值对是否已经存在
      for (size_t i = 0; i < BUCKET_ARRAY_SIZE; ++i) {
        // cmp 相等时返回 0, !cmp(x,x) 表示相等为真
        // IsReadable(i) 为真表示 Bucket 第 i 个位置为有效的键值对
        if (IsReadable(i) && !cmp(KeyAt(i), key) && ValueAt(i) == value) {
          return false;  // <key, value> pair 重复
        }
      }
    
      for (size_t i = 0; i < BUCKET_ARRAY_SIZE; ++i) {
        if (!IsReadable(i)) {  // 该位置没有有效的键值对,即该位置可以插入(无论Occupied 与否),即存在空位置
          SetOccupied(i);                       // 表示该位置已经被占用,删除时,Ocuupied 值不变
          SetReadable(i);                       // 该位置已经插入有效键值对
          array_[i] = MappingType(key, value);  // 插入新的键值对
          return true;
        }
      }
      return false;  // 没有空的位置,插入失败
    }
    
  • 删除某个键值对只需要更改 readable_ 即可

    template <typename KeyType, typename ValueType, typename KeyComparator>
    void HASH_TABLE_BUCKET_TYPE::RemoveAt(uint32_t bucket_idx) {
      // char 类型数组,每个单位 8 bit,能标记8个位置是否有键值对存在, bucket_idx / 8 找到应该修改的字节位置
      // bucket_idx % 8 找出在该字节的对应 bit 位 pos_
      // 构建一个 8 位长度的,除该 pos_ 位为 0 外,其他全为 1 的Byte ,例如: 11110111
      // 然后与相应字节取按位与操作,则实现清除该位置的 1 的操作,而其他位置保持不变
      readable_[bucket_idx / 8] &= (~(1 << (bucket_idx % 8)));
    }
    
  • 判断某个位置是否存在键值对

    template <typename KeyType, typename ValueType, typename KeyComparator>
    bool HASH_TABLE_BUCKET_TYPE::IsReadable(uint32_t bucket_idx) const {
      return readable_[bucket_idx / 8] & (1 << (bucket_idx % 8));  // 只要存在,那么相应位置为 000001000000,肯定不为 0
    }
    

Task #2 HASH TABLE IMPLEMENTATION

这个ExtendiableHash很繁琐,实现细节非常多,需要非常了解其原理才可以正确实现,建议先看看Extendible Hashing (Dynamic approach to DBMS),有个粗略的了解。然后理解其全局深度和局部深度之间的关系,搞明白分裂桶和镜像桶之间的区别与联系很重要。

实现细节
  • 分裂桶编号的扩展方向
    • 本文采用的是低位向高位扩展的方式,最开始全局深度为0,指向第一个桶。
    • 低位向高位扩展举例:
      假设局部深度为2,原先桶编号为 10, 那么其分裂的两个桶编号分别为 110 和 010
  • 如何根据 key 获取其应该存在的桶的编号?
    用 key 值和全局深度对应的全1的掩码按位与操作获得桶编号
    template <typename KeyType, typename ValueType, typename KeyComparator>
    inline uint32_t HASH_TABLE_TYPE::KeyToDirectoryIndex(KeyType key, HashTableDirectoryPage *dir_page) {
      return Hash(key) & dir_page->GetGlobalDepthMask();
    }
    
  • 根据 key 获取其在桶内的值,这就是个取哈希值的过程
    template <typename KeyType, typename ValueType, typename KeyComparator>
    bool HASH_TABLE_TYPE::GetValue(Transaction *transaction, const KeyType &key, std::vector<ValueType> *result) {
      // 添加读锁
      table_latch_.RLock();
    
      // 取目录页和 key 所在的桶页
      auto dpg = FetchDirectoryPage();
      auto bpg = FetchBucketPageByKey(key);
      auto flag = bpg->GetValue(key, comparator_, result);
    
      // !!! 一定不要忘了 UnPin 页面,否则 BufferPool 会因为一直加入页面而无法替换出页面,导致BufferPool 溢出
      buffer_pool_manager_->UnpinPage(directory_page_id_, false, nullptr);
      buffer_pool_manager_->UnpinPage(KeyToPageId(key, dpg), false, nullptr);
    
      // 解锁
      table_latch_.RUnlock();
      return flag;
    }
    
  • 关键的插入操作
    template <typename KeyType, typename ValueType, typename KeyComparator>
    bool HASH_TABLE_TYPE::Insert(Transaction *transaction, const KeyType &key, const ValueType &value) {
      table_latch_.WLock();
    
      page_id_t bucket_page_id = KeyToPageId(key, FetchDirectoryPage());  // 必须提前获取页面,不能等到分裂再获取
      auto bpg = FetchBucketPageByKey(key);                               // 取key 所在的桶页面
      bool flag = bpg->Insert(key, value, comparator_);                   // 记录是否插入成功
      buffer_pool_manager_->UnpinPage(directory_page_id_, false, nullptr);
      // 释放写锁
      table_latch_.WUnlock();
    
      // 如果插入失败,需要判断是因为重复 <key, value> 键值对导致的失败还是因为桶满了导致的失败
      // TODO(zhangw) 这里是否需要对桶进行枷锁操作?
      if (!flag) {
        std::vector<ValueType> result;
        bpg->GetValue(key, comparator_, &result);
        auto iter = std::find(result.begin(), result.end(), value);
        if (iter == result.end()) {  // 如果是桶满导致的失败
          flag = SplitInsert(transaction, key, value);
        }
      }
      // page_id_t bucket_page_id = KeyToPageId(key, dpg);  // 这里 key 获取的 page_id
      // 可能会因为分裂而改变,因此这里是错误的
      buffer_pool_manager_->UnpinPage(bucket_page_id, true, nullptr);
      return flag;
    }
    
  • 什么时候需要分裂,什么时候需要增加全局深度?
    • 当插入的桶满了就需要分裂
    • 当插入的桶需要分裂且该桶的局部深度等于全局深度时则增加全局深度
    • 注意全局深度增加会导致 key 到桶编号映射的改变,因此需要更新此时因为全局深度改变增加出来的那些桶的指向,保证其指向正确非常重要。
  • 增加全局深度后,需要及时更新分裂桶和镜像桶的指向
    • 这里比较难理解,直白点就是把由分裂前global_depth 长度的后缀分流为由 global_depth + 1 长度后缀的两部分(这两部分只有最高位不同0 or 1),然后把数据根据 global_depth + 1 位置的不同,设置其新的页面号。
      // 把指向溢出桶的目录项的相应部分映射到新桶的 page_id 上去
      size_t ld = dpg->GetLocalDepth(overflow_bucket_dir_idx);
      size_t local_depth_bits =
          overflow_bucket_dir_idx &
          ((1 << ld) - 1);  // 获取overflow_bucket_idx 的最低的 local_depth 个 bit ,等价于 % (1 << ld)
      for (size_t i = local_depth_bits; i < dpg->Size(); i += (1 << ld)) {
        if (((i >> ld) & 1) != ((overflow_bucket_dir_idx >> ld) & 1)) {
          dpg->SetBucketPageId(i, img_page_id);
        }
        dpg->IncrLocalDepth(i);
      }
    
  • Merge 操作,当删除元素后桶为空时,则指向 Merge 操作
    • Merge 会把空桶和其镜像桶进行合并操作
  • 22
    点赞
  • 83
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
cmu15445vscode 是指在 CMU 15-445/645 数据库课程中使用的 Visual Studio Code (VS Code) 编辑器的配置。在这个配置中,课程要求在 Linux 环境下运行,但是 Windows 10 可以安装 Linux 子系统并在 VS Code 上进行代码编写和调试。通过使用 VS Code 内置的 Linux 终端或者其他终端,使用 cmake 进行代码的编译和运行。 要配置 VS Code 的 C/C++ 开发环境,可以使用快捷键 "Ctrl + Shift + P" 打开命令面板,然后输入 "C/C: Edit Configurations(JSON)" 来打开 c_cpp_properties.json 文件。在该文件中,将 cStandard 的值修改为 "c17",将 cppStandard 的值修改为 "c17",以指定使用 C17 和 C++17 的标准编译代码。 如果在使用自动评分程序时出现错误信息 "The autograder failed to execute correctly",可能是由于提交的代码中使用了评测机不支持的自定义变量或函数,导致编译错误。在这种情况下,建议联系课程的教师或助教寻求帮助,并提供问题页面的链接,以便他们能够更有效地帮助你解决问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [在vscode(win10)配置 CMU-15445-lab(linux子系统)](https://blog.csdn.net/Kprogram/article/details/124375883)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [记录一下 CMU 15445 项目](https://blog.csdn.net/Tianweidadada/article/details/125340858)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值