内存映射 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. SQLite 无法捕获和处理内存映射文件上的 I/O 错误。相反,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 或更大,具体取决于您的应用程序可以腾出多少地址空间。其余的是自动的。PRAGMA mmap_size语句在不支持内存映射 I/O 的系统上将是静默的空操作。

内存映射 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宏或在启动时使用 sqlite3_config ( SQLITE_CONFIG_MMAP_SIZE ,...) 接口增加默认 mmap_size。

SQLite 还在 mmap_size 上维持一个硬上限。尝试将 mmap_size 增加到此硬上限以上(使用 PRAGMA 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增加到其编译时设置之上,但可以减少或归零。