Linux内核黑客的SQLite简介

2020-11-21 22:53:59

这是针对Linux内核黑客(尤其是那些在Linux文件系统上工作的黑客)的SQLite的简介。

SQLite不是独立的进程或线程。 SQLite是一个子例程。 SQLite嵌入在应用程序中,并使用相同的堆和堆栈。

由于不涉及IPC,因此SQLite具有低延迟。使用SQLite读取或写入许多小Blob比从磁盘上的单独文件读取/写入这些Blob更快。 [1] [2]

可能会不时出现一个额外的暂态日志文件,以帮助实现在崩溃和电源故障时至关重要的事务。

数据库大小范围从512字节到140,735,340,740,610字节。大多数数据库的大小从几兆字节到几千兆字节,尽管已知在生产中使用的是TB级的SQLite数据库。

数据库文件格式稳定,定义明确,文档齐全且跨平台。 SQLite数据库通常用作通过互联网发送结构化内容的存储容器。

通常在数据中心中可以找到其他数据库引擎。 SQLite在网络边缘更为常见。

由于网络边缘是如此广泛,因此有大量正在使用的SQLite数据库-可能超过一万亿(1e12)。

典型的Android手机具有数百个SQLite数据库,每天执行超过5 GB的数据库I / O。

SQLite可以在任何环境下不受监督地工作。SQLite无法选择特定的文件系统类型或specificmount选项。没有可用的配置文件来告诉SQLite它正在运行的系统的功能。 SQLite需要在具有DOS文件系统的USB记忆棒上工作良好,再到具有电池备份和最新文件系统的企业级SSD,以及介于两者之间的所有内容。

为了帮助它最有效地工作,SQLite需要知道它所运行的环境的属性。而且由于缺少配置文件,SQLite需要在运行时自行发现这些属性.SQLite想了解的系统某些属性包括:

如果在用新数据扩展文件的同时发生断电,重新启动后是否可以保证文件包含有效数据,或者文件的扩展区可能包含全零或全1或任意内容?换句话说,文件数据是否总是在文件大小之前提交到磁盘?

如果在文件截断的大约同一时间发生断电,重新启动后文件的截断区域是否可能包含任意数据?换句话说,在释放数据段之前,是否保证文件大小已提交到磁盘?

可以访问该文件的所有进程是否可以将文件的mmap()用于共享内存。 (对于网络文件系统这是错误的。因此,问题与“网络文件系统上的文件是否存在?”大致相同)

如果在断电的同时在文件的一个或两个字节上进行写操作,重新启动后是否保证文件的其他字节不变?还是同一扇区内的其他一些字节也可能被修改?

创建新文件时,成功写入文件,并成功执行fdatasync(),是否还需要在包含电源的目录中打开并对其进行fsync()或确保断电重启后该文件仍然存在?

