Memory-Mapped I/O
Memory-Mapped 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有优点和缺点。优点包括:
- 许多操作,特别是I/O密集型操作,可能会更快,因为内容确实需要在内核空间和用户空间之间进行复制。
- SQLite库可能需要更少的RAM,因为它与操作系统页面缓存共享页面,并不总是需要它自己的工作页面副本。
但也有缺点是:
- 内存映射文件上的I/O错误不能被SQLite捕获和处理。相反,I/O错误会导致一个信号,如果未被应用程序捕获,则会导致程序崩溃。
- 操作系统必须具有统一缓冲区高速缓存,以便内存映射I/O扩展能够正常工作,特别是在两个进程正在访问同一个数据库文件并且一个进程正在使用内存映射I/O的情况下不是。并非所有操作系统都有统一的缓冲区缓存。在一些声称拥有统一缓冲区高速缓存的操作系统中,实现有问题,并可能导致数据库损坏。
- 性能并不总是随着内存映射I/O而增加。实际上,可以构建测试用例,通过使用内存映射I/O来降低性能。
- Windows无法截断内存映射文件。因此,在Windows上,如果诸如VACUUM或auto_vacuum之类的操作尝试减小内存映射数据库文件的大小,则大小缩减尝试将自动失败,从而在数据库文件末尾留下未使用的空间。由于这个问题没有数据丢失,并且未使用的空间将在数据库下一次增长时再次使用。但是,如果3.7.0之前的SQLite版本在这样的数据库上运行PRAGMA integrity_check,它将(错误地)报告由于末尾未使用的空间而导致的数据库损坏。或者,如果3.7.0之前的SQLite版本在数据库中写入数据库的同时尚未使用空间,则可能会使该未使用的空间无法访问,直到下一个VACUUM之后才能重用。
由于潜在的缺点,默认情况下禁用内存映射I/O。要激活内存映射I/O,请使用mmap_size编译指示并将mmap_size设置为一个大数字,通常为256MB或更大,具体取决于应用程序可以节省多少地址空间。其余的是自动的。对于不支持内存映射I/O的系统,PRAGMA mmap_size语句将成为无提示操作。
How Memory-Mapped I/O Works
要使用传统的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主要是查询的好处。
Configuring Memory-Mapped 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个字节,并使用旧版xRead()调用来处理超出N个字节的任何内容。如果数据库文件小于N个字节,则映射整个文件。在未来,新的操作系统接口理论上可以映射除前N个字节以外的文件区域,但目前没有这种实现。
使用“PRAGMA mmap_size”语句为每个数据库文件单独设置mmap_size。通常的默认mmap_size为零,这意味着内存映射I/O默认是禁用的。但是,默认的mmap_size可以在编译时使用SQLITE_DEFAULT_MMAP_SIZE宏或在启动时使用sqlite3_config(SQLITE_CONFIG_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增加到编译时设置以上,但可以减少或归零。
SQLite is in the Public Domain.