数据库任务一 存储管理 任务1.1 缓冲池管理器 任务1.1.1 磁盘存储管理器 任务要求 本任务要求补全DiskManager
类,其负责读写指定页面、分配页面编号,以及对文件进行操作。
DiskManager
类的接口如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class DiskManager { public : explicit DiskManager () ; ~DiskManager () = default ; void write_page (int fd, page_id_t page_no, const char *offset, int num_bytes) ; void read_page (int fd, page_id_t page_no, char *offset, int num_bytes) ; page_id_t AllocatePage (int fd) ; bool is_file (const std::string &path) ; void create_file (const std::string &path) ; int open_file (const std::string &path) ; void close_file (int fd) ; void destroy_file (const std::string &path) ; }
通关截图
代码分析 write_page函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void DiskManager::write_page (int fd, page_id_t page_no, const char *offset, int num_bytes) { lseek (fd, page_no*PAGE_SIZE, SEEK_SET); ssize_t bytes_write = write (fd, offset, num_bytes); if (bytes_write != num_bytes) { throw UnixError (); } }
lseek头文件以及函数声明如下
1 2 3 #include <sys/types.h> #include <unistd.h> off_t lseek (int fd, off_t offset, int whence) ;
代码内的lseek()函数会重新定位被打开文件的位移量,根据参数offset以及whence的组合来决定。
offset为正则向文件末尾移动,为负数则向文件头部。
SEEK_SET:从文件头部开始偏移offset个字节。
SEEK_CUR:从文件当前读写的指针位置开始,增加offset个字节的偏移量。
SEEK_END:文件偏移量设置为文件的大小加上偏移量字节。
write函数头文件以及函数声明如下
1 2 #include <unistd.h> ssize_t write (int fd, const void *buf,size_t nbytes)
fd为文件描述符,buf:指定的缓冲区,即指针,指向一段内存单元,nbytes:要写入文件指定的字节 数。返回值:成功时写入文档的字节数,出错时返回-1。
代码中对写函数的返回值进行判断,如果实际写的字节数不等于要求写的字节数,则报错。
read_page函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 ** * @brief Read the contents of the specified page into the given memory area */ void DiskManager::read_page (int fd, page_id_t page_no, char *offset, int num_bytes) { lseek (fd, page_no*PAGE_SIZE, SEEK_SET); ssize_t bytes_read = read (fd, offset, num_bytes); if (bytes_read != num_bytes) { throw UnixError (); } }
read函数与write函数相像,其头函数和函数声明如下
1 2 #include <unistd.h> ssize_t read (int fd, void *buf, size_t count) ;
返回值:成功返回读取的字节数,出错返回-1并设置errno。如果在调read之前已到达文件末尾,则这次read返回0
AllocatePage函数 1 2 3 4 5 6 7 8 9 10 page_id_t DiskManager::AllocatePage (int fd) { fd2pageno_[fd]++; return fd2pageno_[fd]-1 ; }
根据disk_manager.h中fd2pageno_[fd]代表着文件fd中分配的page no个数,这里采用简单的自增分配策略,指定文件页面编号加一。
is_file函数 1 2 3 4 5 6 7 8 9 10 bool DiskManager::is_file (const std::string &path) { struct stat st ; int rc = stat (path.c_str (), &st); return rc == 0 ? true : false ; }
.c_str()函数将string格式的path转换成char格式
stat函数头文件和定义函数如下所示
1 2 3 #include <sys/stat.h> #include <unistd.h> int stat (const char *file_name, struct stat *buf) ;
通过文件名filename获取文件信息,并保存在buf所指的结构体stat中
返回值:执行成功则返回0,失败返回-1,错误代码存于errno
利用stat函数的返回值判断是否执行成功,成功则说明指定路径文件存在。
create_file函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void DiskManager::create_file (const std::string &path) { if (is_file (path)){ throw FileExistsError (path); } int fd = open (path.c_str (), O_WRONLY | O_CREAT); if (fd < 0 ) throw UnixError (); close (fd); }
首先判断路径文件是否存在,存在则报错
open函数头函数和定义函数
1 2 3 4 5 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open (const char *pathname, int flags) ;int open (const char *pathname, int flags, mode_t mode) ;
返回值:open函数的返回值如果操作成功,它将返回一个文件描述符,如果操作失败,它将返回-1。如果操作失败则抛出异常。并在操作后关闭open。
flags参数表示打开文件所采用的操作,我们需要注意的是:必须指定以下三个常量的一种,且只允许指定一个(ubuntu 20.04不加下面三个也没报错)
O_RDONLY:只读模式
O_WRONLY:只写模式
O_RDWR:可读可写
以下的常量是选用的,这些选项是用来和上面的必选项进行按位或起来作为flags参数。
O_APPEND 表示追加,如果原来文件里面有内容,则这次写入会写在文件的最末尾。
O_CREAT 表示如果指定文件不存在,则创建这个文件
O_EXCL 表示如果要创建的文件已存在,则出错,同时返回 -1,并且修改 errno 的值。
O_TRUNC 表示截断,如果文件存在,并且以只写、读写方式打开,则将其长度截断为0。
O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端。
O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O设置为非阻塞模式(nonblocking mode)
destroy_file函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void DiskManager::destroy_file (const std::string &path) { if (!is_file (path)){ throw FileNotFoundError (path); } if (path2fd_.find (path) != path2fd_.end ()){ throw UnixError (); } int ret = unlink (path.c_str ()); if (ret < 0 ) { throw UnixError (); } }
浏览find函数的文档
可以知道find可以在unordered_map中进行查找,当数据出现时,返回数据所在位置的迭代器,如果map中没有要查找的数据,它返回的迭代器等于end函数返回的迭代器。则可以用path2fd_.find(path) != path2fd_.end()
来判断哈希表数据不存在。
查看disk_manager.h定义,path2fd_
和fd2path_
用于记录文件是否被打开,注意不能重复打开相同文件,如果能够找到记录数据,则抛出异常。
unlink函数头文件和声明函数如下
1 2 #include <unistd.h> int unlink (const char * pathname) ;
unlink从文件系统中中删除一个名字,若这个名字是指向这个文件的最后一个链接,并且没有进程处于打开这个文件的状态,则删除这个文件,释放这个文件占用的空间。如果这个名字是指向这个文件的最后一个链接,但有某个进程处于打开这个文件的状态,则暂时不删除这个文件,要等到打开这个文件的进程关闭这个文件的文件描述符后才删除这个文件。
返回值:调用成功返回0,不成功返回-1。如果不成功则抛出异常。
open_file函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int DiskManager::open_file (const std::string &path) { if (!is_file (path)){ throw FileNotFoundError (path); } if (path2fd_.find (path) != path2fd_.end ()){ throw UnixError (); } int fd = open (path.c_str (), O_RDWR); if (fd < 0 ) { throw UnixError (); } path2fd_.insert (std::make_pair (path, fd)); fd2path_.insert (std::make_pair (fd,path)); return fd; }
同理,文件路径不存在则抛出异常,文件已经被打开则抛出异常,如果打开文件失败则抛出异常。
打开文件后,在path2fd_
和fd2path_
表中更新打开记录。
close_file函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void DiskManager::close_file (int fd) { if (fd2path_.find (fd) == fd2path_.end ()){ throw FileNotOpenError (fd); } path2fd_.erase (fd2path_[fd]); fd2path_.erase (fd); close (fd); }
同理,查找fd2path_
或path2fd_
表判断是否已经打开,如果没有打开记录则抛出异常。
关闭文件,清空表中的记录,关闭open状态。
任务1.1.2 缓冲池替换策略 任务要求 本任务要求补全Replacer
类,其负责跟踪缓冲池中每个页面所在帧的使用情况。当缓冲池没有空闲页面时,需要使用该类提供的替换策略选择一个页面进行淘汰。要求实现的替换策略为最近最少使用(LRU)算法。
Replacer
类的接口如下:
1 2 3 4 5 6 7 8 class Replacer { public : explicit Replacer (size_t num_pages) ; ~Replacer (); bool Victim (frame_id_t *frame_id) ; void Pin (frame_id_t frame_id) ; void Unpin (frame_id_t frame_id) ; };
通关截图
代码分析 Victim函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 bool LRUReplacer::Victim (frame_id_t *frame_id) { std::scoped_lock lock{latch_}; if (LRUlist_.size ()!=0 ){ *frame_id = LRUlist_.back (); LRUlist_.pop_back (); return true ; } else { return false ; } }
std::scoped_lock lock{latch_};
它能够避免死锁发生,其构造函数能够自动进行上锁操作,析构函数会对互斥量进行解锁操作,保证线程安全。
查看lru_replacer.h
中LRUlist_
的定义,发现其按照时间顺序排序,最近被访问过置于首部,最近未被访问的置于尾部。根据先来先淘汰的原则进行Victim操作并返回true,如果LRUlist_
表中没有元素则不进行操作,返回false。
Pin函数 1 2 3 4 5 6 7 8 9 10 11 void LRUReplacer::Pin (frame_id_t frame_id) { std::scoped_lock lock{latch_}; LRUlist_.remove (frame_id); }
根据frame_id从LRUlist_
去掉即可。
Unpin函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void LRUReplacer::Unpin (frame_id_t frame_id) { int flag = 1 ; latch_.lock (); std::list<frame_id_t >::iterator it = LRUlist_.begin (); while (it != LRUlist_.end ()){ if (*it == frame_id){ flag = 0 ; break ; } ++it; } if (flag){ LRUlist_.push_front (frame_id); } latch_.unlock (); }
支持并发锁,函数运行过程中需要使用latch_.lock();
和 latch_.unlock();
上锁
取消固定frame,则需要将其重新添加至LRUlist_
,但是有两种情况,一种是之前不存在,那么我们将frame添加至表头即可,但是如果之前存在,需要恢复至之前的位置。
Size函数 1 2 3 4 5 6 size_t LRUReplacer::Size () { return LRUlist_.size (); }
返回LRUlist_
此时的大小。
任务1.1.3 缓冲池管理器 任务要求 本任务要求补全BufferPoolManager
类,其负责管理缓冲池中的页面与磁盘文件中的页面之间的来回移动。
BufferPoolManager
类的接口如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class BufferPoolManager { public: BufferPoolManager(size_t pool_size, DiskManager *disk_manager); ~BufferPoolManager(); Page *NewPage(PageId *page_id); Page *FetchPage(PageId page_id); bool UnpinPage(PageId page_id, bool is_dirty); bool DeletePage(PageId page_id); bool FlushPage(PageId page_id); void FlushAllPages(int fd); private: // 辅助函数 bool FindVictimPage(frame_id_t *frame_id); void UpdatePage(Page *page, PageId new_page_id, frame_id_t new_frame_id); }
通关截图
代码分析 FindVictimPage函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 bool BufferPoolManager::FindVictimPage (frame_id_t *frame_id) { if (!free_list_.empty ()){ *frame_id = free_list_.front (); free_list_.pop_front (); return true ; } else { return replacer_->Victim (frame_id); } }
从buffer_pool_manager.h
文件可知free_list_是空闲帧id组成的链表,如果这个链表为空,则说明缓冲池已满,使用replacer
中的victim
函数淘汰页面
如果缓冲池未满,则从free_list中获得可淘汰帧页,淘汰链表首部元素。
UpdatePage函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void BufferPoolManager::UpdatePage (Page *page, PageId new_page_id, frame_id_t new_frame_id) { if (page->is_dirty_){ page->is_dirty_ = false ; disk_manager_->write_page (page->GetPageId ().fd, page->GetPageId ().page_no, page->GetData (), PAGE_SIZE); } page_table_.erase (page->GetPageId ()); page_table_.insert (std::make_pair (new_page_id, new_frame_id)); page->ResetMemory (); page->id_ = new_page_id; }
在page.h文件中定义is_dirty_
用于判断是否为脏页,调用即可。
如果是脏页,那么首先设为净页,并且写回磁盘,调用的是disk_manager中的write_page函数
清除旧页面并且将新页面添加进page_table_
中,重置page的data,更新page id。
FetchPage函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 Page *BufferPoolManager::FetchPage (PageId page_id) { std::scoped_lock lock{latch_}; std::unordered_map<PageId, frame_id_t , PageIdHash>::iterator it = page_table_.find (page_id); if (it != page_table_.end ()){ frame_id_t fid = it->second; replacer_->Pin (fid); pages_[fid].pin_count_++; return &pages_[fid]; } else { frame_id_t frame_id = -1 ; if (!FindVictimPage (&frame_id)){ return nullptr ; } Page *R = &pages_[frame_id]; UpdatePage (R, page_id, frame_id); disk_manager_->read_page (page_id.fd, page_id.page_no, R->data_, PAGE_SIZE); replacer_->Pin (frame_id); R->pin_count_ = 1 ; return R; } }
先上锁,std::scoped_lock lock{latch_};
它能够避免死锁发生,其构造函数能够自动进行上锁操作,析构函数会对互斥量进行解锁操作,保证线程安全。
后在page_table_
中查找page_id,如果it != page_table_.end()
说明找到了,页表中存在page_id,该page在缓冲池中。对其进行Pin操作使其防止被替换掉,并且pin_count++。如果没找到,那么我们需要把他从磁盘中调出,并且淘汰缓冲池。检查是否为脏页,执行UpdatePage操作即可。腾出位置给该page,并对其进行Pin操作,初始化pin_count_
为1。
UnpinPage函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 bool BufferPoolManager::UnpinPage (PageId page_id, bool is_dirty) { std::scoped_lock lock{latch_}; std::unordered_map<PageId, frame_id_t , PageIdHash>::iterator it = page_table_.find (page_id); if (it == page_table_.end ()){ return false ; } else { frame_id_t fid = it->second; Page *P = &pages_[fid]; if (P->pin_count_ == 0 ){ return false ; } P->pin_count_ --; if (P->pin_count_ == 0 ){ replacer_->Unpin (fid); } if (is_dirty){ P->is_dirty_ = true ; } return true ; } }
先上锁,查询所给的page_id是否在页表中存在,如果不存在返回false,否则根据判断page的pin_count_的次数,如果UnPin前就为0则报错返回false,如果UnPin前为1执行操作后变为0,则将其放入replacer中的LRUlist_
表中,否则不进行操作。使用id_dirty判断是否置脏。
FlushPage函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 bool BufferPoolManager::FlushPage (PageId page_id) { std::scoped_lock lock{latch_}; std::unordered_map<PageId, frame_id_t , PageIdHash>::iterator it = page_table_.find (page_id); if (it == page_table_.end ()){ return false ; } else { frame_id_t fid = it->second; Page *P = &pages_[fid]; disk_manager_->write_page (P->GetPageId ().fd, P->GetPageId ().page_no, P->GetData (), PAGE_SIZE); P->is_dirty_ = false ; return true ; } }
先上锁,在页表中查找,找不到返回false,如果找到则将其写回磁盘,并置净。
NewPage函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 Page *BufferPoolManager::NewPage (PageId *page_id) { std::scoped_lock lock{latch_}; frame_id_t fid = -1 ; page_id->page_no = disk_manager_->AllocatePage (page_id->fd); if (!FindVictimPage (&fid)) { return nullptr ; } Page *P = &pages_[fid]; UpdatePage (P, *page_id, fid); replacer_->Pin (fid); P->pin_count_ = 1 ; return P; }
上锁,分配新页面,检查缓冲池是否有空间进行淘汰,更新页面,进行Pin操作,设置为初始化1,之后将页面返回。
DeletePage函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 bool BufferPoolManager::DeletePage (PageId page_id) { std::scoped_lock lock{latch_}; std::unordered_map<PageId, frame_id_t , PageIdHash>::iterator it = page_table_.find (page_id); if (it == page_table_.end ()){ return true ; } else { frame_id_t fid = it->second; Page *P = &pages_[fid]; if (P->pin_count_ != 0 ){ return false ; } else { disk_manager_->DeallocatePage (page_id.page_no); page_id.page_no = INVALID_PAGE_ID; UpdatePage (P, page_id, fid); free_list_.push_back (fid); return true ; } } return false ; }
上锁,检查页面是否存在,如果不存在则说明已删除返回true,如果存在但是仍然处于pin状态,即pin_count_
不为0,则失败返回false,否则移除释放页面空间,放回free_list_
,操作成功返回true。
任务1.2 记录管理器 通关截图
任务1.2.1 记录操作 任务要求 本任务要求补全RMFileHandle
类,其负责对文件中的记录进行操作。
每个RMFileHandle
对应一个记录文件,当RMManager
执行打开文件操作时,便会创建一个指向RMFileHandle
的指针。
RMFileHandle
类的接口如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class RmFileHandle { public : RmFileHandle (DiskManager *disk_manager, BufferPoolManager *buffer_pool_manager, int fd); std::unique_ptr<RmRecord> get_record (const Rid &rid, Context *context) const ; Rid insert_record (char *buf, Context *context) ; void delete_record (const Rid &rid, Context *context) ; void update_record (const Rid &rid, char *buf, Context *context) ; RmPageHandle create_new_page_handle () ; RmPageHandle fetch_page_handle (int page_no) const ; RmPageHandle create_page_handle () ; void release_page_handle (RmPageHandle &page_handle) ; };
代码分析 get_record函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 std::unique_ptr<RmRecord> RmFileHandle::get_record (const Rid &rid, Context *context) const { auto record = std::make_unique<RmRecord>(file_hdr_.record_size); RmPageHandle ph = fetch_page_handle (rid.page_no); char *slot = ph.get_slot (rid.slot_no); record->size = file_hdr_.record_size; memcpy (record->data, slot, record->size); return record; }
insert_record函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 Rid RmFileHandle::insert_record (char *buf, Context *context) { RmPageHandle ph = create_page_handle (); int slot_no = Bitmap::first_bit (false , ph.bitmap, file_hdr_.num_records_per_page); Bitmap::set (ph.bitmap, slot_no); ph.page_hdr->num_records++; if (ph.page_hdr->num_records == file_hdr_.num_records_per_page) { file_hdr_.first_free_page_no = ph.page_hdr->next_free_page_no; } char *slot = ph.get_slot (slot_no); memcpy (slot, buf, file_hdr_.record_size); return Rid{ph.page->GetPageId ().page_no, slot_no}; }
delete_record函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void RmFileHandle::delete_record (const Rid &rid, Context *context) { RmPageHandle ph = fetch_page_handle (rid.page_no); if (ph.page_hdr->num_records == file_hdr_.num_records_per_page) { release_page_handle (ph); } Bitmap::reset (ph.bitmap, rid.slot_no); ph.page_hdr->num_records--; }
update_record函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void RmFileHandle::update_record (const Rid &rid, char *buf, Context *context) { RmPageHandle ph = fetch_page_handle (rid.page_no); char *slot = ph.get_slot (rid.slot_no); memcpy (slot, buf, file_hdr_.record_size); }
fetch_page_handle函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 RmPageHandle RmFileHandle::fetch_page_handle (int page_no) const { if (page_no >= file_hdr_.num_pages){ throw PageNotExistError ("name" ,page_no); } PageId page_id; page_id.fd = fd_; page_id.page_no = page_no; Page *page = nullptr ; page = buffer_pool_manager_->FetchPage (page_id); RmPageHandle ph = RmPageHandle (&file_hdr_, page); return ph; }
create_new_page_handle函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 RmPageHandle RmFileHandle::create_new_page_handle () { PageId *page_id = new PageId; page_id->fd = fd_; Page *page = nullptr ; page = buffer_pool_manager_->NewPage (page_id); RmPageHandle ph = RmPageHandle (&file_hdr_, page); ph.page_hdr->num_records = 0 ; ph.page_hdr->next_free_page_no = RM_NO_PAGE; Bitmap::init (ph.bitmap, file_hdr_.bitmap_size); file_hdr_.num_pages ++; file_hdr_.first_free_page_no = page->GetPageId ().page_no; return ph; }
create_page_handle函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 RmPageHandle RmFileHandle::create_page_handle () { if (file_hdr_.first_free_page_no == RM_NO_PAGE){ return create_new_page_handle (); } else { return fetch_page_handle (file_hdr_.first_free_page_no); } }
release_page_handle函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void RmFileHandle::release_page_handle (RmPageHandle &page_handle) { page_handle.page_hdr->next_free_page_no = file_hdr_.first_free_page_no; file_hdr_.first_free_page_no = page_handle.page->GetPageId ().page_no; }
任务1.2.2 记录迭代器 任务要求 本任务要求补全RmScan
类,其用于遍历文件中存放的记录。
RmScan
类继承于RecScan
类,它们的接口如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class RecScan { public: virtual ~RecScan() = default; virtual void next() = 0; virtual bool is_end() const = 0; virtual Rid rid() const = 0; }; class RmScan : public RecScan { public: RmScan(const RmFileHandle *file_handle); void next() override; bool is_end() const override; Rid rid() const override; };
代码分析 RmScan函数 1 2 3 4 5 6 7 8 9 10 11 RmScan::RmScan (const RmFileHandle *file_handle) : file_handle_ (file_handle) { rid_ = Rid{RM_FIRST_RECORD_PAGE, -1 }; next (); }
next函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void RmScan::next () { while (rid_.page_no < file_handle_->file_hdr_.num_pages) { RmPageHandle ph = file_handle_->fetch_page_handle (rid_.page_no); rid_.slot_no = Bitmap::next_bit (true , ph.bitmap, file_handle_->file_hdr_.num_records_per_page, rid_.slot_no); if (rid_.slot_no < file_handle_->file_hdr_.num_records_per_page) { return ; } rid_.slot_no = -1 ; rid_.page_no++; } rid_.page_no = RM_NO_PAGE; }
is_end函数 1 2 3 4 5 6 7 bool RmScan::is_end () const { return rid_.page_no == RM_NO_PAGE; }
rid函数 1 2 3 4 5 6 7 Rid RmScan::rid () const { return rid_; }