相关链接

Rucbase项目地址:GitHub - ruc-deke/rucbase-lab: RUC Educational Database Project open lab

Rucbase-Lab1实验文档地址:rucbase-lab/docs/Rucbase-Lab1[存储管理实验文档].md at main · ruc-deke/rucbase-lab

任务1 缓冲池管理器

任务1.1 磁盘存储管理器

注意事项

像这样的注意事项在后续任务中不会特地标识,只因为这里是系列文章的开头,所以特地在此讲解,后续相同的内容不再陈述。

该任务的内容是补全DiskManager类,这个类在/rucbase-lab/src/storage中的disk_manager.cpp文件中,你需要编写这个文件中带有

// Todo:
// 题目要求1
// 题目要求2

该形式注释的所有函数,其余内容不需填写。(即便他是空的函数)

大多数函数都会在函数定义上方编写按以下规格的注释

/**
 * @brief 函数描述
 * @description: 函数描述
 * @param {输入值类型} 变量名称 变量功能
 * @return {返回值类型} 返回值功能
 */

这段注释的功能是让你能在vscode等编辑器中只需将鼠标移动到函数名上就看到他的函数特性。

预备函数介绍

lseek函数

off_t lseek(int __fd, off_t __offset, int __whence)

他有三个输入:fd,offset,whence

其中fd为磁盘文件的文件句柄,它用来确定需要操作的文件。

offset是文件光标的偏移量,他的功能是让光标移动offset个字节的长度。

whence是用于确定文件光标在该次移动中的起始位置,他有一套定义好的参数:

#define SEEK_SET	0	//从文件开头开始移动光标
#define SEEK_CUR	1	//从文件光标目前的位置开始移动光标
#define SEEK_END	2	//从文件末尾开始移动光标
#define SEEK_DATA	3	//从文件的下一个数据块开始
#define SEEK_HOLE	4	//从文件的下一个空洞开始

举个例子:

第一个需要撰写的函数write_page需要用到这一行代码:

lseek(fd, page_no * PAGE_SIZE, SEEK_SET);            // 定位到文件头

其中,fd用于指示数据需要写入哪个文件,而一个文件会含有多个数据页(存储数据内容的最小单位),由于这个函数的功能就是写入一页数据,若要符合要求,offset光标肯定只能按页移动,因此这里从文件的最前端开始,将光标移动 页码乘上页的长度(4KByte) 的距离,移动到对应页的开头。

类结构说明

DiskManager类

DiskManager类的头文件定义如下所示

/* Copyright (c) 2023 Renmin University of China
RMDB is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
        http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details. */

#pragma once

#include <fcntl.h>     
#include <sys/stat.h>  
#include <unistd.h>    

#include <atomic>
#include <fstream>
#include <iostream>
#include <string>
#include <unordered_map>

#include "common/config.h"
#include "errors.h"  

/**
 * @description: DiskManager的作用主要是根据上层的需要对磁盘文件进行操作
 */
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 allocate_page(int fd);

    void deallocate_page(page_id_t page_id);

    /*目录操作*/
    bool is_dir(const std::string &path);

    void create_dir(const std::string &path);

    void destroy_dir(const std::string &path);

    /*文件操作*/
    bool is_file(const std::string &path);

    void create_file(const std::string &path);

    void destroy_file(const std::string &path);

    int open_file(const std::string &path);

    void close_file(int fd);

    int get_file_size(const std::string &file_name);

    std::string get_file_name(int fd);

    int get_file_fd(const std::string &file_name);

    /*日志操作*/
    int read_log(char *log_data, int size, int offset);

    void write_log(char *log_data, int size);

    void SetLogFd(int log_fd) { log_fd_ = log_fd; }

    int GetLogFd() { return log_fd_; }

    /**
     * @description: 设置文件已经分配的页面个数
     * @param {int} fd 文件对应的文件句柄
     * @param {int} start_page_no 已经分配的页面个数,即文件接下来从start_page_no开始分配页面编号
     */
    void set_fd2pageno(int fd, int start_page_no) { fd2pageno_[fd] = start_page_no; }

    /**
     * @description: 获得文件目前已分配的页面个数,即如果文件要分配一个新页面,需要从fd2pagenp_[fd]开始分配
     * @return {page_id_t} 已分配的页面个数 
     * @param {int} fd 文件对应的句柄
     */
    page_id_t get_fd2pageno(int fd) { return fd2pageno_[fd]; }

    static constexpr int MAX_FD = 8192;

   private:
    // 文件打开列表,用于记录文件是否被打开
    std::unordered_map<std::string, int> path2fd_;  //<Page文件磁盘路径,Page fd>哈希表
    std::unordered_map<int, std::string> fd2path_;  //<Page fd,Page文件磁盘路径>哈希表

    int log_fd_ = -1;                             // WAL日志文件的文件句柄,默认为-1,代表未打开日志文件
    std::atomic<page_id_t> fd2pageno_[MAX_FD]{};  // 文件中已经分配的页面个数,初始值为0
};

在DiskManager类中,存有以下数据:

static constexpr int MAX_FD = 8192;

// 文件打开列表,用于记录文件是否被打开
std::unordered_map<std::string, int> path2fd_;  //<Page文件磁盘路径,Page fd>哈希表
std::unordered_map<int, std::string> fd2path_;  //<Page fd,Page文件磁盘路径>哈希表

int log_fd_ = -1;                             // WAL日志文件的文件句柄,默认为-1,代表未打开日志文件
std::atomic<page_id_t> fd2pageno_[MAX_FD]{};  // 文件中已经分配的页面个数,初始值为0

MAX_FD

是DiskManager中存储的常数,定义在public中,意味着外部可以直接访问。

其功能是规定并标识DiskManager所能操作的文件数的最大值。

path2fd_、fd2path_

是DiskManager中建立的哈希表,他们共同组成了文件打开列表,定义在private中。

仅限于该类的数据都会被定义在private中,以免外部类或函数的非法访问,除非这个数据需要在外部的功能中被频繁访问。

被DiskManager(或者说使用Linux在C语言中的函数)打开的文件会有两个重要的信息:文件路径(path)和文件句柄(fd),这两个哈希表可以帮助路径和句柄进行快速地转换。同时,被打开的文件一定拥有文件句柄而未打开的文件不会拥有(因为C语言中用于Linux的open函数会返回一个文件句柄以便操作,因此这个设计实际上属于Linux系统本身,DiskManager在这里建立哈希表仅是用于自身的记录)。

