我需要最快的方法来定期将文件与内存同步。
我想我想要的是有一个 mmap 文件,它只能手动同步到磁盘。我不确定如何防止发生任何自动同步。
除非我手动指定,否则无法修改该文件。关键是要有一个检查点文件,它将状态的快照保存在内存中。我想尽可能避免复制,因为这需要相当频繁地调用并且速度很重要。
您在文件映射中写入内存的任何内容都MAP_SHARED
被视为当时已写入文件,就好像您曾经使用过write()
. msync()
从这个意义上说,它完全类似于fsync()
- 它只是确保您已经对文件所做的更改实际上被推送到永久存储中。您无法更改这一点 - 这mmap()
就是定义的工作方式。
通常,安全的方法是将数据的完整一致副本写入临时文件,同步临时文件,然后在先前的检查点文件上自动重命名它。这是确保检查点之间的崩溃不会给您留下不一致的文件的唯一方法。任何复制较少的解决方案都需要更复杂的事务日志样式文件格式,并且对应用程序的其余部分更具侵入性(需要在内存中状态更改的每个位置调用特定的挂钩) .
正如其他受访者所建议的那样,我认为没有一种便携的方式可以在不复制的情况下做你想做的事。如果您希望在可以控制操作系统等的专用环境中执行此操作,您可以在 Linux 下使用 btrfs 文件系统执行此操作。
btrfs 支持一个新reflink()
操作,它本质上是一个写时复制文件系统副本。您可以reflink()
在启动时将您的文件临时保存到临时文件,mmap()
然后将文件临时返回到原始检查点。msync()
reflink()
您可以mmap()
将文件作为写入时复制,这样您在内存中所做的任何更新都不会写回文件,然后当您想要同步时,您可以:
A)创建一个新的内存映射,它不会在写入时复制,并且仅将您修改的页面复制到其中。
或者
B) 使用直接 I/O(块大小对齐大小的读写)打开文件(常规文件打开)并只写入您修改的页面。直接 I/O 会很好而且很快,因为您正在写入整个页面(内存页面大小是磁盘块大小的倍数)并且没有缓冲。这种方法的好处是不使用地址空间,以防万一您mmap()
的文件很大并且没有空间容纳mmap()
另一个大文件。
同步后,您的写入副本mmap()
与您的磁盘文件相同,但内核仍然将您需要同步的页面标记为非共享(与磁盘)。因此,如果存在内存压力,您可以关闭并重新创建mmap()
(仍然是写入时复制),这样内核可以在必要时丢弃您的页面(而不是将它们分页到交换空间)。
当然,您必须跟踪自己修改了哪些页面,因为我想不出您将如何访问操作系统保存该信息的位置。(那不是很方便syscall()
吗?)
- 编辑 -
实际上,请参阅是否可以从用户空间找到 mmap 页面的脏污程度?了解如何查看哪些页面是脏的。
mmap
不能用于此目的。没有办法阻止数据写入磁盘。在实践中,使用mlock()
使内存不可交换可能会产生副作用,即除非您要求将其写入磁盘,否则无法将其写入磁盘,但不能保证。当然,如果另一个进程打开该文件,它将看到缓存在内存中的副本(以及您的最新更改),而不是物理磁盘上的副本。在许多方面,您应该做什么取决于您是尝试与其他进程同步还是只是为了在崩溃或电源故障的情况下安全。
如果您的数据量很小,您可以尝试许多其他方法来原子同步到磁盘。一种方法是将整个数据集存储在一个文件名中并使用该名称创建一个空文件,然后删除旧文件。如果启动时存在 2 个文件(由于极不可能的崩溃时间),请删除旧文件并从新文件恢复。如果您的数据大小小于文件系统块、页面大小或磁盘块,也write()
可能是原子的,但我不知道马上就能保证这种效果。你必须做一些研究。
另一种非常标准的方法,只要您的数据不是太大以至于磁盘上无法容纳 2 个副本:只需使用临时名称创建第二个副本,然后rename()
将其覆盖在旧副本之上。rename()
总是原子的。这可能是最好的方法,除非你有理由不这样做。
我高度怀疑可能不会被任何操作系统利用,但操作系统可能会注意到以下方面的优化:
int fd = open("file", O_RDWR | O_SYNC | O_DIRECT);
size_t length = get_lenght(fd);
uint8_t * map_addr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
...
// This represents all of the changes that could possibly happen before you
// want to update the on disk file.
change_various_data(map_addr);
if (is_time_to_update()) {
write(fd, map_addr, length);
lseek(fd, 0, SEEK_SET);
// you could have just used pwrite here and not seeked
}
操作系统可能利用这一点的原因是,直到您写入特定页面(并且没有其他人这样做),操作系统可能只会使用该位置的实际文件页面作为该页面的交换。
然后,当您写入其中一些页面时,操作系统会在写入时为您的进程复制 这些页面,但仍保留原始文件备份的未写入页面。
然后,在调用操作系统时write
,操作系统会注意到写入在内存和磁盘上都是块对齐的,然后它会注意到一些源内存页面已经与它们正在写入的那些确切的文件系统页面同步,并且只写出已更改的页面。
话虽如此,如果任何操作系统都没有完成这种优化,我不会感到惊讶,而且这种类型的代码最终会非常慢,并且在您调用“write”时会导致大量磁盘写入。如果它被利用,那就太酷了。