CMU 15-445 Lab1 - Buffer Pool Manager

本文档详细介绍了数据库管理系统中缓冲池管理器的实现,包括LRU和Clock页面替换策略。LRU策略使用双向链表和哈希表来跟踪最近最少使用的页面,而Clock策略则通过一个带有访问位和修改位的环形数组来选择可替换的页面。BufferPoolManager负责读取和写入磁盘页面,同时使用LRU或Clock策略进行页面置换。测试用例包括LRUReplacer和BufferPoolManager的单元测试,确保了线程安全和正确性。

Lab1 - Buffer Pool Manager

个人笔记

实验指导书

  1. 构建一个新的面向磁盘的存储管理器,这样的存储管理器假定数据库的主要存储位置在磁盘上。

  2. 在存储管理器中实现缓冲池。缓冲池负责将 pag主存磁盘来回移动。允许 DBMS 支持大于系统可用内存量的数据库。缓冲池的操作对系统中的其他部分是透明的。例如,系统使用其唯一标识符 ( page_id_t)向缓冲池请求页面,但它不知道该页面是否已经在内存中,或者系统是否必须从磁盘中检索它。

  3. 实现需要是线程安全的。多个线程将同时访问内部数据结构,因此需要确保临界区受到 latches的保护。

  4. 需要在存储管理器中实现以下两个任务:

    (1)LRU 替换原则

    (2)缓冲池管理器

  5. 任务 1 - LRU 替换策略

    (1)该组件负责跟踪缓冲池中的页面使用情况。将在 src/include/buffer/lru_replacer.h 中实现一个新的子类 LRUReplacer ,它相应的实现文件在 src/buffer/lru_replacer.cpp 。LRUReplacer 继承了抽象类 Replacer (src/include/buffer/replacer.h)。

    (2) LRUReplacer 的大小与 BufferPoolManager 相同,因为在 BufferPoolManager 中包含了所有 framesplaceholders 。 但是,并非所有 frames 都被视为 LRUReplacer. 将 LRUReplacer 被初始化为有没有 frames。然后,LRUReplacer 将只考虑新的没有划分的那些。

    (3)需要实现课程中讨论的 LRU 策略。您将需要实现以下方法:

    • Victim(T*) : Replacer 跟踪与所有元素相比最近访问次数最少的对象并删除,将其删除页号存储在输出参数中,并返回 True。如果 Replacer 为空则返回 False
    • Pin(T) :在将 page 固定到 BufferPoolManager 中的 frame 之后,应该调用此方法。它应该从 LRUReplacer 中删除固定包含固定 pageframe
    • Unpin(T):当页面的引用计数变为 0 时,应该调用此方法。这个方法应该将未包含固定 pageframe 添加到 LRUReplacer。(注意,需要判断是否超出了内存大小,如果超过了,则删除较新的页面,然后再添加。)
    • Size():这个方法返回当前在 LRUReplacer 中的页面数。
  6. 任务 2 - 缓冲池管理器

  • 接下来,需要在系统中实现缓冲池管理器 ( BufferPoolManager)。该 BufferPoolManager 负责从 DiskManager 中读取数据库页并将它们存储在内存中。BufferPoolManager 还可以在明确指示或需要为新页腾出空间时,将脏页写入磁盘。
  • 为了确保实现与系统的其余部分一起正常工作,提供了一些已经填写好的功能。也不需要实现实际读写数据到磁盘的代码(在给出的实现中称为 DiskManager)。
  • 系统中所有的内存页面都由 Page 对象表示。BufferPoolManager 并不需要了解这些页的内容。但是,作为系统开发人员,重要的是要理解 Page 对象只是缓冲池中用于存储内存的容器,因此不是特定于唯一的页面。也就是说,每个Page对象都包含一个内存块,DiskManager将它用作复制从磁盘读取的 page 内容的位置。当它来回移动到磁盘时,BufferPoolManager 将重用相同的 Page 对象来存储数据。这意味着相同的 Page 在系统的整个生命周期中可能包含不同的物理页面。Page 对象的标识符 (page_id) 跟踪它所包含的物理页面,如果 Page 对象不包含物理页,则必须将其 page_id 设置为INVALID_Page_id
  • 每个 Page 对象还维护了一个计数器,用于表示 “固定” 该页面的线程数。BufferPoolManager 不允许释放被 ”固定“ 的页面。每个 Page 对象还跟踪它标记的脏页。我们需要判断页面在解除绑定之前是否被修改过。BufferPoolManager 必须将 dirty Page 的内容写回磁盘,然后才能重用该对象。
  • BufferPoolManager 的实现将使用上述步骤中创建的 LRUReplacer 类。它将使用LRUReplacer 来跟踪 Page 对象被访问的时间,以便在必须释放 frame 来腾出空间给从磁盘复制的新物理页时决定删除哪个对象。
  • 需要实现头文件(src / compress / buffer_pool_pool_pool.h)中定义的在源文件 (src / buffer / buffer_pool_manager.cpp) 中的下列函数:
    • FetchPageImpl(page_id)
    • NewPageImpl(page_id)
    • UnpinPageImpl(page_id, is_dirty)
    • FlushPageImpl(page_id)
    • DeletePageImpl(page_id)
    • FlushAllPagesImpl()
  • 对于 FetchPageImpl,如果空闲列表中没有页面可用,并且所有其他页面当前都被固定,则应该返回 NULL。不管 FlushPageImpl 的引用状态如何,都应该刷新页面。
  1. 测试

    • LRUReplacer:

      test/buffer/lru_replacer_test.cpp

    • BufferPoolManager:

      test/buffer/buffer_pool_manager_test.cpp

    • 本地测试:

    // 别忘了删除测试中的 disabled
    
    $ mkdir build
    $ cd build
    $ make lru_replacer_test
    $ ./test/lru_replacer_test
    
    $ mkdir build
    $ cd build
    $ make buffer_pool_manager_test
    $ ./test/buffer_pool_manager_test
    
    // 检查
    $ cd build
    $ make format
    $ make check-lint
    $ make check-clang-tidy
    
  2. 提交:

    • src/include/buffer/lru_replacer.h

    • src/buffer/lru_replacer.cpp

    • src/include/buffer/buffer_pool_manager.h

    • src/buffer/buffer_pool_manager.cpp

    • 打包

      // 下面的写在一行,空格分隔。最好是写个bash脚本,比较方便。
      zip project1.zip 
      src/include/buffer/lru_replacer.h 
      src/buffer/lru_replacer.cpp 
      src/include/buffer/buffer_pool_manager.h 
      src/buffer/buffer_pool_manager.cpp
      