log_fd_

是DiskManager中专用于指向日志文件的文件句柄,如果置-1意为未打开,因为文件句柄一定>=0

fd2pageno_

是DiskManager中用于存储一个文件中分配了几个页面数量的数据,如:fd2pageno_[10]=11就意味着文件句柄fd为10的文件中存储了11个page。

函数功能思路

1. write_page

/**
 * @description: 将数据写入文件的指定磁盘页面中
 * @param {int} fd 磁盘文件的文件句柄
 * @param {page_id_t} page_no 写入目标页面的page_id
 * @param {char} *offset 要写入磁盘的数据
 * @param {int} num_bytes 要写入磁盘的数据大小
 */
void DiskManager::write_page(int fd, page_id_t page_no, const char *offset, int num_bytes) {
    // Todo:
    // 1.lseek()定位到文件头,通过(fd,page_no)可以定位指定页面及其在磁盘文件中的偏移量
    // 2.调用write()函数
    // 注意write返回值与num_bytes不等时 throw InternalError("DiskManager::write_page Error");
    lseek(fd, page_no * PAGE_SIZE, SEEK_SET);            // 定位到文件头
    ssize_t bytes_write = write(fd, offset, num_bytes);  // 用write函数写入数据,返回写入的字节数
    if (bytes_write != num_bytes) {  // 如果写入的字节数与目标写入字节数num_bytes不等
        throw InternalError("DiskManager::write_page Error");  // 抛出指定的异常
    }
}

write_page函数有多个输入:文件句柄fd、页面在文件中的顺序page_no、写入内容offset、写入内容的长度num_bytes。

文件的修改和人工使用终端vim的操作类似,先打开一个文件,再移动我们的光标到对应需要修改的位置。lseek函数能够完成这两个工作:先打开第一个参数(fd)对应的文件,第三个参数(SEEK_SET)用于指定打开文件时光标的默认位置为开头,而第二个参数则从默认位置开始向后移动光标。

write函数用于完成写入操作。write函数使用的写入方式是平常使用文本编辑器不常用的覆盖模式(在一般的Word程序中,按下键盘的Insert键可以切换到覆盖模式,再按一下切换回来):输入的内容会覆盖掉原先的内容而非插入到原先的内容之间。他会返回成功写入的字节数。

当成功写入的字节数和原本写入内容的长度(目标写入字符数)不相符,说明写入不正确(写入时丢失了数据或写入了过多的数据),因此应当抛出异常。

2.read_page

void DiskManager::read_page(int fd, page_id_t page_no, char *offset, int num_bytes) {
    // Todo:
    // 1.lseek()定位到文件头,通过(fd,page_no)可以定位指定页面及其在磁盘文件中的偏移量
    // 2.调用read()函数
    // 注意read返回值与num_bytes不等时,throw InternalError("DiskManager::read_page Error");
    lseek(fd, page_no * PAGE_SIZE, SEEK_SET);          // 定位到文件头
    ssize_t bytes_read = read(fd, offset, num_bytes);  // 用read函数读取数据,返回读取的字节数
    if (bytes_read != num_bytes) {  // 如果读取的字节数与目标读取字节数num_bytes不等
        throw InternalError("DiskManager::read_page Error");  // 抛出指定的异常
    }
}

read_page函数所需的参数和write_page相同,且运行机制类似,此处不再赘述。

3.create_file

/**
 * @description: 用于创建指定路径文件
 * @return {*}
 * @param {string} &path
 */
void DiskManager::create_file(const std::string &path) {
    // Todo:
    // 调用open()函数,使用O_CREAT模式
    // 注意不能重复创建相同文件
    if (is_file(path)) {// 如果文件存在
        throw FileExistsError(path);    //抛出文件存在异常
    }
    int fd = open(path.c_str(), O_CREAT | O_EXCL, 0644);    // 使用O_CREATE模式 O_EXCL模式表示不能重复创建
    if (fd == -1) {// 当open函数创建失败,则返回fd为-1
        throw UnixError();// 抛出Unix异常
    }
    close(fd);  // 关闭文件
}

该函数输入值为string类型的path,需要通过这个路径数据进行文件的创建

创建一个文件需要以下步骤:检查文件是否存在,如果存在则不能创建以免误将其他文件重置-->创建文件-->检查是否创建失败-->由于是使用open函数创建,他实际上打开了一个新文件,但这里不需要进一步操作,因此要关闭掉文件流

4.destroy_file

源码

源码展示:

/* Copyright (c) 2023 Renmin University of China
RMDB is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
        http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details. */

#include "storage/disk_manager.h"

#include <assert.h>    // for assert
#include <string.h>    // for memset
#include <sys/stat.h>  // for stat
#include <unistd.h>    // for lseek

#include "defs.h"

DiskManager::DiskManager() { memset(fd2pageno_, 0, MAX_FD * (sizeof(std::atomic<page_id_t>) / sizeof(char))); }

/**
 * @description: 将数据写入文件的指定磁盘页面中
 * @param {int} fd 磁盘文件的文件句柄
 * @param {page_id_t} page_no 写入目标页面的page_id
 * @param {char} *offset 要写入磁盘的数据
 * @param {int} num_bytes 要写入磁盘的数据大小
 */
void DiskManager::write_page(int fd, page_id_t page_no, const char *offset, int num_bytes) {
    // Todo:
    // 1.lseek()定位到文件头,通过(fd,page_no)可以定位指定页面及其在磁盘文件中的偏移量
    // 2.调用write()函数
    // 注意write返回值与num_bytes不等时 throw InternalError("DiskManager::write_page Error");
    lseek(fd, page_no * PAGE_SIZE, SEEK_SET);            // 定位到文件头
    ssize_t bytes_write = write(fd, offset, num_bytes);  // 用write函数写入数据,返回写入的字节数
    if (bytes_write != num_bytes) {  // 如果写入的字节数与目标写入字节数num_bytes不等
        throw InternalError("DiskManager::write_page Error");  // 抛出指定的异常
    }
}

/**
 * @description: 读取文件中指定编号的页面中的部分数据到内存中
 * @param {int} fd 磁盘文件的文件句柄
 * @param {page_id_t} page_no 指定的页面编号
 * @param {char} *offset 读取的内容写入到offset中
 * @param {int} num_bytes 读取的数据量大小
 */
