如何可移植地扩展使用mmap()访问的文件

时间:2013-03-28 14:43:20

标签: linux macos mmap

我们正在尝试更改嵌入式数据库系统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()的一次大调用来解映射它们吗?

3 个答案:

答案 0 :(得分:3)

  1. 我认为#2是目前最好的解决方案。除此之外,在64位系统上,您可以在操作系统永远不会为映射选择的地址(例如Linux中的0x6000 0000 0000 0000)中明确创建映射,以避免操作系统无法在第一个映射后立即放置新映射一。

  2. 使用单个munmap调用取消映射多个mappinsg总是安全的。如果您愿意,甚至可以取消映射部分映射。

答案 1 :(得分:3)

  1. 使用fallocate()而不是ftruncate()(如果可用)。如果没有,只需在O_APPEND模式下打开文件,并通过写入一些零来增加文件。这大大减少了碎片。

  2. 使用"大页面"如果可用 - 这大大减少了大映射的开销。

  3. pread()/ pwrite()/ pwritev()/ preadv()具有不那么小的块大小并不是很慢。实际上可以比IO快得多。

  4. 使用mmap()时的IO错误只会产生段错误,而不是EIO左右。

  5. 大多数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足以表明您尚未分配任何资源