Small. Fast. Reliable.
Choose any three.

内存映射的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而另一个进程正在使用内存映射的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编译指示 并将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个字节,则将映射整个文件。从理论上讲,将来,新的OS接口可以映射文件的前N个字节以外的区域,但目前尚不存在这样的实现。

使用“ PRAGMA mmap_size ”语句为每个数据库文件分别设置mmap_size。通常的默认mmap_size为零,这意味着默认情况下禁用内存映射的I / O。但是,可以在编译时使用SQLITE_DEFAULT_MMAP_SIZE宏或在启动时使用 sqlite3_configSQLITE_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_configSQLITE_CONFIG_MMAP_SIZE,X,Y)接口在启动时将其降低或归零 。X和Y参数都必须是64位有符号整数。X参数是进程的默认mmap_size,Y参数是新的硬上限。使用SQLITE_CONFIG_MMAP_SIZE不能将硬上限提高到其编译时设置之上,但可以将其降低或清零。