设计思路

  1. LRU(Least-Recently Used)的策略:
    • 数据一般是存储在磁盘上的,当我们需要读取一个数据的时候,会首先把它加载到内存中,然后返回给客户端。
    • 因为一般内存比磁盘小,容量有限,不可能同时存储那么多的数据。因此,内存会时常把暂时不需要的页面换回磁盘中,等到调用的时候再次置换回来。
    • LRU 正是页面置换算法的一种,最近最少使用的策略,它有一个潜在的假设,如果某个页的数据被访问过一次,那么下次再被访问的机率也就更高。
  2. LRU 的思路:
    • 首先,页面需要使用一个数据结构来存储,考虑到它需要不断的插入和删除,特别是需要头插和尾删,因此使用双向链表是比较方便的。另外,可以增加一个哈希表来加快查找和定位的速度(O(1))。综合考虑,使用双向链表 + Hash Table 来完成比较合适。
    • 双向链表负责维护一个页面的集合,数据按照从最近使用过的最近没有使用的来排序。因此,每当更新某个页面的时候,都应该把它放在双向链表的头部,即使是某个页面已经出现在链表中,也要拿出来,重新放置。每当置换的时候,都应该从双向链表的尾部置换页面,取出最近没有使用的页面。
    • Hash Table 负责维护一个从页面到链表节点的映射。它的作用有两个,一个是方便查找某个页面当前是否在这个链表当中。另一个是能够快速定位到当前页面在链表中的位置,方便删除操作。
  3. Clock 策略:
    • Clock 也是页面置换算法的一种。Clock 置换算法分为两种,一种是简单的置换算法,与 LRU 算法类似。另一种是改进型的,相比于前一种,减少了磁盘IO,性能更加高效。
    • 简单 Clock 的思想是先将内存中的所有页面想象成一个环形队列,通过维护一个访问位,每次更新的时候,如果访问位为 0,表示最近没有被访问,则可以置换;否则,将访问位置 0,继续寻找。
    • 改进版的思想是,需要添加一个修改位,修改了的置 1 ,没有则置 0 。那么当要置换时,(访问位,修改位)可能的组合按优先级分为以下四种:(0,0)、(1,0)、(0,1)、(1,1)。因此,它的执行过程是:(1)循环扫描查找(0,0)。有则退出。如果访问位为1则置 0 。(2)循环扫描查找(0,1)。有则退出。如果访问位为1则置 0 。(3)重复第一步。
  4. Clock 设计思路:
    • 这里只考虑简单的 Clock 设计思路。维护一个访问位数组和一个指针。每次当成环形数组循环查找当前需要被置换的页面。

