以原子方式写入文件而不更改 inode(保留硬链接)

cat*_*cat 5 system-calls hard-link inode rename write

X在 Unix 上安全、原子地写入文件的正常方法是:

  1. 将新文件内容写入临时文件Y
  2. rename(2) YX

在两个步骤中,我们似乎除了X“就地”更改之外什么也没做。

它可以防止竞争条件和意外数据丢失(X被破坏但Y不完整或被破坏)。

这样做的缺点(在这种情况下)是它不会写入X就地引用的 inode ;rename(2)makeX引用一个新的 inode 编号。

X链接计数> 1(显式硬链接)的文件时,现在它不像以前一样引用相同的inode,硬链接已损坏。

消除缺点的明显方法是就地写入文件,但这不是原子的,可能会失败,可能导致数据丢失等。

有没有办法像原子一样做到rename(2)但保留硬链接?

也许将Y(临时文件)的 inode 编号更改为与 相同X,并为其X命名?一个 inode 级别的“重命名”。

这将有效地写入XwithY的新内容引用的 inode ,但不会破坏其硬链接属性,并且会保留旧名称。

如果假设的 inode“重命名”是原子的,那么我认为这将是原子的并且可以防止数据丢失/竞争。

spe*_*ras 5

问题

您有一个(大部分)详尽的系统调用列表here

您会注意到没有“替换此 inode 的内容”调用。修改该内容总是意味着:

  1. 打开文件以获取文件描述符。
  2. 可选 查找所需的写入偏移量
  3. 写入文件。
  4. 可选 如果新数据较小,则截断旧数据。

第 4 步可以提前完成。还有一些快捷方式,例如pwrite,它直接在指定的偏移量处写入,结合步骤 #2 和 #3,或分散写入

另一种方法是使用内存映射,但它会变得更糟,因为写入的每个字节都可能独立发送到底层文件(从概念上讲,好像每次写入都是 1 字节write调用)。

? 关键是您可以拥有的最佳场景仍然是 2 个操作:一个write和一个truncate

无论您执行它们的顺序如何,您仍然需要冒另一个过程来弄乱中间的文件并最终导致文件损坏。

解决方案

正解

正如您所指出的,这就是为什么规范方法是创建一个新文件的原因,您知道您是唯一的作者(您甚至可以通过组合O_TMPFILE和来保证这一点linkat),然后以原子方式将旧名称重定向到新文件。

还有其他两种选择,但都以某种方式失败:

强制锁定

它通过设置特殊的标志组合来拒绝其他进程访问文件。听起来像是这项工作的工具,对吧?然而:

  • 它必须在文件系统级别启用(这是挂载时的标志)。
  • 警告:强制锁定的 Linux 实现是不可靠的。

    自 Linux 4.5 起,强制锁定已成为可选功能。这是完全删除此功能的第一步。

这只是合乎逻辑的,因为 Unix 总是避免使用锁。它们容易出错,并且不可能覆盖所有边缘情况并保证不发生死锁。

咨询锁定

它是使用fcntl系统调用设置的。然而,它只是建议性的,大多数程序只是忽略它。

事实上,它只适用于在多个协作的进程之间管理共享文件的锁。

结论

有没有办法像 rename(2) 那样以原子方式进行但保留硬链接?

不。

inode 是低级的,几乎是一个实现细节。很少有 API 承认它们的存在(我相信stat调用系列是唯一的)。

无论您尝试做什么,都可能依赖于滥用 Unix 文件系统的设计或只是对其要求过高。

这可能是XY 问题吗?