SQLite 操作系统接口或“VFS”

、简介

本文介绍了 SQLite OS 可移植层或“VFS”——位于 SQLite 实现堆栈底部的模块,可提供跨操作系统的可移植性。

2. VFS 与 SQLite 其余部分的关系

SQLite 库的内部组织可以看作是右侧显示的模块堆栈。Tokenizer、Parser 和 Code Generator 组件用于处理 SQL 语句并将其转换为虚拟机语言或字节码的可执行程序。粗略地说,这前三层实现 了 sqlite3_prepare_v2()上面三层生成的字节码是prepared statement. 虚拟机模块负责运行SQL语句字节码。B-Tree 模块将数据库文件组织成具有有序键和对数性能的多个键/值存储。Pager 模块负责将数据库文件的页面加载到内存中,用于实施和控制事务,以及创建和维护日志文件以防止崩溃或电源故障后数据库损坏。操作系统接口是一个精简的抽象,它提供了一组通用的例程,用于调整 SQLite 以在不同的操作系统上运行。粗略地说,底部四层实现了 sqlite3_step()

这篇文章是关于底层的。

操作系统接口——也称为“VFS”——使 SQLite 可以跨操作系统移植。每当 SQLite 中的任何其他模块需要与操作系统通信时,它们都会调用 VFS 中的方法。然后,VFS 调用满足请求所需的特定于操作的代码。因此,将 SQLite 移植到新的操作系统只是编写新的操作系统接口层或“VFS”的问题。

3.多个VFS

标准 SQLite 源代码树包含用于 unix 和 windows 的内置 VFS。可以使用 sqlite3_vfs_register()接口在启动时或运行时添加替代 VFS。

可以同时注册多个 VFS。每个 VFS 都有一个唯一的名称。同一进程中的不同数据库连接可以同时使用不同的 VFS。就此而言,如果单个数据库连接使用ATTACH命令打开多个数据库文件,则每个附加的数据库可能使用不同的 VFS。

3.1. 标准 Unix VFS

Unix 构建带有多个内置 VFS。unix 的默认 VFS 称为“unix”,用于大多数应用程序。可能在 unix 中找到的其他 VFS(取决于编译时选项)包括:

  1. unix-dotfile - 使用点文件锁定而不是 POSIX 建议锁。

  2. unix-excl - 获取并持有数据库文件的独占锁,防止其他进程访问数据库。还将 wal-index保存在堆中而不是共享内存中。

  3. unix-none - 所有文件锁定操作都是空操作。

  4. unix-namedsem - 使用命名信号量进行文件锁定。仅限 VXWorks。

各种 unix VFS 的不同之处仅在于它们处理文件锁定的方式——它们彼此共享大部分实现,并且都位于同一个 SQLite 源文件中: os_unix.c请注意,除了“unix”和“unix-excl”之外,各种 unix VFS 都使用不兼容的锁定实现。如果两个进程使用不同的 unix VFS 访问同一个 SQLite 数据库,它们可能看不到彼此的锁,最终可能会相互干扰,从而导致数据库损坏。特别是“unix-none”VFS 根本没有锁定,如果同时被两个或多个数据库连接使用,很容易导致数据库损坏。鼓励程序员仅使用“unix”或“unix-excl”

3.2. 标准 Windows VFS

Windows 构建还带有多个内置 VFS。默认的 Windows VFS 称为“win32”,用于大多数应用程序。Windows 构建中可能发现的其他 VFS 包括:

  1. win32-longpath - 类似于“win32”,除了路径名的长度最多为 65534 字节,而“win32”中的路径名最大为 1040 字节。

  2. win32-none - 所有文件锁定操作都是空操作。

  3. win32-longpath-none - “win32-longpath”和“win32-none”的组合 - 支持长路径名并且所有锁定操作都是空操作。

与 unix 一样,各种 Windows VFS 的大部分代码都是共享的。

3.3. 指定要使用的 VFS

总是有一个 VFS,它是默认的 VFS。在 unix 系统上,“unix”VFS 作为默认值出现,在 windows 上它是“win32”。如果不采取任何其他操作,新的数据库连接将使用默认的 VFS。

默认 VFS 可以通过使用带有第二个参数 1 的sqlite3_vfs_register()接口 注册或重新注册 VFS 来更改。因此,如果(unix)进程想要始终使用“unix-nolock”VFS 代替“ unix”,下面的代码可以工作:

sqlite3_vfs_register(sqlite3_vfs_find("unix-nolock"), 1);

也可以将备用 VFS 指定为 sqlite3_open_v2()函数的第四个参数。例如:

int rc = sqlite3_open_v2("demo.db", &db, SQLITE_OPEN_READWRITE, "unix-nolock");

最后,如果启用了URI 文件名,则可以使用 URI 上的“vfs=”参数指定替代 VFS。此技术适用于sqlite3_open()sqlite3_open16()sqlite3_open_v2(),以及当新数据库被ATTACH连接到现有数据库连接时。例如:

ATTACH 'file:demo2.db?vfs=unix-none' AS demo2;

URI 指定的 VFS 具有最高优先级。之后是指定为sqlite3_open_v2()的第四个参数的 VFS 。如果没有指定 VFS,则使用默认 VFS。

3.4. VFS 垫片