自打开文件以来,文件是否已取消链接或重命名? (SQLite现在通过记住从fstat()获得的设备和inode编号,并将它们与后续stat()调用的结果与原始文件名进行比较,来完成此操作。是否有更有效的方法?

是否有可能(或有帮助)告诉文件系统特定文件的内容不需要重新启动就可以生存?

是否有可能(或有帮助)告诉文件系统特定文件可以在重新启动后取消链接?

是否有可能(或有帮助)告知文件系统有关当前未使用的数据库文件部分,并且文件系统可以归零而不损害数据库?

与所有关系数据库一样,即使应用程序崩溃(例如SIGKILL)或发生意外断电,SQLite也需要实现ACID(原子,一致性,隔离和持久)的事务。这可以通过以下三种方式之一来实现:

回滚日志方法是最慢的方法,但也最适用于任何文件系统。因此,默认设置为“回滚”。“预写日志”(WAL模式)更快,但仅适用于以下系统:(1)一次只有单个进程访问数据库,或者(2)可以通过mmap()编辑文件用作共享内存。

第三个选项是最快的,但目前仅Linux上的F2FS文件系统支持。

每个数据库文件都处于回滚模式或WAL模式,所有进程均按其定义的模式访问数据库文件。更改数据库的模式需要对数据库的独占访问权限.F2FS原子写入功能(选项3)为回滚模式的扩展(选项1)。

一个SQLite数据库是一个或多个“页面”的序列。同一数据库文件中的所有页面都具有相同的大小。但是对于不同的数据库,页面大小可以是512到65536之间的任何2的幂。通常,对数据库的单个更改涉及对多个页面的修改。从根本上讲,这是如下所示:

在与数据库相同的目录中创建回滚日志文件,并具有相同的名称,只是添加了“ -journal”扩展名。

任何要修改的数据库页面的原始内容都会写入回滚日志中。

打开包含回滚日志的目录和该目录的fdatasync(),以确保断电后文件名仍然存在

在数据库文件上调用fdatasync()。如果数据库文件的大小减小,则也调用ftruncate()。

在步骤(1)中,回滚日志和数据库文件保留在同一目录中,以确保它们位于同一卷上,因此在重新引导后不会彼此分离。

在步骤(10),SQLite隐式假定三个提交操作(取消链接,截断或标头覆盖)中的每一个都是原子的。

对数据库文件的所有写操作都是整数页,并且页对齐。

对回滚日志的写入是追加或线性覆盖,除了在步骤10c覆盖标头。

最初打开数据库文件时,请检查是否存在格式正确的回退日志。如果未找到→完成。

将修改后的页面追加到预写日志文件(“ WAL文件”)中。 WAL文件是与数据库位于同一目录中的文件,名称与数据库相同,但附加了“ -wal”。

可以省略步骤3,结果是事务不再持久。换句话说,据报告已提交的事务可能会在电源故障后回滚。只要数据库在重新启动后仍保持格式良好且一致,大多数应用程序都会掉电而丢失最后几笔事务,这很酷。

想要访问数据库的所有进程都将mmap()-ed为单独的“ -shm”文件。 “ -shm”文件包含一个哈希表,用于快速查找先前写入“ -wal”文件中的页面。

假设最后一个进程完全关闭,则最后一个关闭其与数据库的连接的进程将自动运行检查点,然后取消链接WAL文件和“ -shm”文件。如果最后一个访问数据库的进程只是退出()而没有调用sqlite3_close(),或者该进程崩溃或断电,则“ -wal”和“ -shm”文件会留在磁盘上。剩下的“ -wal”和“ -shm”文件将在下一步打开数据库的过程中清除。

等待所有并发阅读器停止使用主数据库中WAL文件中已更改的页面。

屏障-必须先完成对WAL文件的所有写入,然后再进行对数据库文件的任何写入。

将WAL文件中每个页面的最新更改写回到数据库文件中。

页面被排序,以便它们以递增顺序写入。 →这在Linux上有帮助吗?还是我们可以跳过排序并以任意顺序编写页面?

截断WAL文件,否则以其他方式使WAL文件从头开始。

我们已经看到,覆盖现有文件比截断和追加速度更快。总是这样吗?

默认操作是运行导致WAL文件超过1000页的提交的第一个进程运行一个检查点。但是,此行为可以由应用程序更改。可以提高或降低“自动检查点”阈值。或者,系统可以指定一个单独的线程或进程来运行定期检查点。

检查点可能会阻塞到步骤2。或者,如果只完成了一部分工作,然后可以提早返回。非阻塞是默认设置。

F2FS是Linux的日志结构文件系统,具有(有限的)atomicwrite功能。 SQLite能够使用F2FS的原子写入功能来绕过回滚日志。有传闻称,重新格式化为使用F2FS的Android手机的速度明显更快。 F2FS使旧的旧电话感觉像新电话一样。

F2FS_IOC_ABORT_VOLATILE_WRITE(以下称“ F2FS-ROLLBACK”)-回滚事务的所有更改,并将文件描述符恢复到F2FS-BEGIN之前的状态。

F2FS原子写入功能仅对否则将处于回滚模式的数据库有用。原子写入功能对WAL模式数据库没有帮助。

通过调用F2FS-BEGIN开始新事务。如果该ioctl()失败,则回退到使用回滚日志。

将更改直接写入数据库文件。如果任何写入失败,SQLite将通过调用F2FS-ROLLBACK终止事务,并使用回滚日志重新启动事务。

如果应用程序通过发出SQL“ ROLLBACK”语句来请求事务中止,则SQLite通过调用F2FS-ROLLBACK ioctl()来中止原子写入。

通过调用F2FS-COMMIT ioctl()提交事务。如果该ioctl()失败,SQLite将使用传统的回滚日志重试该事务。

在使用F2FS-BEGIN启动了事务的文件描述符上发出的写入,在F2FS-COMMIT之前的任何其他文件描述符上都不得可见。

如果在F2FS-COMMIT之前断电或其他系统崩溃,那么重新启动后文件中所有未提交的更改都不会出现。

如果发出F2FS-BEGIN的文件描述符在F2FS-COMMIT之前关闭,则自上一个F2FS-BEGIN以来,该文件描述符上的所有更改都将被丢弃。

F2F2-BEGIN和SQLite通过取消事务(使用F2FS-ROLLBACK)并使用传统的回滚日志重新启动事务后,内核可以使所有操作失败。

应用程序可以随时中止事务。 SQLite将调用F2FS-ROLLBACK,并期望系统忘记通过最新的F2FS-BEGIN对该文件描述符的写操作所做的任何更改。

发出F2FS-BEGIN的同一文件描述符上的write()调用结果必须对随后的read()调用可见。但是,直到从F2FS-COMMIT之后,这些写入对于从单独的文件描述符进行的read()都必须不可见。

在F2FS-ROLLBACK之后,如果从未发生对最近F2FS-BEGIN的write()调用,则所有read()调用都必须返回与返回的数据相同的数据。

Linux上SQLite的性能已经非常快。但是,可能会通过新的内核接口进一步加以改进。

SQLite使用fsync()或fdatasync()来确保I / O操作完成。但是,如果应用程序在断电后愿意放弃耐久性(“ ACID”中的“ D”),则许多fdatasync()调用可以由假设的fbarrier()代替。这在上面用“障碍”标记的每个位置中将很有用。

对至少两个文件描述符进行序列操作。例如,对回滚日志的写操作必须在对数据库的任何写操作开始之前完成。

当应用程序从SQLite数据库删除内容时,SQLite通常不会覆盖已删除的内容,而只是记住该空间可供重用。这样可以避免不必要的写入。 (例外:有时,应用程序实际上实际上希望覆盖内容以避免法医跟踪,而SQLite可以根据要求支持此内容。例如,Firefox将SQLite置于一种模式,当您清除搜索历史记录时,它将用零覆盖已删除的内容。

SQLite可以告知文件系统有关文件中未使用且不需要保留的区域。

来自指定为未使用区域的read()可能返回全零,全1或任意位,而SQLite不会在意。

对未使用区域的第一个write()将废除该区域的“未使用”状态。