X
在 Unix 上安全、原子地写入文件的正常方法是:
- 将新文件内容写入临时文件
Y
。 rename(2)
Y
到X
在这两个步骤中,我们似乎除了X
“就地”更改之外什么也没做。
它可以防止竞争条件和意外数据丢失(已X
损坏但Y
不完整或已损坏)。
X
这样做的缺点(在本例中)是它不会就地写入引用的 inode ;rename(2)
使X
引用一个新的索引节点号。
当X
一个文件的链接计数> 1(显式硬链接)时,现在它不再像以前那样引用相同的inode,硬链接已损坏。
消除该缺点的明显方法是就地写入文件,但这不是原子的,可能会失败,可能会导致数据丢失等。
是否有某种方法可以像原子一样完成此操作rename(2)
但保留硬链接?
也许可以将 (临时文件) 的 inode 编号更改Y
为与 相同X
,并为其X
命名?inode 级别的“重命名”。
X
这将有效地写入with的新内容引用的 inode Y
,但不会破坏其硬链接属性,并保留旧名称。
如果假设的索引节点“重命名”是原子的,那么我认为这将是原子的并且可以防止数据丢失/竞争。
答案1
问题
您有一个(大部分)详尽的系统调用列表这里。
您会注意到没有“替换此索引节点的内容”调用。修改该内容始终意味着:
第4步可以提前完成。还有一些快捷方式,例如写入,直接在指定的偏移处写入,结合步骤#2和#3,或者散写。
另一种方法是使用内存映射,但情况会变得更糟,因为写入的每个字节都可能独立发送到底层文件(概念上就好像每次写入都是 1 字节write
调用)。
→ 重点是,您可以拥有的最佳场景仍然是 2 个操作: 1write
和 1 truncate
。
无论您以什么顺序执行它们,您仍然面临另一个进程弄乱中间文件并最终导致文件损坏的风险。
解决方案
正常溶液
正如您所指出的,这就是为什么规范的方法是创建一个新文件,您知道您是唯一的编写者(您甚至可以通过组合O_TMPFILE
和来保证这一点linkat
),然后自动将旧名称重定向到新文件。
还有其他两个选项,但是都以某种方式失败:
强制锁定
它通过设置特殊的标志组合来拒绝其他进程的文件访问。听起来像是这项工作的工具,对吧?然而:
- 它必须在文件系统级别启用(它是安装时的一个标志)。
警告:Linux 的强制锁定实现是不可靠的。
从 Linux 4.5 开始,强制锁定已成为可选功能。这是完全删除此功能的第一步。
这是合乎逻辑的,因为 Unix 一直避免使用锁。它们很容易出错,并且不可能覆盖所有边缘情况并保证没有死锁。
咨询锁定
它是使用福康特尔系统调用。然而,它只是建议性的,大多数程序都会忽略它。
事实上,它只适用于管理多个协作进程之间共享文件的锁。
结论
有没有某种方法可以像 rename(2) 一样以原子方式执行此操作,但保留硬链接?
不。
索引节点是低级别的,几乎是一个实现细节。很少有 API 承认它们的存在(我相信stat
调用系列是唯一的)。
无论你尝试做什么,都可能依赖于滥用 Unix 文件系统的设计,或者只是对它要求太多。
这可能有点像XY-问题?