【问题标题】:Race condition during file write文件写入期间的竞争条件
【发布时间】:2022-01-11 18:26:03
【问题描述】:

假设两个不同的进程独立打开同一个文件,因此在打开文件表(系统范围)中有不同的条目。但它们指的是同一个 i-node 条目。

由于文件描述符引用了打开文件表(系统范围)中的不同条目,因此它们可能具有不同的文件偏移量。由于文件偏移量不同,write 期间是否会有竞争条件?而内核又是如何避免的呢?

书籍:Linux 编程接口;页码95;第 5 章(文件 I/O:更多细节);第 5.4 节

【问题讨论】:

  • 你能在这种情况下定义竞争条件吗?您是在谈论内部内核数据结构的一致性,还是从用户空间查看文件内容?

标签: c linux file inode unistd.h


【解决方案1】:

(我假设因为您使用了write(),所以该问题涉及 POSIX 系统。)

每个write() 操作假定是完全原子的,假设是 POSIX 系统(假定使用 write())。

POSIX 7's 2.9.7 Thread Interactions with Regular File Operations:

以下所有函数对于每个函数都应是原子的 其他在 POSIX.1-2017 中指定的效果中运行时 常规文件或符号链接:

chmod()
chown()
close()
creat()
dup2()
fchmod()
fchmodat()
fchown()
fchownat()
fcntl()
fstat()
fstatat()
ftruncate()
lchown()
link()
linkat()
lseek()
lstat()
open()
openat()
pread()
read()
readlink()
readlinkat()
readv()
pwrite()
rename()
renameat()
stat()
symlink()
symlinkat()
truncate()
unlink()
unlinkat()
utime()
utimensat()
utimes()
write()
writev()

如果两个线程分别调用其中一个函数,则每次调用都应 要么看到另一个调用的所有指定效果,要么没有 他们。 close() 函数的要求也应适用 每当文件描述符成功关闭时,不管是什么原因(对于 例如,由于调用 close()、调用 dup2() 或 进程终止)。

但要特别注意specification for write()(我的粗体字):

write() 函数应尝试写入 nbyte 字节 ...

POSIX 说write() 对文件的调用应该是原子的。 POSIX 并没有说write() 调用将是完成Here's a Linux bug report 信号中断了部分完成的write()。注意解释:

现在,就规范(POSIX,SUS,...)而言,这是完全有效的行为(如果我遗漏了什么,请纠正我)。所以我会说这个程序是不正确的。但是OTOH我同意这在a50527b1之前是不可能的,我们不想破坏用户空间。我不想恢复那个提交,因为它允许我们中断执行大量写入的进程(尤其是当出现问题时),但是如果你向我们解释为什么这种行为对你来说是一个问题,那么我想我将不得不恢复它。

这不过是承认,POSIX 要求 write() 调用是原子的(如果不完整的话),并提供恢复到早期行为的提议,其中 write() 调用显然在同样的情况下也是完整的。

但请注意,有很多文件系统不符合 POSIX 标准。

【讨论】:

    【解决方案2】:

    由于文件描述符引用打开文件表(系统范围)中的不同条目,因此它们可能具有不同的文件偏移量。由于文件偏移量不同,写入过程中是否会出现竞争条件?

    Linux 中的任何 write() 都可以返回一个短计数,例如由于信号被传递到用户空间处理程序。为简单起见,我们忽略它,只考虑成功写入的数据会发生什么。

    有两种情况:

    1. 写入的区域不重叠。

      (例如,一个进程从偏移量 23 开始写入 100 个字节,另一个进程从偏移量 200 开始写入 50 个字节。)

      在这种情况下没有竞争条件。

    2. 写入的区域重叠。

      (例如,一个进程从偏移量 50 开始写入 100 个字节,另一个进程从偏移量 70 开始写入 10 个字节。)

      存在竞争条件。无法预测(没有咨询锁等)数据更新的顺序。

      取决于目标文件系统,并且如果写入足够大(以便可以观察到分页效果),在 Linux 中的某些文件系统上,这两个写入甚至可能“混合”(以页面大小的块)不止一个硬件线程,尽管 POSIX 说这不应该发生。

    通常,写入会通过 Linux 页面缓存。其中一个进程可能已经使用O_DIRECT | O_SYNC 打开了文件,绕过了页面缓存。在这种情况下,可能会出现许多额外的极端情况。具体来说,即使您使用共享时钟源,并且可以显示在进行直接写入调用之前正常/页面缓存写入已完成,页面缓存写入仍有可能覆盖直接写入内容。

    内核如何避免它?

    它没有。为什么要呢? POSIX 说每次写入都是原子的,但是没有实际的方法可以避免仅依赖于它的竞争条件(并获得一致和预期的结果)。

    用户空间程序至少有四种不同的方法来避免此类竞争:

    1. 建议文件使用flock() 接口锁定整个打开的文件。

    2. 建议文件使用lockf() 接口锁定整个打开的文件。在 Linux 中,这些只是在整个文件上放置/删除 fcntl() 咨询锁的简写。

    3. 使用fcntl() 接口对文件进行咨询记录锁定。只要文件服务器配置为支持文件锁定,这甚至可以跨共享卷使用。

    4. 使用fcntl() 接口获取打开文件的独占租约。

    咨询文件锁就像路灯:它们旨在用于协作流程,以便轻松确定谁可以在何时去。但是,它们不会阻止任何其他进程实际忽略“锁定”并访问文件。

    文件租约是一种机制,其中一个或多个进程可以同时获得对同一个文件的读租约,但只有一个进程可以获得写租约,并且只有当该进程是唯一打开文件的进程时.当被授予时,写租约(或独占租约)意味着如果任何其他进程试图打开同一个文件,租约所有者进程会收到一个信号通知(您可以使用 fcntl() 接口进行控制),并配置一个放弃租约的时间(通常为 45 秒;参见 man 5 proc/proc/sys/fs/lease-break-time,以秒为单位)。打开器在内核中被阻塞,直到租约被降级或租约中断时间过去,在这种情况下内核会中断租约。 这允许租赁持有人将开业推迟一小段时间。 但是,承租人不能阻止开口,例如用诱饵文件替换文件; opener 已经持有 inode,租约中断时间只是清理工作的宽限期。

    从技术上讲,第五种方法是强制文件锁定,但除了内核使用 wrt。执行的二进制文件,它们没有被使用,而且在 Linux 中实际上是错误的。在 Linux 中,只有当内核将 inode 作为二进制文件执行时,inode 才会被锁定以防修改。 (您仍然可以重命名或删除原始文件,然后创建一个新文件,以便任何后续 exec 将执行修改后的/新数据。尝试修改作为二进制文件执行的文件将失败并出现错误 EBUSY .)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-07-22
      • 2012-04-17
      • 1970-01-01
      • 1970-01-01
      • 2017-03-20
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多