如何使用mmap创建一个只能手动同步到磁盘的检查点文件

6

我需要最快的方法定期同步文件和内存。

我认为我想要的是使用 mmap 的文件,只有在手动同步到磁盘时才会同步。我不确定如何防止自动同步发生。

文件只能在我手动指定的时间进行修改。重点是要有一个检查点文件,保存内存中状态的快照。我希望尽可能减少复制次数,因为这将会是相当频繁调用的,速度很重要。

5个回答

4
您可以将文件作为写时复制使用mmap(),这样在内存中进行的任何更新都不会写回文件,然后当您想要同步时,您可以:
A)创建一个新的内存映射,该映射不是写时复制,并仅将您修改的页面复制到其中。
B)以直接I/O(块大小对齐的读写)打开文件(常规文件打开),并仅写入您修改的页面。直接I/O非常快速,因为您正在写入整个页面(内存页面大小是磁盘块大小的倍数),而且没有缓冲区。如果您的mmap()很大并且没有空间将另一个巨大的文件mmap(),则此方法具有不使用地址空间的好处。
同步后,您的写时复制mmap()与磁盘文件相同,但内核仍然将您需要同步的页面标记为非共享(与磁盘不共享)。因此,您可以关闭并重新创建mmap()(仍为写时复制),这样内核就可以在需要时丢弃您的页面(而不是将它们换出到交换空间),如果存在内存压力。
当然,你得自己跟踪修改过的页面,因为我想不出如何获取操作系统保存该信息的位置。(这难道不是一个方便的syscall()吗?)
-- 编辑 --
实际上,参见Can the dirtiness of pages of a mmap be found from userspace?来获取如何查看哪些页面是脏的的想法。

1
你的回答完美地表达了我的想法。关键是脏页标记。你怎么知道它是否脏了? - Matt Joiner

4
在使用MAP_SHARED映射文件时,写入内存的任何内容都被视为在此时写入文件,就像你使用了write()一样。从这个意义上讲,msync()fsync()完全相似-它只是确保你已经对文件进行的更改实际上被推送到永久存储器中。你不能改变这一点-这是mmap()定义的工作方式。
通常,安全的方法是将数据的完整一致副本写入临时文件中,同步临时文件,然后原子性地将其重命名为先前的检查点文件。这是确保在检查点之间发生崩溃时不会留下不一致文件的唯一方法。任何少复制的解决方案都需要更复杂的事务日志样式文件格式,并且对您应用程序的其他部分更具侵入性(需要在每个更改内存状态的地方调用特定的钩子函数)。

2

mmap不能用于此目的。无法防止数据被写入磁盘。在实践中,使用mlock()使内存不可交换可能会有一个副作用,即除非您要求将其写入,否则它不会被写入磁盘,但没有保证。当然,如果另一个进程打开文件,则它将看到缓存在内存中的副本(带有您最新的更改),而不是物理磁盘上的副本。在很多方面,你应该根据你是否想要与其他进程同步或仅为了在崩溃或断电时安全而做出决定。

如果您的数据量较小,您可以尝试许多其他原子同步到磁盘的方法。一种方法是将整个数据集存储在文件名中,并通过该名称创建一个空文件,然后删除旧文件。如果在启动时存在2个文件(由于极少见的崩溃时间),请删除旧文件并从新文件恢复。如果您的数据大小小于文件系统块、页面大小或磁盘块,则write()可能也是原子性的,但我不知道任何保证效果的方法。您需要进行一些研究。

另一种非常标准的方法,只要您的数据不太大,2个副本就不适合放在磁盘上:只需使用临时名称创建第二个副本,然后将其rename()覆盖旧的副本。 rename()始终是原子性的。除非您有理由不以这种方式进行,否则这可能是最佳方法。


不需要同步进程,这更像是备份。如果可能的话,我也想避免进行任何复制。数据大小至少为50MB。 - arsenm

2
正如其他回答者所建议的那样,我认为在不复制文件的情况下实现您想要的功能是没有一种便携式的方法的。如果您想在可以控制操作系统等特殊环境中执行此操作,则可能可以在Linux上使用btrfs文件系统来完成。
btrfs支持一个新的reflink()操作,它本质上是一个写时复制文件系统副本。您可以将文件reflink()到一个临时文件中,在启动时mmap()该临时文件,然后将该临时文件msync()和reflink()回原始文件以进行检查点。

-1

我非常怀疑任何操作系统都可能不会利用这一点,但是对于操作系统来说,注意到以下优化是有可能的:

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”时导致大量磁盘写入。如果能够利用这种方式就太棒了。


@Matt Joiner:你的“wtf”是什么?这基本上与James Caccese的答案中你说准确表达了你的想法的B部分相同,只是让操作系统内核(它有访问脏位的权限)来决定是否写入每个页面。除了我不知道是否有任何Unix实际上会这样做。然而,这是相同的想法。 - nategoose
@nategoose,你有没有进行过任何测试来证明任何操作系统都可以利用私有mmap区域与pwrite后的底层文件相同这一事实,从而仅保留一个内存副本以供私有mmap和文件缓存使用?我不认为当前的操作系统能够进行此优化。 - Kan Li

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接