LRU策略可以由哈希表加双向链表的方式实现,其中链表充当队列的功能以记录页面被访问的先后顺序,哈希表则记录<页面ID - 链表节点>键值对,以在O(1)复杂度下删除链表元素。实际实现中使用STL中的哈希表unordered_map
和双向链表list
,并在unordered_map
中存储指向链表节点的list::iterator
。
page_table_
用于保存磁盘页面IDpage_id
和槽位IDframe_id_t
的映射。
Buffer Pool Manager
则是向系统提供了获取 page 的接口。系统拿着一个 page_id
就可以向 Buffer Pool Manager
索要对应的 page,而不关心这个 page 具体存放在哪。系统并不关心(也不可见)获取这个 page 的过程,无论是从 disk 还是 memory 上读取,还是 page 可能发生的在 disk 和 memory 间的移动。这些内部的操作交由 Buffer Pool Manager
完成。
Buffer Pool Manager 里有几个重要的成员:
- pages:buffer pool 中缓存 pages 的指针数组
- disk_manager:可以用来读取 disk 上指定 page id 的 page 数据,或者向 disk 上给定 page id 对应的 page 里写入数据。(DiskManager负责数据库中页面的分配和回收。它执行从磁盘读取和写入页面的操作,在数据库管理系统的上下文中提供一个逻辑文件层。)将日志内容写入磁盘文件,
- page_table: Extendible Hash Table,用来将 page id 映射到 frame id,即 page 在 buffer pool 中的位置
- replacer:刚才实现的 LRU-K Replacer,在需要驱逐 page 腾出空间时,告诉我们应该驱逐哪个 page。
- free_list:空闲的 frame 列表
Buffer Pool Manager 给上层调用者提供的两个最重要的功能是 new page 和 fetch page。
New Page
上层调用者希望新建一个 page,调用 NewPgImp
。
如果当前 buffer pool 已满并且所有 page 都是 unevictable 的,直接返回。否则:
- 如果当前 buffer pool 里还有空闲的 frame,创建一个空的 page 放置在 frame 中。
- 如果当前 buffer pool 里没有空闲的 frame,但有 evitable 的 page,利用 LRU-K Replacer 获取可以驱逐的 frame id,将 frame 中原 page 驱逐,并创建新的 page 放在此 frame 中。驱逐时,
- 如果当前 frame 为 dirty(发生过写操作),将对应的 frame 里的 page 数据写入 disk,并重置 dirty 为 false。清空 frame 数据,并移除 page_table 里的 page id,移除 replacer 里的引用记录。
- 如果当前 frame 不为 dirty,直接清空 frame 数据,并移除 page_table 里的 page id,移除 replacer 里的引用记录。
在 replacer 里记录 frame 的引用记录,并将 frame 的 evictable 设为 false。因为上层调用者拿到 page 后可能需要对其进行读写操作,此时 page 必须驻留在内存中。
使用 AllocatePage
分配一个新的 page id(从0递增)。
auto BufferPoolManagerInstance::AllocatePage() -> page_id_t { return next_page_id_++; }
将此 page id 和存放 page 的 frame id 插入 page_table。
page 的 pin_count 加 1。
76 Page *BufferPoolManagerInstance::NewPgImp(page_id_t *page_id) {
77 // 0. Make sure you call AllocatePage!
78 // 1. If all the pages in the buffer pool are pinned, return nullptr.
79 // 2. Pick a victim page P from either the free list or the replacer. Always pick from the free list first.
80 // 3. Update P's metadata, zero out memory and add P to the page table.
81 // 4. Set the page ID output parameter. Return a pointer to P.
82 frame_id_t new_frame_id;
83 latch_.lock();
84 if (!free_list_.empty()) {
85 new_frame_id = free_list_.front();
86 free_list_.pop_front();
87 } else if (!replacer_->Victim(&new_frame_id)) {
88 latch_.unlock();
89 return nullptr;
90 }
91 *page_id = AllocatePage();
92 if (pages_[new_frame_id].IsDirty()) {
93 page_id_t flush_page_id = pages_[new_frame_id].page_id_;
94 pages_[new_frame_id].is_dirty_ = false;
95 disk_manager_->WritePage(flush_page_id, pages_[new_frame_id].GetData());
96 }
97 page_table_.erase(pages_[new_frame_id].page_id_);
98 page_table_[*page_id] = new_frame_id;
99 pages_[new_frame_id].page_id_ = *page_id;
100 pages_[new_frame_id].ResetMemory();
101 pages_[new_frame_id].pin_count_ = 1;
102 replacer_->Pin(new_frame_id);
103 latch_.unlock();
104 return &pages_[new_frame_id];
105 }
Fetch Page
上层调用者给定一个 page id,Buffer Pool Manager 返回对应的 page 指针。调用 FetchPgImp
。
假如可以在 buffer pool 中找到对应 page,直接返回。
否则需要将磁盘上的 page 载入内存,也就是放进 buffer pool。
107 Page *BufferPoolManagerInstance::FetchPgImp(page_id_t page_id) {
108 // 1. Search the page table for the requested page (P).
109 // 1.1 If P exists, pin it and return it immediately.
110 // 1.2 If P does not exist, find a replacement page (R) from either the free list or the replacer.
111 // Note that pages are always found from the free list first.
112 // 2. If R is dirty, write it back to the disk.
113 // 3. Delete R from the page table and insert P.
114 // 4. Update P's metadata, read in the page content from disk, and then return a pointer to P.
115 frame_id_t frame_id;
116 latch_.lock();
117 if (page_table_.count(page_id) != 0U) {
118 frame_id = page_table_[page_id];
119 pages_[frame_id].pin_count_++;
120 replacer_->Pin(frame_id);
121 latch_.unlock();
122 return &pages_[frame_id];
123 }
124
125 if (!free_list_.empty()) {
126 frame_id = free_list_.front();
127 free_list_.pop_front();
128 page_table_[page_id] = frame_id;
129 disk_manager_->ReadPage(page_id, pages_[frame_id].data_);
130 pages_[frame_id].pin_count_ = 1;
131 pages_[frame_id].page_id_ = page_id;
132 replacer_->Pin(frame_id);
133 latch_.unlock();
134 return &pages_[frame_id];
135 }
136 if (!replacer_->Victim(&frame_id)) {
137 latch_.unlock();
138 return nullptr;
139 }
140 if (pages_[frame_id].IsDirty()) {
141 page_id_t flush_page_id = pages_[frame_id].page_id_;
142 pages_[frame_id].is_dirty_ = false;
143 disk_manager_->WritePage(flush_page_id, pages_[frame_id].GetData());
144 }
145 page_table_.erase(pages_[frame_id].page_id_);
146 page_table_[page_id] = frame_id;
147 pages_[frame_id].page_id_ = page_id;
148 disk_manager_->ReadPage(page_id, pages_[frame_id].data_);
149 pages_[frame_id].pin_count_ = 1;
150 replacer_->Pin(frame_id);
151 latch_.unlock();
152 return &pages_[frame_id];
153 }
flushPgImp
用于显式地将缓冲池页面写回磁盘。首先,应当检查缓冲池中是否存在对应页面ID的页面,如不存在则返回False;如存在对应页面,则将缓冲池内的该页面的is_dirty_
置为false,并使用WritePage
将该页面的实际数据data_
写回磁盘。
51 bool BufferPoolManagerInstance::FlushPgImp(page_id_t page_id) {
52 // Make sure you call DiskManager::WritePage!
53 frame_id_t frame_id;
54 latch_.lock();
55 if (page_table_.count(page_id) == 0U) {
56 latch_.unlock();
57 return false;
58 }
59 frame_id = page_table_[page_id];
60 pages_[frame_id].is_dirty_ = false;
61 disk_manager_->WritePage(page_id, pages_[frame_id].GetData());
62 latch_.unlock();
63 return true;
64 }