void DiskManager::read_page(int fd, page_id_t page_no, char *offset, int num_bytes) {
    // Todo:
    // 1.lseek()定位到文件头,通过(fd,page_no)可以定位指定页面及其在磁盘文件中的偏移量
    // 2.调用read()函数
    // 注意read返回值与num_bytes不等时,throw InternalError("DiskManager::read_page Error");
    lseek(fd, page_no * PAGE_SIZE, SEEK_SET);          // 定位到文件头
    ssize_t bytes_read = read(fd, offset, num_bytes);  // 用read函数读取数据,返回读取的字节数
    if (bytes_read != num_bytes) {  // 如果读取的字节数与目标读取字节数num_bytes不等
        throw InternalError("DiskManager::read_page Error");  // 抛出指定的异常
    }
}

/**
 * @description: 分配一个新的页号
 * @return {page_id_t} 分配的新页号
 * @param {int} fd 指定文件的文件句柄
 */
page_id_t DiskManager::allocate_page(int fd) {
    // 简单的自增分配策略,指定文件的页面编号加1
    assert(fd >= 0 && fd < MAX_FD);
    return fd2pageno_[fd]++;
}

void DiskManager::deallocate_page(__attribute__((unused)) page_id_t page_id) {}

/**
 * @brief 该函数判断指定路径是否是目录
 * @param {string} path 路径地址
 * @return {bool} 返回是否是目录
 */
bool DiskManager::is_dir(const std::string &path) {
    struct stat st;
    return stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode);
}

/**
 * @brief 该函数创建一个目录
 * @param {string} path 目录路径
 * @return {void} 无返回值
 */
void DiskManager::create_dir(const std::string &path) {
    // Create a subdirectory
    std::string cmd = "mkdir " + path;  // 这里是向cmd写入一个创建目录的命令
    if (system(cmd.c_str()) < 0) {      // 创建一个名为path的目录
        throw UnixError();              // 创建失败,抛出Unix异常
    }
}

/**
 * @brief 用于删除指定路径的目录
 *
 * @param {string} path
 */
void DiskManager::destroy_dir(const std::string &path) {
    std::string cmd = "rm -r " + path;  // 删除目录,-r表示递归删除,即删除目录下的所有文件
    if (system(cmd.c_str()) < 0) {      // 删除目录,这个命令使用system函数调用并执行
        throw UnixError();              // 返回值小于0时意味着删除失败,抛出Unix异常
    }
}

/**
 * @description: 判断指定路径文件是否存在
 * @return {bool} 若指定路径文件存在则返回true
 * @param {string} &path 指定路径文件
 */
bool DiskManager::is_file(const std::string &path) {
    // 用struct stat获取文件信息
    struct stat st;
    return stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode);
}

/**
 * @description: 用于创建指定路径文件
 * @return {*}
 * @param {string} &path
 */
void DiskManager::create_file(const std::string &path) {
    // Todo:
    // 调用open()函数,使用O_CREAT模式
    // 注意不能重复创建相同文件
    if (is_file(path)) {
        throw FileExistsError(path);
    }
    int fd = open(path.c_str(), O_CREAT | O_EXCL, 0644);
    if (fd == -1) {
        throw UnixError();
    }
    close(fd);
}

/**
 * @description: 删除指定路径的文件
 * @param {string} &path 文件所在路径
 */
void DiskManager::destroy_file(const std::string &path) {
    // Todo:
    // 调用unlink()函数
    // 注意不能删除未关闭的文件
    if (!is_file(path)) {               // 检测路径是否正确
        throw FileNotFoundError(path);  // 如果不正确,抛出文件未找到异常
    }
    if (path2fd_.count(path)) {          // 检测文件是否关闭
        throw FileNotClosedError(path);  // 如果没关闭,抛出文件未关闭异常
    }
    if (unlink(path.c_str()) == -1) {  // 删除文件
        throw UnixError();             // 删除失败,抛出Unix异常
    }  // 这个判断是多余的,因为unlink函数不会删除目录,只会删除文件,所以不添加也可以通过测试
}

/**
 * @description: 打开指定路径文件
 * @return {int} 返回打开的文件的文件句柄
 * @param {string} &path 文件所在路径
 */
int DiskManager::open_file(const std::string &path) {
    // Todo:
    // 调用open()函数,使用O_RDWR模式
    // 注意不能重复打开相同文件,并且需要更新文件打开列表
    if (path2fd_.count(path)) {  // 检测文件是否已经打开,path2fd_是一个哈希表,和fd2path_用于记录文件是否被打开
        // 这里的path2fd_和fd2path_就是题目要求所提到的文件打开列表
        // 从表名来看,path2fd_是通过文件路径找到文件句柄,fd2path_是通过文件句柄找到文件路径
        // 因此输入值为path时应当用path2fd_,输入值为fd时应当用fd2path_
        throw FileNotClosedError(path);
    }
    if (!this->is_file(path)) {  // 检测路径是否正确
        throw FileNotFoundError(path);
    }
    int fd = open(path.c_str(), O_RDWR);  // 打开文件
    if (fd == -1) {                       // 打开文件失败
        throw UnixError();                // 抛出Unix异常
    }
    // 更新文件打开列表
    path2fd_[path] = fd;
    fd2path_[fd] = path;
    return fd;  // 返回文件句柄
}

/**
 * @description:用于关闭指定路径文件
 * @param {int} fd 打开的文件的文件句柄
 */
void DiskManager::close_file(int fd) {
    // Todo:
    // 调用close()函数
    // 注意不能关闭未打开的文件,并且需要更新文件打开列表
    if (!fd2path_.count(fd)) {       // 检测文件是否打开
        throw FileNotOpenError(fd);  // 如果没有打开,抛出文件未打开异常
    }
    close(fd);                        // 关闭文件
    std::string path = fd2path_[fd];  // 通过文件句柄找到文件路径
    // 更新文件打开列表
    fd2path_.erase(fd);    // 从fd2path_中删除文件句柄
    path2fd_.erase(path);  // 从path2fd_中删除文件路径
}

/**
 * @description: 获得文件的大小
 * @return {int} 文件的大小
 * @param {string} &file_name 文件名
 */
