cmu15445_lab1_BUFFER POOL
Buffer Pool Manager
整理一下cmu15445
的实验的实现内容,具体实验的代码写的太丑就不公开了。
Page
page
:page
是一个数据库中存储的最小单位,也是磁盘和内存交换的最小单位,每一个page
都有一个page_id
, 在storage/page/page.h
中有该文件的实现,Page
类中主要维护了以下几个信息:
inline void ResetMemory() { memset(data_, OFFSET_PAGE_START, PAGE_SIZE); }
char data_[PAGE_SIZE]{};
page_id_t page_id_ = INVALID_PAGE_ID;
int pin_count_ = 0;
bool is_dirty_ = false;
ReaderWriterLatch rwlatch_;
data
用来表示一个Page
存储的基本信息page_id
是为每一个page
分配的唯一表示pin_count
用来表示当前有多少个线程在占用该类, 当有线程在占用该page
的时候就不能将其放在LRU
中,只有当pint_count == 0
是才能放进LRU
is_dirty
用来表示当前page
是否被修改过,内存中的page
和磁盘中的page
的内容是否一致,如果不一致要刷盘。
有一个地方要注意区分的是page_id
是一个page
在磁盘中的唯一表示,frame_id
是一个page
对应内存中的编号,frame
(内存帧),是一个page
在内存中的表示。
LRU REPLACEMENT POLICY
内存的大小比磁盘要小很多,我们需要逐步替换掉内存中的frame
来换进磁盘中的page
,采用LRU
最近最就未使用算法来实现。
std::unordered_map<frame_id_t, std::list<frame_id_t>::iterator> to_pos_;
std::list<frame_id_t> lru_cache_;
采用双向链表和hash_table
来实现,我们将刚刚使用过的frame
都放在链表的头部,很久没使用的放在链表的尾部。
Victim(frame_id_t*)
, 淘汰掉一个最久未使用的frame
,直接移除链表的最后一个节点即可。Pin(frame_id_t)
, 表示有一个进程需要使用BufferPoolManager
中的一个frame
,就Pin
一下将这个frame
移除LRU
.void Unpin(frame_id_t)
, 如果一个帧没有线程使用就存入LRU
等待被淘汰。Size()
返回一个LRU
的大小。
BUFFER POOL MANAGER INSTANCE
DISK MANAGER
DiskManager
用来管理一个数据库中pages
的分配和释放,以及用来读和写一个page
的内容到磁盘中,同时维护了一个日志文件的读写和分配。
应为每一个page
的大小是固定的我们通过page_id * page_size
来找到该page
在内存中的位置,同时读取和写入,我们通过ifstream
和ostream
的方式向磁盘中读取和写入数据。
void DiskManager::WritePage(page_id_t page_id, const char *page_data) {
std::scoped_lock scoped_db_io_latch(db_io_latch_);
size_t offset = static_cast<size_t>(page_id) * PAGE_SIZE;
// set write cursor to offset
num_writes_ += 1;
db_io_.seekp(offset);
db_io_.write(page_data, PAGE_SIZE);
// check for I/O error
if (db_io_.bad()) {
LOG_DEBUG("I/O error while writing");
return;
}
// needs to flush to keep disk file in sync
db_io_.flush();
}
/**
* Read the contents of the specified page into the given memory area
*/
void DiskManager::ReadPage(page_id_t page_id, char *page_data) {
std::scoped_lock scoped_db_io_latch(db_io_latch_);
int offset = page_id * PAGE_SIZE;
// check if read beyond file length
if (offset > GetFileSize(file_name_)) {
LOG_DEBUG("I/O error reading past end of file");
// std::cerr << "I/O error while reading" << std::endl;
} else {
// set read cursor to offset
db_io_.seekp(offset);
db_io_.read(page_data, PAGE_SIZE);
if (db_io_.bad()) {
LOG_DEBUG("I/O error while reading");
return;
}
// if file ends before reading PAGE_SIZE
int read_count = db_io_.gcount();
if (read_count < PAGE_SIZE) {
LOG_DEBUG("Read less than a page");
db_io_.clear();
// std::cerr << "Read less than a page" << std::endl;
memset(page_data + read_count, 0, PAGE_SIZE - read_count);
}
}
}
BUFFER POOL MANAGER INSTANCE
BufferPoolManagerInstance
实在LRU
的基础上来管理我们的内存中的page
, 主要维护了一下几个信息:
const size_t pool_size_; //BuufferPool 的大小
const uint32_t num_instances_ = 1; //用来维护并行bufferpool的数量
const uint32_t instance_index_ = 0; //在bufferpool池中的编号
std::atomic<page_id_t> next_page_id_{instance_index_}; //每一个bufferpool都用来维护一个bufferpool的pageid
Page *pages_; //一个bufferpool中的pages数组,每一个帧对应的Pages
/** Pointer to the disk manager. */
DiskManager *disk_manager_ __attribute__((__unused__));
/** Pointer to the log manager. */
LogManager *log_manager_ __attribute__((__unused__));
std::unordered_map<page_id_t, frame_id_t> page_table_; //page_table用来维护page_id和frame_id之间的映射关系
Replacer *replacer_; //替换规则
std::list<frame_id_t> free_list_; //空闲的帧
std::mutex latch_; //latch锁来保护bufferpool内部的数据结构
FetchPgImp(page_id)
用来获取一个page
, 首先我们先判断page_table
中是否以及有对应的page_id
如果有的话直接从中提取,同时要记得Pin
一下,同时增加其引用计数。如果page_table
没有对应的page_id
就去空闲链表中查看,如果空闲链表中有剩余的frame
,就提取出来如果没有就从LRU
中采用淘汰算法淘汰一个,然后找到对应的pages
, 注意这个时候我们需要判断淘汰的frame
是否是脏页,如果是脏页就需要刷盘,同时移除page_table
旧的frame
对应的page_id
更新为新的page_id
, 并将其pin_count
设置为1并Pin
一下。UnpinPgImp(page_id, is_dirty)
,Unpin
BufferPool
中的一个Page
,减少其pin_count
,如果pin_count == 0
就将其从LRU
中移除,如果is_dirty
为true
,就要修改对应的page
的is_dirty
。FlushPgImp(page_id)
刷盘,更新对应page
的磁盘内容, 刷盘之后注意要将is_dirty
设置为false
, 因为此时磁盘中和内存中的内容相同了。NewPgImp(page_id)
在buffer_pool
中新分配一个page
, 和FetchPgImp
相似,我们现在空闲链表中查找空闲的frame
,如果空闲链表为空就执行LRU
淘汰一个没有进程使用的frame
,如果淘汰的frame
为脏页注意要刷盘,然后调用AllocatePage
分配一个页,更新Page_tabl
的映射关系。注意这个时候需要刷盘,因为这里只是新分配一个页,磁盘里面是没有这个page
的,需要刷盘将其写入磁盘,也要Pin
一下。DeletePgImp(page_id)
从BufferPool
中删除该页,删除完之后注意将对应的frame
增加到空闲链表当中,同时设置is_dirty
为false
.FlushAllPagesImpl()
刷新所有的page
。
PARALLEL BUFFER POOL MANAGER
一个并发的缓冲池,通过简单的取模运算来将对应的page_id
映射到对应的缓冲池中,直接调用之前实现好的缓冲池的接口即可。