小巧. 快速. 可靠.
三者选其二.

内存映射 I/O

SQLite 访问和更新数据库磁盘文件的默认机制是 sqlite3_io_methods VFS 对象的 xRead() 和 xWrite() 方法。这些方法通常实现为“read()”和“write()”系统调用,导致操作系统将磁盘内容在内核缓冲区缓存和用户空间之间复制。

3.7.17 版本(2013-05-20)开始,SQLite 可以选择使用内存映射 I/O 和 sqlite3_io_methods 上的新 xFetch() 和 xUnfetch() 方法直接访问磁盘内容。

使用内存映射 I/O 有优点也有缺点。优点包括

  1. 许多操作,尤其是 I/O 密集型操作,速度更快,因为内容不需要在内核空间和用户空间之间复制。

  2. SQLite 库可能需要更少的 RAM,因为它与操作系统页面缓存共享页面,并且并不总是需要它自己的工作页面副本。

但也有缺点

  1. 内存映射文件的 I/O 错误无法被 SQLite 捕获和处理。相反,I/O 错误会导致信号,如果应用程序没有捕获该信号,则会导致程序崩溃。

  2. 操作系统必须具有统一的缓冲区缓存,才能使内存映射 I/O 扩展正常工作,尤其是在两个进程访问同一个数据库文件并且一个进程使用内存映射 I/O 而另一个进程不使用的情况下。并非所有操作系统都具有统一的缓冲区缓存。在一些声称具有统一缓冲区缓存的操作系统中,实现存在错误,会导致数据库损坏。

  3. 性能并不总是随着内存映射 I/O 的使用而提高。实际上,有可能构建性能因使用内存映射 I/O 而降低的测试用例。

  4. Windows 无法截断内存映射文件。因此,在 Windows 上,如果操作(例如 VACUUMauto_vacuum)尝试减小内存映射数据库文件的大小,则大小缩减尝试将静默失败,在数据库文件末尾留下未使用的空间。由于此问题,不会丢失数据,并且在数据库下次增长时,将再次使用未使用的空间。但是,如果 3.7.0 之前的 SQLite 版本在这样的数据库上运行 PRAGMA integrity_check,它将(错误地)报告数据库损坏,原因是文件末尾存在未使用的空间。或者,如果 3.7.0 之前的 SQLite 版本在数据库中仍有未使用的空间时写入数据库,它可能会使该未使用的空间无法访问,并且无法在下次 VACUUM 之前重新使用。

由于潜在的缺点,内存映射 I/O 默认处于禁用状态。要激活内存映射 I/O,请使用 mmap_size pragma 并将 mmap_size 设置为某个较大的数字,通常为 256MB 或更大,具体取决于应用程序可以节省多少地址空间。其余部分将自动执行。在不支持内存映射 I/O 的系统上,PRAGMA mmap_size 语句将成为静默的无操作。

内存映射 I/O 的工作原理

要使用传统的 xRead() 方法读取数据库内容页面,SQLite 首先分配一个页面大小的堆内存块,然后调用 xRead() 方法,该方法会导致数据库页面内容被复制到新分配的堆内存中。这至少涉及整个页面的复制。

但是,如果 SQLite 想要访问数据库文件的页面并且启用了内存映射 I/O,它首先调用 xFetch() 方法。xFetch() 方法要求操作系统返回指向请求页面的指针(如果可能)。如果请求的页面已映射或可以映射到应用程序地址空间,则 xFetch 返回一个指向该页面的指针,供 SQLite 使用,而无需复制任何内容。跳过复制步骤是使内存映射 I/O 更快的关键。

SQLite 不假设 xFetch() 方法会起作用。如果对 xFetch() 的调用返回一个 NULL 指针(表示请求的页面当前未映射到应用程序地址空间),则 SQLite 会静默地回退到使用 xRead()。只有当 xRead() 也失败时才会报告错误。

更新数据库文件时,SQLite 始终在修改页面之前将页面内容的副本复制到堆内存中。这是出于两个原因。首先,数据库的更改不应在事务提交之前对其他进程可见,因此更改必须发生在私有内存中。其次,SQLite 使用只读内存映射来防止应用程序中的错误指针覆盖和损坏数据库文件。

完成所有必要的更改后,使用 xWrite() 将内容移回数据库文件。因此,使用内存映射 I/O 不会显着改变数据库更改的性能。内存映射 I/O 主要有利于查询。

配置内存映射 I/O

“mmap_size”是 SQLite 尝试一次性映射到进程地址空间的数据库文件字节的最大数量。mmap_size 对每个数据库文件单独应用,因此可能使用的进程地址空间的总量是 mmap_size 乘以打开的数据库文件数量。

要激活内存映射 I/O,应用程序可以将 mmap_size 设置为某个较大的值。例如

PRAGMA mmap_size=268435456;

要禁用内存映射 I/O,只需将 mmap_size 设置为零即可

PRAGMA mmap_size=0;

如果 mmap_size 设置为 N,则所有当前实现都会映射数据库文件的头 N 个字节,并对 N 个字节之外的任何内容使用传统的 xRead() 调用。如果数据库文件小于 N 个字节,则整个文件都会被映射。理论上,将来可能会出现新的操作系统接口,映射文件中的其他区域而不是前 N 个字节,但目前还没有这样的实现。

使用“PRAGMA mmap_size”语句为每个数据库文件单独设置 mmap_size。通常的默认 mmap_size 为零,这意味着内存映射 I/O 默认处于禁用状态。但是,可以使用 SQLITE_DEFAULT_MMAP_SIZE 宏在编译时增加默认的 mmap_size,或者使用 sqlite3_config(SQLITE_CONFIG_MMAP_SIZE,...) 接口在启动时增加默认的 mmap_size。

SQLite 还维护着 mmap_size 的硬性上限。尝试使用 PRAGMA mmap_size 将 mmap_size 增加到此硬性上限以上将自动将 mmap_size 限制在硬性上限处。如果硬性上限为零,则内存映射 I/O 不可能。可以使用 SQLITE_MAX_MMAP_SIZE 宏在编译时设置硬性上限。如果将 SQLITE_MAX_MMAP_SIZE 设置为零,则用于实现内存映射 I/O 的代码将从构建中省略。在某些平台(例如 OpenBSD)上,由于缺乏统一的缓冲区缓存,内存映射 I/O 无法正常工作,因此会自动将硬性上限设置为零。

如果在编译时 mmap_size 的硬性上限不为零,它仍然可以使用 sqlite3_config(SQLITE_CONFIG_MMAP_SIZE,X,Y) 接口在启动时被降低或设置为零。X 和 Y 参数必须都是 64 位有符号整数。X 参数是进程的默认 mmap_size,而 Y 是新的硬性上限。硬性上限不能使用 SQLITE_CONFIG_MMAP_SIZE 提高到其编译时设置以上,但可以降低或设置为零。