![为什么文件在被另一个进程写入时有时会显示为空](https://linux22.com/image/720770/%E4%B8%BA%E4%BB%80%E4%B9%88%E6%96%87%E4%BB%B6%E5%9C%A8%E8%A2%AB%E5%8F%A6%E4%B8%80%E4%B8%AA%E8%BF%9B%E7%A8%8B%E5%86%99%E5%85%A5%E6%97%B6%E6%9C%89%E6%97%B6%E4%BC%9A%E6%98%BE%E7%A4%BA%E4%B8%BA%E7%A9%BA.png)
我们遇到过这样的情况:文件通过 FTP 上传到文件夹,然后由 nginx 提供服务。我们发现,如果在修改文件后立即发送 GET 请求,nginx 将返回 0 字节的文件。
在尝试调试这个问题时,我编写了 2 个 python 脚本来查看是否能以简单的方式重现该错误。
第一个写入文件
while True:
with open('testfile' , 'w') as f:
f.write("test")
第二句是
while True:
with open('testfile' , 'r') as cf:
print(cf.read())
当在两个单独的进程中运行这些文件时,读取器的输出要么是“test”,要么是“”,这表明有时文件对读取器来说似乎是空的。这似乎与 python 实现无关,因为我可以用 bash 重现这种效果,如下所示:
(writer.sh)
while true; do
echo test > testfile
done
(读者.sh)
while true; do
cat testfile
printf "\n"
done
文件系统是ext4,操作系统是Ubuntu 16.04。
所以:
为什么读者有时会看到一个空文件(大约 50% 的时间)?
为什么我们从来没有看到部分写入(“te”,“tes”等)?
提前感谢您的帮助。
答案1
太棒了,你刚刚发现了文件缓冲。写入磁盘时,你可以使用缓冲写入或直接 I/O 写入。出于性能原因,大多数软件(包括 Python 解释器)默认使用缓冲写入。如果你需要执行直接 I/O,有一个很好的 Python 模块,名字恰如其分方向就是这样的。
然而大多数时候您不需要直接 I/O,除非您正在写入某些日志文件或数据库。
答案2
其他人已经描述了这是如何缓冲 I/O,您可以在其内容被刷新之前看到被截断的文件。
关于解决此问题的几种方法的更多详细信息:
将文件上传到与目标相同的文件系统上的临时目录,然后 mv 到位。重命名是一个原子操作,因此读者只会看到旧文件或新文件,而不会看到介于两者之间的文件。但是,除非应用程序调用 fsync(),否则内核仍会按计划完成对磁盘的写入。关闭文件或等待任意时间不会不是可靠地使文件存在于磁盘上。
或者,将应用程序更改为由数据库支持。让数据库在内存和存储中提供文档的一致视图,这就是它们的作用。如果唯一的原因是为了摆脱非常小的不一致窗口,那么实施努力可能不值得。
答案3
您可能会遇到以下竞争条件:
- 由于重定向(“>”),写入会截断文件。
- 该文件被读取器读取(空文件)。
- 该文件是由作者编写的。
如果在写入循环中设置短暂的睡眠时间,则会出现更少的这种情况。
您可以使用原子操作创建文件来避免这种情况,例如:
while true do;
echo test > file.tmp
mv file.tmp testfile
done
您的原始代码将不断截断并写入同一个文件。上面的循环将不断创建新文件。该mv
命令是原子的,读取器将始终看到一个包含数据的文件。这将是被删除的文件mv
或新文件。
答案4
文件通常以块(完整数据的子集)的形式写入,这些块的大小由所使用的功能和可用的系统资源共同决定,因此操作系统通常会尝试优化块大小。实际情况是,块在写入磁盘之前会先写入 RAM,然后整个块会一次性写入磁盘,在写入该块期间没有时间进行读取。这导致写入速度比其他方式快得多。
在您的情况下,写入单词“test”将小于操作系统选择的任何块大小,因此它将一次性全部写入。对于您的测试,您应该编写一个更大的测试,并可能设置块大小(尽管大多数情况下最好让操作系统决定)。
我怀疑测试中发生的情况是,一半时间是在写入之前捕获空文件,另一半时间是在写入块之后捕获空文件。如果您尝试写入大于块大小的数据量,我认为您会看到部分写入的文件。