简单数据库实现——Part5 - 持久存储到磁盘
目前的数据库在终止程序重新启动后会丢失所有记录,所以我们会通过将整个数据库保存到一个文件来保存记录。
我们已经通过将行序列化到页面大小的内存快中,为了增加持久性,我们可以简单的将内存块写入文件并在下次程序启动时读入内存。
为了简化这一过程,我们将进行一个称为寻呼机(pager)的抽象。我们向寻呼机询问页码x,寻呼机返回给我们一块内存。它会首先查看他的缓存(cache),如果缓存中不存在,那么它会从磁盘(disk)中将数据复制到内存中。
寻呼机(pager)访问页面缓存和文件,表对象通过寻呼机请求页面:
+typedef struct {
+ int file_descriptor;
+ uint32_t file_length;
+ void* pages[TABLE_MAX_PAGES];
+} Pager;
+
typedef struct {
- void* pages[TABLE_MAX_PAGES];
+ Pager* pager;
uint32_t num_rows;
} Table;
将 new_table()
重命名为 db_open()
,因为它现在的作用是连接数据库:
- 打开数据库文件
- 初始化寻呼机(pager)数据结构
- 初始化表(table)数据结构
-Table* new_table() {
+Table* db_open(const char* filename) {
+ Pager* pager = pager_open(filename);
+ uint32_t num_rows = pager->file_length / ROW_SIZE;
+
Table* table = malloc(sizeof(Table));
- table->num_rows = 0;
+ table->pager = pager;
+ table->num_rows = num_rows;
return table;
}
db_open()
依次调用 paper_open()
,这将打开数据文件并跟踪其大小。它还将页面缓存初始化为 NULL
。
函数的使用:
open()
函数(APUE 3.3和4.5节):
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int oflag,.../*, mode_t mode */);
/* 返回:若成功为文件描述符若出错为-1 */
pathname
是要打开或创建的文件的名字。oflag
参数可用来说明此函数的多个选择项。
O_RDWR
读、写打开O_CREAT
若此文件不存在则创建它。使用此选择项时,需同时说明第三个参数mode,用其说明该新文件的存取许可权位。S_IWUSR
用户-写S_IRUSR
用户-读
lseek()
函数(APUE 3.6节):
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int filedes, off_t offset, int whence);
/*返回:若成功为新的文件位移若出错为-1 */
对参数offset
的解释与参数whence
的值有关。
- 若
whence
是SEEK_END
,则将该文件的位移量设置为文件长度加offset
,offset
可为正或负。
若lseek
成功执行,则返回新的文件位移量。
+Pager* pager_open(const char* filename) {
+ int fd = open(filename,
+ O_RDWR | // Read/Write mode
+ O_CREAT, // Create file if it does not exist
+ S_IWUSR | // User write permission
+ S_IRUSR // User read permission
+ );
+
+ if (fd == -1) {
+ printf("Unable to open file\n");
+ exit(EXIT_FAILURE);
+ }
+
+ off_t file_length = lseek(fd, 0, SEEK_END);
+
+ Pager* pager = malloc(sizeof(Pager));
+ pager->file_descriptor = fd;
+ pager->file_length = file_length;
+
+ for (uint32_t i = 0; i < TABLE_MAX_PAGES; i++) {
+ pager->pages[i] = NULL;
+ }
+
+ return pager;
+}
我们现在将获取页面移动到它自己的方法 get_page()
中。
void* row_slot(Table* table, uint32_t row_num) {
uint32_t page_num = row_num / ROWS_PER_PAGE;
- void* page = table->pages[page_num];
- if (page == NULL) {
- // Allocate memory only when we try to access page
- page = table->pages[page_num] = malloc(PAGE_SIZE);
- }
+ void* page = get_page(table->pager, page_num);
uint32_t row_offset = row_num % ROWS_PER_PAGE;
uint32_t byte_offset = row_offset * ROW_SIZE;
return page + byte_offset;
}
get_page()
方法可以处理缓存丢失。我们假设页面一个接一个保存在数据库文件中:第0页位于偏移量0,第1页位于偏移量4096,第2页位于偏移量8192,等等。如果请求的页面位于文件的边界之外,我们知道它应该是空的,所以我们只需分配一些内存并返回它。当我们稍后将缓存更新到磁盘时,该页面将被添加到文件中。
+void* get_page(Pager* pager, uint32_t page_num) {
+ if (page_num > TABLE_MAX_PAGES) {
+ printf("Tried to fetch page number out of bounds. %d > %d\n", page_num,
+ TABLE_MAX_PAGES);
+ exit(EXIT_FAILURE);
+ }
+
+ if (pager->pages[page_num] == NULL) {
+ // Cache miss. Allocate memory and load from file.
+ void* page = malloc(PAGE_SIZE);
+ uint32_t num_pages = pager->file_length / PAGE_SIZE;