内存映射文件和单块原子写入

15

如果我使用普通IO API读写单个文件,每块写操作都保证是原子性的。也就是说,如果我的写操作仅修改一个块,操作系统保证要么整个块被写入,要么什么都不写。

那么,如何在内存映射文件上实现相同的效果呢?

内存映射文件只是字节数组,所以如果我修改字节数组,操作系统无法知道何时认为写操作“完成”,因此它可能(尽管可能性很小)会在我块写操作的中途交换内存,从而实际上只写了半个块。

我需要一种“进入/退出关键部分”的方法,或者一种将文件页“固定”在内存中的方法,以便在写入时将其锁定。类似这样的方法是否存在?如果是这样,是否可以在常见的POSIX系统和Windows上可移植使用?


你的映射文件有多少个应用程序正在交互? - Justin
只有一个进程,即数据库服务器。 - Martin Probst
2个回答

5

保持日志的技术似乎是唯一的方法。我不知道多个应用程序如何写入同一个文件。Cassandra项目有一篇好文章介绍如何使用日志实现性能优化。关键是确保日志仅记录积极的操作(我的第一种方法是将每个写操作的前图像写入日志,以便回滚,但这变得过于复杂)。

因此,基本上你的内存映射文件在头部有一个transactionId,如果头部适合一个块,你就知道它不会被损坏,尽管许多人似乎会用校验和写两次:[header[cksum]] [header[cksum]]。如果第一个校验和失败,则使用第二个。

日志看起来像这样:

[beginTxn[txnid]] [offset, length, data...] [commitTxn[txnid]]

您只需不断添加日志记录,直到其变得太大,然后在某个时刻进行翻转。启动程序时,您检查文件的事务ID是否为日志的最后一个事务ID--如果不是,则回放日志中的所有事务以同步。


是的,记录是正确的方法,我知道那些算法。但问题在于,即使使用日志,您也必须确保数据文件的单个页面仅在完整写入时才被写入,否则会出现“半写”页面的风险,并且无法检测它是否已损坏。这就是为什么我正在寻找一种在映射文件中进行原子写入页面的方法。 - Martin Probst
1
为什么这个不起作用:partialWrite = (file.transaction-id < journal.transaction-id)。因为你只在最后更新文件的transaction-id(一旦页面已经更新)。同时,要实现完美的耐久性也很困难(请参见http://www.h2database.com/html/advanced.html#durability_problems)。 - Justin
1
@MartinProbst:你根本无法对页面进行原子写入。我相信这对于Windows内核来说是一种基本的异步操作。你可能需要查看FlushFileBuffers Win API函数。 - Noldorin
非常感谢您提供的信息丰富的答案!双写方法非常棒。您能否提供使用此方法的代码参考?至于“如果您的标题适合一个块,那么您就知道它不会被损坏”,我不明白为什么会这样,因为在将数据复制到mmap()内存时,进程可能会在中途崩溃。您能否详细说明一下?再次感谢! - wenjun.yan

0
如果我使用普通IO API读写单个文件,则每块基础上的写入都是保证原子性的。也就是说,如果我的写入仅修改了一个块,则操作系统保证要么整个块被写入,要么根本不写入。
一般情况下,操作系统不会保证使用“普通IO API”进行的“块写入”具有原子性:
  • 块更多地是文件系统概念 - 文件系统的块大小实际上可能映射到多个磁盘扇区...
  • 假设您指的是扇区,那么您怎么知道您的写入仅映射到一个扇区?没有任何东西表明I/O在经过文件系统的间接后与扇区对齐。
  • 没有任何东西表明您的磁盘必须实现扇区原子性。 “真正的磁盘”通常会这样做,但这不是强制性的或保证的属性。不幸的是,除非它是NVMe磁盘并且您可以访问原始设备,否则您的程序无法“检查”此属性,或者您正在向原始设备发送具有原子性保证的原始命令。
此外,通常你更关心在多个扇区中的持久性(例如,如果发生断电故障,我之前发送的数据是否一定在稳定存储上?)。如果存在任何缓冲,除非你使用另一个命令进行首次检查/使用请求缓存绕过并且这些标志实际被遵守的标志打开文件/设备,否则你的写入可能仍然只存在于 RAM/磁盘缓存中。

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