我们正在尝试更改嵌入式数据库系统SQLite, 使用mmap()而不是通常的read()和write()调用来访问 磁盘上的数据库文件。使用单个大映射表示整个映射 文件。假设文件足够小,我们没有问题 在虚拟内存中为此找到空间。
到目前为止一切顺利。在许多情况下使用mmap()似乎要快一点 比read()和write()。在某些情况下要快得多。
调整映射大小以提交写入事务 扩展数据库文件似乎是个问题。为了延伸 数据库文件,代码可以这样做:
ftruncate(); // extend the database file on disk
munmap(); // unmap the current mapping (it's now too small)
mmap(); // create a new, larger, mapping
然后将新数据复制到新内存映射的末尾。 但是,munmap / mmap是不合需要的,因为它意味着下次每次都是如此 访问数据库文件的页面发生次要页面错误 系统必须在OS页面缓存中搜索正确的帧 与虚拟内存地址关联。换句话说,它会变慢 随后的数据库读取。
在Linux上,我们可以使用非标准的mremap()系统调用 munmap()/ mmap()来调整映射的大小。这似乎避免了 小页面错误。
问题:如何在其他系统上处理,如OSX, 没有mremap()?
目前我们有两个想法。关于每个问题:
1)创建大于数据库文件的映射。然后,延伸时 数据库文件,只需调用ftruncate()来扩展文件 磁盘并继续使用相同的映射。
这将是理想的,似乎在实践中有效。但是,我们是 在手册页中担心这个警告:
“更改基础文件大小的效果 映射在与添加或删除的区域对应的页面上 该文件未指定。“
问题:这是我们应该担心的吗?或者是时代错误 在这一点?
2)扩展数据库文件时,使用mmap()的第一个参数 请求对应于数据库的新页面的映射 文件位于虚拟当前映射之后 记忆。有效地扩展初始映射。如果是系统 不能遵守之后立即放置新映射的请求 第一个,回到munmap / mmap。
在实践中,我们发现OSX非常适合定位 以这种方式映射,所以这个技巧在那里工作。
问题:如果系统确实立即分配第二个映射 在虚拟内存中的第一个之后,它最终是否安全 使用对munmap()的一次大调用来解映射它们吗?
答案 0 :(得分:3)
我认为#2是目前最好的解决方案。除此之外,在64位系统上,您可以在操作系统永远不会为映射选择的地址(例如Linux中的0x6000 0000 0000 0000)中明确创建映射,以避免操作系统无法在第一个映射后立即放置新映射一。
使用单个munmap调用取消映射多个mappinsg总是安全的。如果您愿意,甚至可以取消映射部分映射。
答案 1 :(得分:3)
使用fallocate()而不是ftruncate()(如果可用)。如果没有,只需在O_APPEND模式下打开文件,并通过写入一些零来增加文件。这大大减少了碎片。
使用"大页面"如果可用 - 这大大减少了大映射的开销。
pread()/ pwrite()/ pwritev()/ preadv()具有不那么小的块大小并不是很慢。实际上可以比IO快得多。
使用mmap()时的IO错误只会产生段错误,而不是EIO左右。
大多数SQLite WRITE性能问题集中在良好的事务性使用上(即您应该在COMMIT实际执行时进行调试)。
答案 2 :(得分:1)
2可以工作,但是您不必依赖操作系统恰好有可用的空间,您可以预先保留地址空间,这样固定的映射将始终成功。
例如,要保留1 GB的地址空间。做一个
mmap(NULL, 1U << 30, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
这将保留1 GB的连续地址空间,而无需实际分配任何内存或资源。然后,您可以在此空间上执行将来的映射,它们将成功。因此,将文件映射到返回空间的开头,然后根据需要使用固定标志映射文件的其他部分。 mmap将成功,因为您的地址空间已由您分配和保留。
注意:linux也有MAP_NORESERVE标志,如果您正在分配RAM,这是您希望进行初始映射的行为,但是在我的测试中,它被忽略了,因为PROT_NONE足以表明您尚未分配任何资源