Coding

C++ :std::scoped_lock 能够避免死锁的发生。它的构造函数能够自动进行上锁操作,析构函数会对互斥量进行解锁操作。能够保证线程安全。

  1. 根据上面 LRU 的设计思路,我们可以定义出 LRUReplacer 的数据结构,如下。

    class LRUReplacer : public Replacer {
         
         
     private:
      // TODO(student): implement me!
    	// 为了线程安全需要加的锁
      std::mutex latch;
    	// 这个表示 LRUReplacer 的大小,Buffer Pool大小相同。
      size_t capacity;
    	// 我们使用 双向链表 + 哈希表 的数据结构。
      std::list<frame_id_t> LRUList;
      // 使用 unordered_map(注意加头文件),从 frame_id 映射到 Node。 
      std::unordered_map<frame_id_t, std::list<frame_id_t>::iterator> LRUHash;
    };
    
  2. 根据上面 Clock 的设计思路,我们可以定义出 ClockReplacer 的数据结构,如下。

    
    class ClockReplacer : public Replacer {
         
         
     private:
      // TODO(student): implement me!
      // 对于每个 frame_id,都需要标记两个元素。isPin 表示当前 frame 是正在被引用。
      // ref 表示当前的 frame 最近是否被使用过。
      struct clockItem {
         
         
        bool isPin;
        bool ref;
      };
      // 为了线程安全需要加的锁
      std::mutex latch;
      // 这个表示 ClockReplacer 的大小,Buffer Pool大小相同。
      size_t victim_number;
      // clock 指针当前所指位置。
      size_t clockHand;
      // clockItem 数组,数组下表表示 frame_id。
      std::vector<clockItem> victimArray;
    };
    
  3. Replacer :追踪 page 使用情况的抽象类。类中不包含 page 的具体信息,只是为 Buffer Pool Manager 服务,提供了一个可以替换的 frame_id 。这个类包含了四个主要的函数。这里为了方便,将两个实现 LRUReplacer ClockReplacer 根据功能写在了一起,看的时候如果不舒服可以一次看一种实现方法。

    class Replacer {
         
         
     public:
      Replacer() = default;
      virtual ~Replacer() = default;
    
      /**
       * Remove the victim frame as defined by the replacement policy.
       * @param[out] frame_id id of frame that was removed, nullptr if no victim was found
       * @return true if a victim frame was found, false otherwise
       */
      virtual bool Victim(frame_id_t *frame_id) = 0;
    
      /**
       * Pins a frame, indicating that it should not be victimized until it is unpinned.
       * @param frame_id the id of the frame to pin
       */
      virtual void Pin(frame_id_t frame_id) = 0;
    
      /**
       * Unpins a frame, indicating that it can now be victimized.
       * @param frame_id the i
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值