int DiskManager::get_file_size(const std::string &file_name) {
    struct stat stat_buf;                         // 使用Linux中的stat函数获取文件信息
    int rc = stat(file_name.c_str(), &stat_buf);  // stat函数返回0代表成功,-1代表失败
    return rc == 0 ? stat_buf.st_size : -1;       // 如果成功,返回文件大小,否则返回-1
}

/**
 * @description: 根据文件句柄获得文件名
 * @return {string} 文件句柄对应文件的文件名
 * @param {int} fd 文件句柄
 */
std::string DiskManager::get_file_name(int fd) {
    if (!fd2path_.count(fd)) {       // 检测文件是否打开
        throw FileNotOpenError(fd);  // 如果没有打开,抛出文件未打开异常
    }
    return fd2path_[fd];  // 返回文件路径
}

/**
 * @description:  获得文件名对应的文件句柄
 * @return {int} 文件句柄
 * @param {string} &file_name 文件名
 */
int DiskManager::get_file_fd(const std::string &file_name) {
    if (!path2fd_.count(file_name)) {  // 检测文件是否打开
        return open_file(file_name);   // 如果没有打开,打开文件,返回文件句柄
    }
    return path2fd_[file_name];  // 返回文件句柄
}

/**
 * @description:  读取日志文件内容
 * @return {int} 返回读取的数据量,若为-1说明读取数据的起始位置超过了文件大小
 * @param {char} *log_data 读取内容到log_data中
 * @param {int} size 读取的数据量大小
 * @param {int} offset 读取的内容在文件中的位置
 */
int DiskManager::read_log(char *log_data, int size, int offset) {  // 读取日志文件内容
    // read log file from the previous end
    if (log_fd_ == -1) {                     // 如果日志文件未打开
        log_fd_ = open_file(LOG_FILE_NAME);  // 打开日志文件
    }
    int file_size = get_file_size(LOG_FILE_NAME);  // 获取日志文件大小
    if (offset > file_size) {  // 如果读取的数据起始位置超过了文件大小,说明输入的offset不合法
        return -1;             // 返回-1,表示读取失败
    }

    size = std::min(size, file_size - offset);           // 读取的数据量大小不能超过文件大小
    if (size == 0) return 0;                             // 如果读取的数据量大小为0,直接返回0
    lseek(log_fd_, offset, SEEK_SET);                    // 定位到offset位置
    ssize_t bytes_read = read(log_fd_, log_data, size);  // 读取数据
    assert(bytes_read == size);  // 读取的数据量大小应该等于size,否则抛出异常
    // assert函数的作用是如果它括号中的条件语句返回false,则终止程序执行
    // 这个函数的好处是在抛出异常时会标明在哪个文件的哪个行号出现了运行问题,
    // 缺陷是没有更具体的信息,在不看代码的时候无法确定问题出在哪里,因此只能用于调试
    return bytes_read;  // 返回读取的数据量大小
}

/**
 * @description: 写日志内容
 * @param {char} *log_data 要写入的日志内容
 * @param {int} size 要写入的内容大小
 */
void DiskManager::write_log(char *log_data, int size) {
    if (log_fd_ == -1) {                     // 如果日志文件未打开
        log_fd_ = open_file(LOG_FILE_NAME);  // 打开日志文件
    }

    // write from the file_end
    lseek(log_fd_, 0, SEEK_END);                           // 定位到文件末尾
    ssize_t bytes_write = write(log_fd_, log_data, size);  // 写入数据
    if (bytes_write != size) {                             // 如果写入的数据量大小不等于size
        throw UnixError();                                 // 抛出Unix异常
    }
}

代码测试

如果你按要求写完了所有的代码,想要测试一下是否正确:

请在终端中移动到rucbase-lab这一文件夹下(项目文件夹)的build文件夹中,输入测试程序的编译指令并运行他,全过程如下:

//首先移动到rucbase-lab文件夹下
cd build
make disk_manager_test      //编译测试程序
./bin/disk_manager_test     //运行测试程序

如果代码没有任何问题,那么他会显示如下内容:

stu@stu-VirtualBox:~/rucbase/rucbase-lab/build$ ./bin/disk_manager_test
Running main() from /home/stu/rucbase/rucbase-lab/deps/googletest/googletest/src/gtest_main.cc
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from DiskManagerTest
[ RUN      ] DiskManagerTest.FileOperation
[       OK ] DiskManagerTest.FileOperation (3 ms)
[ RUN      ] DiskManagerTest.PageOperation
[       OK ] DiskManagerTest.PageOperation (6 ms)
[----------] 2 tests from DiskManagerTest (10 ms total)