从 SQLite 堆栈的上层来看,每个打开的数据库文件都使用一个 VFS。但在实践中,一个特定的 VFS 可能只是另一个真正工作的 VFS 的薄包装。我们称包装器 VFS 为“垫片”。

shim 的一个简单示例是“vfstrace”VFS。这是一个 VFS(在 test_vfstrace.c 源文件中实现),它将与每个 VFS 方法调用关联的消息写入日志文件,然后将控制权交给另一个 VFS 以完成实际工作。

3.5. 其他示例 VFS

以下是公共 SQLite 源代码树中可用的其他 VFS 实现:

  • appendvfs.c - 此 VFS 允许将 SQLite 数据库附加到其他文件的末尾。例如,这可用于将 SQLite 数据库附加到可执行文件的末尾,以便在运行时可以轻松找到附加的数据库。如果使用 --append 选项启动,命令行 shell将使用此 VFS,并且其 .archive 命令将在给定 --append 标志的情况下使用它。

  • test_demovfs.c - 该文件实现了一个名为“demo”的非常简单的 VFS,它使用 POSIX 函数,例如 open()、read()、write()、fsync()、close()、fsync()、sleep()、time (), 等等。此 VFS 仅适用于 unix 系统。但它并不是要替代 unix 平台上默认使用的标准“unix”VFS。“演示”VFS 故意保持非常简单,以便它可以用作学习辅助工具或用作构建其他 VFS 或将 SQLite 移植到新操作系统的模板。

  • test_quota.c - 该文件实现了一个名为“配额”的垫片,该垫片对数据库文件集合强制执行累积文件大小限制。辅助接口用于定义“配额组”。配额组是一组文件(数据库文件、日志和临时文件),其名称均与GLOB模式匹配。跟踪每个配额组中所有文件的大小总和,如果该总和超过为配额组定义的阈值,则调用回调函数。该回调可以增加阈值或导致超出配额的操作因 SQLITE_FULL错误而失败。此 shim 的用途之一是用于在 Firefox 中对应用程序数据库实施资源限制。

  • test_multiplex.c - 该文件实现了一个垫片,允许数据库文件超过底层文件系统的最大文件大小。这个 shim 为 SQLite 的上六层提供了一个接口,使它看起来像是正在使用非常大的文件,而实际上每个这样的大文件在底层系统上都被拆分成许多较小的文件。例如,此垫片已用于允许数据库在 FAT16 文件系统上增长到大于 2 GB。

  • test_onefile.c - 该文件实现了一个名为“fs”的演示 VFS,展示了如何在缺少文件系统的嵌入式设备上使用 SQLite。内容直接写入底层媒体。从该演示代码派生的 VFS 可由具有有限闪存的小工具使用,以使 SQLite 充当设备上闪存的文件系统。

  • test_journal.c - 该文件实现了 SQLite 测试期间使用的垫片,用于验证数据库和回滚日志是否以正确的顺序写入并在适当的时间“同步”以确保数据库可以从断电中恢复是很困难的随时重置。shim 检查数据库和回滚日志操作的几个不变量,如果违反任何这些不变量,则引发异常。反过来,这些不变量确保数据库始终是可恢复的。使用此 shim 运行大量测试用例可进一步确保 SQLite 数据库不会因意外电源故障或设备重置而损坏。

  • test_vfs.c - 该文件实现了一个可用于模拟文件系统故障的垫片。在测试期间使用此垫片来验证 SQLite 是否对硬件故障或其他错误情况(例如文件系统空间不足)做出正确的响应,这些情况难以在真实系统上进行测试。

在核心 SQLite 源代码库和可用扩展中还有其他 VFS 实现。上面的列表并不意味着详尽无遗,而只是代表可以使用 VFS 接口实现的各种功能。

4. VFS 实现

一个新的 VFS 是通过继承三个对象来实现的:

sqlite3_vfs对象定义了 VFS 的名称和实现操作系统接口的核心方法,例如检查文件是否存在、删除文件、创建文件和打开以及读取和/或写入、将文件名转换为规范文件名形式。sqlite3_vfs对象还包含从操作系统获取随机性、暂停进程(休眠)以及查找当前日期和时间的方法。

sqlite3_file对象表示一个打开的文件sqlite3_vfs的xOpen方法在打开文件时构造了一个sqlite3_file 对象。sqlite3_file文件打开时跟踪文件的状态。

sqlite3_io_methods对象包含用于与打开的文件交互的方法每个sqlite3_file都包含一个指向sqlite3_io_methods对象的指针,该对象适用于它所代表的文件。sqlite3_io_methods对象包含的方法可以执行诸如从文件读取和写入、截断文件、刷新持久存储的任何更改、查找文件大小、锁定和解锁文件以及关闭文件和销毁sqlite3_file对象。

为新的 VFS 编写代码涉及为sqlite3_vfs对象构造一个子类,然后使用对sqlite3_vfs_register()的调用注册该 VFS 对象。VFS 实现还为sqlite3_filesqlite3_io_methods提供了子类,但这些对象没有直接在 SQLite 中注册。相反, sqlite3_file对象是从 sqlite3_vfs 的 xOpen 方法返回的, sqlite3_file对象指向sqlite3_io_methods 对象的一个​​实例