30

我们正在尝试更改嵌入式数据库系统 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() 来调整映射大小。这似乎避免了轻微的页面错误。

问题:在没有 mremap() 的其他系统(如 OSX)上应该如何处理?


目前我们有两个想法。还有一个关于每个问题的问题:

1) 创建大于数据库文件的映射。然后,在扩展数据库文件时,只需调用 ftruncate() 来扩展磁盘上的文件并继续使用相同的映射。

这将是理想的,并且似乎在实践中有效。但是,我们担心手册页中的这个警告:

“更改映射的基础文件大小对与文件的添加或删除区域相对应的页面的影响是未指定的。”

问题:这是我们应该担心的事情吗?还是在这一点上不合时宜?

2) 扩展数据库文件时,使用 mmap() 的第一个参数来请求与位于虚拟内存中当前映射之后的数据库文件的新页面相对应的映射。有效地扩展了初始映射。如果系统不能满足在第一次之后立即放置新映射的请求,则回退到 munmap/mmap。

在实践中,我们发现 OSX 以这种方式定位映射非常好,所以这个技巧在那里有效。

问题:如果系统确实在虚拟内存中的第一个映射之后立即分配第二个映射,那么最终使用对 munmap() 的一次大调用最终取消映射它们是否安全?

4

3 回答 3

6

2 will work but you don't have to rely on the OS happening to have space available, you can reserve your address space beforehand so your fixed mmapings will always succeed.

For instance, To reserve one gigabyte of address space. Do a

mmap(NULL, 1U << 30, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);

Which will reserve one gigabyte of continuous address space without actually allocating any memory or resources. You can then perform future mmapings over this space and they will succeed. So mmap the file into the beginning of the space returned, then mmap further sections of the file as needed using the fixed flag. The mmaps will succeed because your address space is already allocated and reserved by you.

Note: linux also has the MAP_NORESERVE flag which is the behavior you would want for the initial mapping if you were allocating RAM, but in my testing it is ignored as PROT_NONE is sufficient to say you don't want any resources allocated yet.

于 2018-07-18T01:34:24.160 回答
3
  1. 我认为#2是目前最好的解决方案。除此之外,在 64 位系统上,您可以在操作系统永远不会为映射选择的地址(例如 Linux 中的 0x6000 0000 0000 0000)显式创建映射,以避免操作系统无法在第一个映射之后立即放置新映射的情况一。

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

于 2013-05-23T05:06:55.200 回答
3
  1. 在可用的情况下使用 fallocate() 而不是 ftruncate()。如果没有,只需在 O_APPEND 模式下打开文件并通过写入一些零来增加文件。这大大减少了碎片。

  2. 如果可用,请使用“大页面”——这大大减少了大映射的开销。

  3. 块大小不那么小的 pread()/pwrite()/pwritev()/preadv() 确实并不慢。比实际执行的 IO 快得多。

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

  5. 大多数 SQLite WRITE 性能问题都集中在良好的事务使用上(即您应该在 COMMIT 实际执行时进行调试)。

于 2015-05-12T04:50:31.527 回答