[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (10 ms total)
[  PASSED  ] 2 tests.

只要看到PASSED就意味着该次测试完美通过。

如果不是,可能会看到如下情况:

Running main() from /home/stu/rucbase/rucbase-lab/deps/googletest/googletest/src/gtest_main.cc
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from DiskManagerTest
[ RUN      ] DiskManagerTest.FileOperation
disk_manager_test: /home/stu/rucbase/rucbase-lab/src/test/storage/disk_manager_test.cpp:106: virtual void DiskManagerTest_FileOperation_Test::TestBody(): Assertion `false' failed.
已放弃 (核心已转储)

错误代码中的最后几行会告诉你错在哪里:

disk_manager_test: /home/stu/rucbase/rucbase-lab/src/test/storage/disk_manager_test.cpp:106: virtual void 

这一条信息是告诉你测试程序的地址和测试程序中哪一条测试代码没有通过。比如这里就是/home/stu/rucbase/rucbase-lab/src/test/storage/disk_manager_test.cpp中的106行代码报错。

vscode中只要用ctrl+左键点击该路径就可以打开该代码并移动到对应行,接下来就看看他的注释,就知道自己的错误在哪个功能上。

任务1.2 缓冲池替换策略

源码

/* Copyright (c) 2023 Renmin University of China
RMDB is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
        http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details. */

#include "lru_replacer.h"

LRUReplacer::LRUReplacer(size_t num_pages) { max_size_ = num_pages; }

LRUReplacer::~LRUReplacer() = default;  

/**
 * @description: 使用LRU策略删除一个victim frame,并返回该frame的id
 * @param {frame_id_t*} frame_id 被移除的frame的id,如果没有frame被移除返回nullptr
 * @return {bool} 如果成功淘汰了一个页面则返回true,否则返回false
 */
bool LRUReplacer::victim(frame_id_t* frame_id) {
    // C++17 std::scoped_lock
    // 它能够避免死锁发生,其构造函数能够自动进行上锁操作,析构函数会对互斥量进行解锁操作,保证线程安全。
    std::scoped_lock lock{latch_};  //  如果编译报错可以替换成其他lock

    // Todo:
    //  利用lru_replacer中的LRUlist_,LRUHash_实现LRU策略
    //  选择合适的frame指定为淘汰页面,赋值给*frame_id

    if (LRUlist_.empty()) {
        return false;  // 无页面可淘汰
    }

    *frame_id = LRUlist_.back();  // 选择链表尾部(LRU页面)
    LRUlist_.pop_back();          // 从链表移除
    LRUhash_.erase(*frame_id);    // 从哈希表移除

    return true;
}

/**
 * @description: 固定指定的frame,即该页面无法被淘汰
 * @param {frame_id_t} 需要固定的frame的id
 */
void LRUReplacer::pin(frame_id_t frame_id) {
    std::scoped_lock lock{latch_};
    // Todo:
    // 固定指定id的frame
    // 在数据结构中移除该frame

    auto it = LRUhash_.find(frame_id);  // 在哈希表中查找frame_id
    if (it != LRUhash_.end()) {      // 如果存在于可淘汰队列
        LRUlist_.erase(it->second);  // 从链表删除
        LRUhash_.erase(it);          // 从哈希表删除
    }
}

/**
 * @description: 取消固定一个frame,代表该页面可以被淘汰
 * @param {frame_id_t} frame_id 取消固定的frame的id
 */
void LRUReplacer::unpin(frame_id_t frame_id) {
    // Todo:
    //  支持并发锁
    //  选择一个frame取消固定
    std::scoped_lock lock{latch_};

    if (LRUhash_.count(frame_id) == 0) {        // 避免重复插入
        LRUlist_.push_front(frame_id);          // 插入链表头部
        LRUhash_[frame_id] = LRUlist_.begin();  // 记录迭代器
    }
}

/**
 * @description: 获取当前replacer中可以被淘汰的页面数量
 */
size_t LRUReplacer::Size() { return LRUlist_.size(); }

代码测试

cd build
make lru_replacer_test
./bin/lru_replacer_test

任务1.3 缓冲池管理器

源码

/* Copyright (c) 2023 Renmin University of China
RMDB is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
        http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details. */

#include "buffer_pool_manager.h"

/**
 * @description: 从free_list或replacer中得到可淘汰帧页的 *frame_id
 * @return {bool} true: 可替换帧查找成功 , false: 可替换帧查找失败
 * @param {frame_id_t*} frame_id 帧页id指针,返回成功找到的可替换帧id
 */
bool BufferPoolManager::find_victim_page(frame_id_t* frame_id) {
    // Todo:
    // 1 使用BufferPoolManager::free_list_判断缓冲池是否已满需要淘汰页面
    // 1.1 未满获得frame
    // 1.2 已满使用lru_replacer中的方法选择淘汰页面

    if (!free_list_.empty()) {          // 如果缓冲池未满
        *frame_id = free_list_.front(); // 从free_list中获取一个frame空位
        free_list_.pop_front();         // 将该空位从空闲帧列表删除
        return true;                    // 返回成功
    }

    return replacer_->victim(frame_id); // 调用LRU淘汰策略
}

/**
 * @description: 更新页面数据, 如果为脏页则需写入磁盘,再更新为新页面,更新page元数据(data, is_dirty, page_id)和page table
 * @param {Page*} page 写回页指针
 * @param {PageId} new_page_id 新的page_id
 * @param {frame_id_t} new_frame_id 新的帧frame_id
 */
void BufferPoolManager::update_page(Page *page, PageId new_page_id, frame_id_t new_frame_id) {
    // Todo:
    // 1 如果是脏页,写回磁盘,并且把dirty置为false
    // 2 更新page table
    // 3 重置page的data,更新page id

    if (page->is_dirty_) {// 如果是脏页
        // 脏页:被修改的页面但没有写入磁盘的页会被标注为脏页,相当于没有保存写入的内容
        disk_manager_->write_page(page->id_.fd, page->id_.page_no, page->data_, PAGE_SIZE);// 写回磁盘
        page->is_dirty_ = false;// dirty置为false
    }
    // 这里将其保存到磁盘,这样该页就不再为脏页

    page_table_.erase(page->id_);             // 移除旧页表项
    page->id_ = new_page_id;                  // 更新页面ID
    page_table_[new_page_id] = new_frame_id;  // 添加新页表项
}

/**
 * @description: 从buffer pool获取需要的页。
 *              如果页表中存在page_id(说明该page在缓冲池中),并且pin_count++。
 *              如果页表不存在page_id(说明该page在磁盘中),则找缓冲池victim page,将其替换为磁盘中读取的page,pin_count置1。
 * @return {Page*} 若获得了需要的页则将其返回,否则返回nullptr
 * @param {PageId} page_id 需要获取的页的PageId
 */
Page* BufferPoolManager::fetch_page(PageId page_id) {
    //Todo:
    // 1.     从page_table_中搜寻目标页
    // 1.1    若目标页有被page_table_记录,则将其所在frame固定(pin),并返回目标页。
    // 1.2    否则,尝试调用find_victim_page获得一个可用的frame,若失败则返回nullptr
    // 2.     若获得的可用frame存储的为dirty page,则须调用updata_page将page写回到磁盘
    // 3.     调用disk_manager_的read_page读取目标页到frame
    // 4.     固定目标页,更新pin_count_
    // 5.     返回目标页
    std::scoped_lock lock(latch_);  // 上互斥锁,确保线程安全

    // 已在缓冲池中
    if (auto it = page_table_.find(page_id); it != page_table_.end()) {// 如果在page_table_中找到了page_id
        frame_id_t frame_id = it->second;// 获取frame_id
        Page* page = &pages_[frame_id];// 获取page
        replacer_->pin(frame_id);  // 固定页面防止被淘汰
        page->pin_count_++;// 固定页面计数+1
        return page;// 返回page
    }

    // 需要载入新页面
    frame_id_t victim_fid;
    if (!find_victim_page(&victim_fid)) return nullptr;

    Page* victim_page = &pages_[victim_fid];
    if (victim_page->is_dirty_) {
        disk_manager_->write_page(victim_page->id_.fd, victim_page->id_.page_no, victim_page->data_, PAGE_SIZE);
        victim_page->is_dirty_ = false;
    }

    // 更新页表
    page_table_.erase(victim_page->id_);
    page_table_[page_id] = victim_fid;

    // 读取磁盘数据
    disk_manager_->read_page(page_id.fd, page_id.page_no, victim_page->data_, PAGE_SIZE);

    // 初始化页面元数据
    victim_page->id_ = page_id;
    victim_page->pin_count_ = 1;
    replacer_->pin(victim_fid);

    return victim_page;
}

/**
 * @description: 取消固定pin_count>0的在缓冲池中的page
 * @return {bool} 如果目标页的pin_count<=0则返回false,否则返回true
 * @param {PageId} page_id 目标page的page_id
 * @param {bool} is_dirty 若目标page应该被标记为dirty则为true,否则为false
 */
bool BufferPoolManager::unpin_page(PageId page_id, bool is_dirty) {
    // Todo:
    // 0. lock latch
    // 1. 尝试在page_table_中搜寻page_id对应的页P
    // 1.1 P在页表中不存在 return false
    // 1.2 P在页表中存在,获取其pin_count_
    // 2.1 若pin_count_已经等于0,则返回false
    // 2.2 若pin_count_大于0,则pin_count_自减一
    // 2.2.1 若自减后等于0,则调用replacer_的Unpin
    // 3 根据参数is_dirty,更改P的is_dirty_
    std::scoped_lock lock(latch_);

    auto it = page_table_.find(page_id);
    if (it == page_table_.end()) return false;

    frame_id_t frame_id = it->second;
    Page* page = &pages_[frame_id];

    if (page->pin_count_ <= 0) return false;

    if (--page->pin_count_ == 0) {
        replacer_->unpin(frame_id);
    }

    page->is_dirty_ |= is_dirty;  // 保留原有脏标记
    return true;
}

/**
 * @description: 将目标页写回磁盘,不考虑当前页面是否正在被使用
 * @return {bool} 成功则返回true,否则返回false(只有page_table_中没有目标页时)
 * @param {PageId} page_id 目标页的page_id,不能为INVALID_PAGE_ID
 */
bool BufferPoolManager::flush_page(PageId page_id) {
    // Todo:
    // 0. lock latch
    // 1. 查找页表,尝试获取目标页P
    // 1.1 目标页P没有被page_table_记录 ,返回false
    // 2. 无论P是否为脏都将其写回磁盘。
    // 3. 更新P的is_dirty_

    std::scoped_lock lock(latch_);

    auto it = page_table_.find(page_id);
    if (it == page_table_.end()) return false;

    Page* page = &pages_[it->second];
    disk_manager_->write_page(page_id.fd, page_id.page_no, page->data_, PAGE_SIZE);
    page->is_dirty_ = false;
    return true;
}

/**
 * @description: 创建一个新的page,即从磁盘中移动一个新建的空page到缓冲池某个位置。
 * @return {Page*} 返回新创建的page,若创建失败则返回nullptr
 * @param {PageId*} page_id 当成功创建一个新的page时存储其page_id
 */
Page* BufferPoolManager::new_page(PageId* page_id) {
    // 1.   获得一个可用的frame,若无法获得则返回nullptr
    // 2.   在fd对应的文件分配一个新的page_id
    // 3.   将frame的数据写回磁盘
    // 4.   固定frame,更新pin_count_
    // 5.   返回获得的page
    std::scoped_lock lock(latch_);

    frame_id_t frame_id;
    if (!find_victim_page(&frame_id)) return nullptr;

    // 分配新页号
    page_id->page_no = disk_manager_->allocate_page(page_id->fd);

    Page* page = &pages_[frame_id];
    if (page->is_dirty_) {
        disk_manager_->write_page(page->id_.fd, page->id_.page_no, page->data_, PAGE_SIZE);
    }

    // 更新元数据
    page_table_.erase(page->id_);
    page->id_ = *page_id;
    page->is_dirty_ = false;
    page->pin_count_ = 1;
    page_table_[*page_id] = frame_id;

    disk_manager_->write_page(page_id->fd, page_id->page_no, page->data_, PAGE_SIZE);

    replacer_->pin(frame_id);
    return page;
}

/**
 * @description: 从buffer_pool删除目标页
 * @return {bool} 如果目标页不存在于buffer_pool或者成功被删除则返回true,若其存在于buffer_pool但无法删除则返回false
 * @param {PageId} page_id 目标页
 */
bool BufferPoolManager::delete_page(PageId page_id) {
    // 1.   在page_table_中查找目标页,若不存在返回true
    // 2.   若目标页的pin_count不为0,则返回false
    // 3.   将目标页数据写回磁盘,从页表中删除目标页,重置其元数据,将其加入free_list_,返回true

    std::scoped_lock lock(latch_);  // 确保线程安全

    // 1. 检查页是否存在
    auto it = page_table_.find(page_id);
    if (it == page_table_.end()) {
        return true;  // 页面不存在,视为删除成功
    }

    // 2. 获取页面元数据
    frame_id_t frame_id = it->second;
    Page* page = &pages_[frame_id];

    // 3. 检查页面是否被占用
    if (page->pin_count_ > 0) {
        return false;  // 页面仍在使用中,无法删除
    }

    // 4. 处理脏页
    if (page->is_dirty_) {
        disk_manager_->write_page(page->id_.fd, page->id_.page_no, page->data_, PAGE_SIZE);
        page->is_dirty_ = false;
    }

    // 5. 清除页表映射
    page_table_.erase(it);

    // 6. 重置页面状态
    page->id_.fd = INVALID_FRAME_ID;  // 标记为无效文件
    page->id_.page_no = INVALID_PAGE_ID;
    page->pin_count_ = 0;
    page->is_dirty_ = false;
    page->reset_memory();  // 清空内存数据(如有必要)

    // 7. 回收frame资源
    free_list_.push_back(frame_id);

    return true;
}

/**
 * @description: 将buffer_pool中的所有页写回到磁盘
 * @param {int} fd 文件句柄
 */
void BufferPoolManager::flush_all_pages(int fd) {
    std::scoped_lock lock(latch_);  // 全局锁保护

    // 1. 遍历页表项
    for (auto it = page_table_.begin(); it != page_table_.end();) {
        PageId curr_page_id = it->first;
        frame_id_t frame_id = it->second;

        // 2. 仅处理目标文件描述符的页
        if (curr_page_id.fd == fd) {
            Page* page = &pages_[frame_id];

            // 3. 强制写回磁盘(无论是否为脏)
            disk_manager_->write_page(fd, curr_page_id.page_no, page->data_, PAGE_SIZE);
            page->is_dirty_ = false;  // 清除脏标记

            // 4. 维护迭代器(若需要删除页可在此扩展)
            ++it;
        } else {
            ++it;
        }
    }
}

代码测试

cd build
make buffer_pool_manager_test
./bin/buffer_pool_manager_test

任务2 记录管理器

源码

rm_file_handle.cpp

/* Copyright (c) 2023 Renmin University of China
RMDB is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
        http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details. */

#include "rm_file_handle.h"

/**
 * @description: 获取当前表中记录号为rid的记录
 * @param {Rid&} rid 记录号,指定记录的位置
 * @param {Context*} context
 * @return {unique_ptr<RmRecord>} rid对应的记录对象指针
 */
std::unique_ptr<RmRecord> RmFileHandle::get_record(const Rid& rid, Context* context) const {
    // Todo:
    // 1. 获取指定记录所在的page handle
    // 2. 初始化一个指向RmRecord的指针(赋值其内部的data和size)

    RmPageHandle page_handle = fetch_page_handle(rid.page_no);//获取指定记录所在的page handle
    char* data = page_handle.get_slot(rid.slot_no);//获取指定记录的数据
    int size = page_handle.file_hdr->record_size;//获取指定记录的大小
    return std::make_unique<RmRecord>(size,data);//初始化一个指向RmRecord的指针(赋值其内部的data和size)
}

/**
 * @description: 在当前表中插入一条记录,不指定插入位置
 * @param {char*} buf 要插入的记录的数据
 * @param {Context*} context
 * @return {Rid} 插入的记录的记录号(位置)
 */
Rid RmFileHandle::insert_record(char* buf, Context* context) {
    // Todo:
    // 1. 获取当前未满的page handle
    // 2. 在page handle中找到空闲slot位置
    // 3. 将buf复制到空闲slot位置
    // 4. 更新page_handle.page_hdr中的数据结构
    // 注意考虑插入一条记录后页面已满的情况,需要更新file_hdr_.first_free_page_no

    RmPageHandle page_handle = create_page_handle();//获取当前未满的page handle
    auto slot_no = Bitmap::first_bit(false,page_handle.bitmap,file_hdr_.num_records_per_page);
    Bitmap::set(page_handle.bitmap, slot_no);
    // 如果该页面被记满
    if (++page_handle.page_hdr->num_records == file_hdr_.num_records_per_page) {
        file_hdr_.first_free_page_no = page_handle.page_hdr->next_free_page_no;// 更改文件的新
    }
    memcpy(page_handle.get_slot(slot_no), buf, file_hdr_.record_size);
    Rid rid{page_handle.page->get_page_id().page_no, slot_no};
    buffer_pool_manager_->unpin_page(page_handle.page->get_page_id(), true);
    return rid;
}

/**
 * @description: 在当前表中的指定位置插入一条记录
 * @param {Rid&} rid 要插入记录的位置
 * @param {char*} buf 要插入记录的数据
 */
void RmFileHandle::insert_record(const Rid& rid, char* buf) {
}

/**
 * @description: 删除记录文件中记录号为rid的记录
 * @param {Rid&} rid 要删除的记录的记录号(位置)
 * @param {Context*} context
 */
void RmFileHandle::delete_record(const Rid& rid, Context* context) {
    // Todo:
    // 1. 获取指定记录所在的page handle
    // 2. 更新page_handle.page_hdr中的数据结构
    // 注意考虑删除一条记录后页面未满的情况,需要调用release_page_handle()

    //获取指定记录所在的page handle
    RmPageHandle page_handle = fetch_page_handle(rid.page_no);

    //进行删除操作
    if (!Bitmap::is_set(page_handle.bitmap,rid.slot_no)){
        throw RecordNotFoundError(rid.page_no,rid.slot_no);
    }
    Bitmap::reset(page_handle.bitmap,rid.slot_no);
    page_handle.page_hdr->num_records--;//更新page_handle.page_hdr中的数据结构
    if (page_handle.page_hdr->num_records == page_handle.file_hdr->num_records_per_page - 1) {//如果页面未满
        release_page_handle(page_handle);//调用release_page_handle()
    }
    buffer_pool_manager_->unpin_page(page_handle.page->get_page_id(),true);
}


/**
 * @description: 更新记录文件中记录号为rid的记录
 * @param {Rid&} rid 要更新的记录的记录号(位置)
 * @param {char*} buf 新记录的数据
 * @param {Context*} context
 */
void RmFileHandle::update_record(const Rid& rid, char* buf, Context* context) {
    // Todo:
    // 1. 获取指定记录所在的page handle
    // 2. 更新记录

    //获取指定记录所在的page handle
    RmPageHandle page_handle = fetch_page_handle(rid.page_no);
    if (!Bitmap::is_set(page_handle.bitmap,rid.slot_no)){
        throw RecordNotFoundError(rid.page_no,rid.slot_no);
    }
    //更新记录
    memcpy(page_handle.get_slot(rid.slot_no),buf,file_hdr_.record_size);
    buffer_pool_manager_->unpin_page(page_handle.page->get_page_id(),true);
}

/**
 * 以下函数为辅助函数,仅提供参考,可以选择完成如下函数,也可以删除如下函数,在单元测试中不涉及如下函数接口的直接调用
*/
/**
 * @description: 获取指定页面的页面句柄
 * @param {int} page_no 页面号
 * @return {RmPageHandle} 指定页面的句柄
 */
RmPageHandle RmFileHandle::fetch_page_handle(int page_no) const {
    // Todo:
    // 使用缓冲池获取指定页面,并生成page_handle返回给上层
    // if page_no is invalid, throw PageNotExistError exception

    //判断page_no是否处于正常的上下限区间
    if (page_no < 0 || page_no >= file_hdr_.num_pages) {// 如果page_no<0或者page_no>=文件页面总量
        // 那么该页面一定不存在
        throw PageNotExistError(disk_manager_->get_file_name(fd_),page_no);// 使用fd获取文件名并抛出页面不存在异常
    }
    // PageId应当具有两个信息:文件句柄和页面序号
    // 因为想要定位一个Page,当然要先找到他所处的文件地址,再定位其在文件中的页面序号
    // 文件句柄能够定位文件地址(还可以使用disk_manager找到fd_对应的文件路径),而页面序号可以在文件中定位页面
    PageId page_id = PageId{this->fd_, page_no};// 使用文件句柄和页面序号创建一个page id
    Page* page = buffer_pool_manager_->fetch_page(page_id);// 使用缓冲池获取指定页面
    if (page == nullptr){// 如果取出的页面指针是空指针
        // 说明页面不存在
        throw PageNotExistError(disk_manager_->get_file_name(fd_),page_no);// 使用fd获取文件名并抛出对应异常信息
    }
    return RmPageHandle(&file_hdr_, page);// 返回page_handle
}

/**
 * @description: 创建一个新的page handle
 * @return {RmPageHandle} 新的PageHandle
 */
RmPageHandle RmFileHandle::create_new_page_handle() {
    // Todo:
    // 1.使用缓冲池来创建一个新page
    // 2.更新page handle中的相关信息
    // 3.更新file_hdr_

    //创建新page
    PageId* page_id_p = new PageId();//创建一个新的page id
    page_id_p -> fd = fd_;
    Page* page = buffer_pool_manager_->new_page(page_id_p);//使用缓冲池来创建一个新page,并将pageID赋予page_id变量
    if (page == nullptr)// 如果创建新page失败,就抛出一个page不存在的异常
    {
        // 其中包含fd_的文件名和pageid中的序号信息
        throw PageNotExistError(disk_manager_->get_file_name(fd_),page_id_p->page_no);
    }

    //更新page handle中的相关信息
    RmPageHandle page_handle(&file_hdr_, page);//创建一个新的page handle
    page_handle.page_hdr->num_records = 0;// 初始化新PageHandle的记录数
    page_handle.page_hdr->next_free_page_no = RM_NO_PAGE;//将下一个空页设置为null
    Bitmap::init(page_handle.bitmap,file_hdr_.bitmap_size);//将新PageHandle的bitmap初始化

    //更新file_hdr_
    file_hdr_.num_pages++;//页数加一
    if (file_hdr_.first_free_page_no == -1) {//如果没有空闲页
        file_hdr_.first_free_page_no = page_id_p->page_no;//更新first_free_page_no
    }

    //返回page handle
    return page_handle;
}

/**
 * @brief 创建或获取一个空闲的page handle
 *
 * @return RmPageHandle 返回生成的空闲page handle
 * @note pin the page, remember to unpin it outside!
 */
RmPageHandle RmFileHandle::create_page_handle() {
    // Todo:
    // 1. 判断file_hdr_中是否还有空闲页
    //     1.1 没有空闲页:使用缓冲池来创建一个新page;可直接调用create_new_page_handle()
    //     1.2 有空闲页:直接获取第一个空闲页
    // 2. 生成page handle并返回给上层

    // // 或许不必要
    // std::mutex latch_;// 创建一个变量用于互斥锁
    // std::scoped_lock lock{latch_};// 设立互斥锁,以防止并发请求时产生的获取相同空闲页或过多创建空闲页导致的错误
    if (file_hdr_.first_free_page_no == INVALID_PAGE_ID) {//如果没有空闲页
        return create_new_page_handle();//创建一个新的page handle,并返回
    }
    else {
        return fetch_page_handle(file_hdr_.first_free_page_no);//获取第一个空闲页
    }
}

/**
 * @description: 当一个页面从没有空闲空间的状态变为有空闲空间状态时,更新文件头和页头中空闲页面相关的元数据
 */
void RmFileHandle::release_page_handle(RmPageHandle&page_handle) {
    // Todo:
    // 当page从已满变成未满,考虑如何更新:
    // 1. page_handle.page_hdr->next_free_page_no
    // 2. file_hdr_.first_free_page_no

    //更新page_handle.page_hdr中的数据结构
    page_handle.page_hdr->next_free_page_no = file_hdr_.first_free_page_no;
    //更新file_hdr_.first_free_page_no
    file_hdr_.first_free_page_no = page_handle.page->get_page_id().page_no;
}

rm_scan.cpp

/* Copyright (c) 2023 Renmin University of China
RMDB is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
        http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details. */

#include "rm_scan.h"
#include "rm_file_handle.h"

/**
 * @brief 初始化file_handle和rid
 * @param file_handle
 */
RmScan::RmScan(const RmFileHandle *file_handle) : file_handle_(file_handle) {
    // Todo:
    // 初始化file_handle和rid(指向第一个存放了记录的位置)

    rid_ = {RM_FIRST_RECORD_PAGE,-1};
    if (rid_.page_no < file_handle_->file_hdr_.num_pages) {
        next();
        return;
    }
    rid_.page_no=RM_NO_PAGE;
}

/**
 * @brief 找到文件中下一个存放了记录的位置
 */
void RmScan::next() {
    // Todo:
    // 找到文件中下一个存放了记录的非空闲位置,用rid_来指向这个位置

    while (true){
        auto page_handle = file_handle_->fetch_page_handle(rid_.page_no);
        rid_.slot_no = Bitmap::next_bit(
            true,
            page_handle.bitmap,    // 在当前页中
      page_handle.file_hdr->num_records_per_page,
       rid_.slot_no
        );    // 从原先的slot_no开始
        //找到下一个非空闲位置的slot_no
        if (rid_.slot_no == page_handle.file_hdr->num_records_per_page){// 如果返回的slot_no超过了范围
            // 说明在该页面没有找到非空闲位置
            rid_.page_no++;// 于是换到下一页再找
            rid_.slot_no = -1;
            if (rid_.page_no >= file_handle_->file_hdr_.num_pages){
                break;
            }
        }
        else{// 如果找到了
            return;// 那么应当跳出循环
        }
    }
    rid_.page_no=RM_NO_PAGE;
}

/**
 * @brief ​ 判断是否到达文件末尾
 */
bool RmScan::is_end() const {
    // Todo: 修改返回值

    return rid_.page_no == RM_NO_PAGE;
}

/**
 * @brief RmScan内部存放的rid
 */
Rid RmScan::rid() const {
    return rid_;
}