为什么在覆盖文件时关闭文件等待同步,而不是在创建时?

JMC*_*JMC 9 cache ext4 files

运行此脚本时:

#!/usr/bin/env python3
f = open("foo", "w")
f.write("1"*10000000000)
f.close()
print("closed")
Run Code Online (Sandbox Code Playgroud)

我可以在我的 Ubuntu 机器上观察到以下过程:

内存充满 10GB。页面缓存填充了 10GB 的脏页面。(/proc/meminfo) 打印“关闭”并且脚本终止。一段时间后,脏页减少。

但是,如果文件“foo”已经存在,close() 会阻塞,直到所有脏页都被写回。

这种行为的原因是什么?

如果文件不存在,这是 strace:

openat(AT_FDCWD, "foo", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 3
fstat(3, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
ioctl(3, TCGETS, 0x7ffd50dc76f0)        = -1 ENOTTY (Inappropriate ioctl for device)
lseek(3, 0, SEEK_CUR)                   = 0
ioctl(3, TCGETS, 0x7ffd50dc76c0)        = -1 ENOTTY (Inappropriate ioctl for device)
lseek(3, 0, SEEK_CUR)                   = 0
lseek(3, 0, SEEK_CUR)                   = 0
mmap(NULL, 10000003072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcd9892e000
mmap(NULL, 10000003072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcb4486f000
write(3, "11111111111111111111111111111111"..., 10000000000) = 2147479552
write(3, "11111111111111111111111111111111"..., 7852520448) = 2147479552
write(3, "11111111111111111111111111111111"..., 5705040896) = 2147479552
write(3, "11111111111111111111111111111111"..., 3557561344) = 2147479552
write(3, "11111111111111111111111111111111"..., 1410081792) = 1410081792
munmap(0x7fcb4486f000, 10000003072)     = 0
munmap(0x7fcd9892e000, 10000003072)     = 0
close(3)                                = 0
write(1, "closed\n", 7closed
)                 = 7
rt_sigaction(SIGINT, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fcfedd5cf20}, {sa_handler=0x62ffc0, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fcfedd5cf20}, 8) = 0
sigaltstack(NULL, {ss_sp=0x2941be0, ss_flags=0, ss_size=8192}) = 0
sigaltstack({ss_sp=NULL, ss_flags=SS_DISABLE, ss_size=0}, NULL) = 0
exit_group(0)                           = ?
+++ exited with 0 +++
Run Code Online (Sandbox Code Playgroud)

如果它存在,这是strace:

openat(AT_FDCWD, "foo", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 3
fstat(3, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
ioctl(3, TCGETS, 0x7fffa00b4fe0)        = -1 ENOTTY (Inappropriate ioctl for device)
lseek(3, 0, SEEK_CUR)                   = 0
ioctl(3, TCGETS, 0x7fffa00b4fb0)        = -1 ENOTTY (Inappropriate ioctl for device)
lseek(3, 0, SEEK_CUR)                   = 0
lseek(3, 0, SEEK_CUR)                   = 0
mmap(NULL, 10000003072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f71de68b000
mmap(NULL, 10000003072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f6f8a5cc000
write(3, "11111111111111111111111111111111"..., 10000000000) = 2147479552
write(3, "11111111111111111111111111111111"..., 7852520448) = 2147479552
write(3, "11111111111111111111111111111111"..., 5705040896) = 2147479552
write(3, "11111111111111111111111111111111"..., 3557561344) = 2147479552
write(3, "11111111111111111111111111111111"..., 1410081792) = 1410081792
munmap(0x7f6f8a5cc000, 10000003072)     = 0
munmap(0x7f71de68b000, 10000003072)     = 0
close(3#### strace will block exactly here until write-back is completed ####)                                = 0 
write(1, "closed\n", 7closed
)                 = 7
rt_sigaction(SIGINT, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7f7433ab9f20}, {sa_handler=0x62ffc0, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7f7433ab9f20}, 8) = 0
sigaltstack(NULL, {ss_sp=0x1c68be0, ss_flags=0, ss_size=8192}) = 0
sigaltstack({ss_sp=NULL, ss_flags=SS_DISABLE, ss_size=0}, NULL) = 0
exit_group(0)                           = ?
+++ exited with 0 +++
Run Code Online (Sandbox Code Playgroud)

当简单地打印和管道到文件而不是使用 python file-io 时,以及使用小的等效 C++ 程序打印到 cout 时,可以观察到相同的行为。它似乎是阻塞的实际系统调用。

ilk*_*chu 8

这听起来像是在提醒我们O_PONIES刚刚度过了 11 岁生日的惨败。

在 ext4 出现之前,ext3 已经获得了一种在断电时保持稳定的声誉。它很少损坏,也很少从文件中丢失数据。然后,ext4 增加了数据块的延迟分配,这意味着它甚至不会立即尝试将文件数据写入磁盘。通常,只要数据在某个时间到达那里就不是问题,而对于临时文件,结果可能根本不需要将数据写入磁盘。

但是 ext4 确实写入了元数据更改,并记录了文件发生了一些变化。现在,如果系统崩溃,文件被标记为截断,但之后的写入不会存储在磁盘上(因为没有为它们分配块)。因此,在 ext4 上,您经常会看到最近修改的文件在崩溃后被截断为零长度。

这当然不是大多数用户想要的,但有人认为如此关心他们的数据的应用程序应该调用fsync(),如果他们真的关心重命名,他们应该fsync()(或至少fdatasync())包含目录也。然而,几乎没有人这样做,部分原因是在 ext3 上,fsync()同步了整个磁盘,可能包括大量不相关的数据。(或者尽可能接近整个磁盘,无论如何差异都无关紧要。)

现在,一方面,您的 ext3 性能不佳,fsync()另一方面,ext4 要求fsync()不丢失文件。这不是一个好情况,考虑到大多数应用程序会关心实现特定于文件系统的行为,甚至比fsync()在正确时刻调用的僵硬舞蹈还要少。显然,首先要弄清楚文件系统作为 ext3 还是 ext4 挂载并不容易。

最后,ext4 开发人员对最常见的看似关键的情况进行了一些更改

  • 在另一个文件上重命名文件。在正在运行的系统上,这是一个原子更新,通常用于放置文件的新版本。
  • 覆盖现有文件(您的情况)。这在正在运行的系统上不是原子的,但通常意味着应用程序想要替换文件,而不是截断文件。如果覆盖失败,您也会丢失旧版本的文件,因此这与创建一个全新的文件略有不同,后者断电只会丢失最新的数据。

据我所知,即使在 ext4 之前,XFS 在崩溃后也表现出类似的零长度文件。不过,我从来没有这样做过,所以我不知道他们会做什么样的修复。

参见,例如这篇关于 LWN 的文章,其中提到了修复:ext4 和数据丢失(2009 年 3 月)

当然,当时还有其他关于此的文章,但我不确定链接到它们是否有用,因为这主要是指指点点的问题。