我有一个进程(据说)使用 libparquet/libarrow 的 StreamWriter 定期写入文件(应该每约 65MB 写入一次)。当通过 shell 例如 查询文件时watch du -sh {file_name}
,它的大小似乎没有增加。但是,当该过程完成时,该文件似乎已完全写入。
这里到底发生了什么?我猜测该进程已打开文件并继续写入文件直至完成,但是文件的实际大小何时反映给用户(或操作系统)?是在文件最终关闭后吗?
当我重复写入 python 中的文件时,例如,通过orwith open('file.txt','w') as fp
查询 shell 中的文件反映文件的实际大小。这个Python只是在幕后添加额外的步骤吗?ls
du
答案1
如果没有详细说明程序使用什么方法,答案是“介于非常快和从不之间”,因为执行 I/O 的方法有很多种,而且过程中可能会出现很多问题。此外,各种操作系统或文件系统可能会使事情变得复杂,这里不予讨论。
请注意,某些系统可能需要定义随机魔术字符串来编译如下所示的测试程序;例如,参见
feature_test_macros(7)
Linux 的某些版本。您可能还需要一个 C99 编译器,现在很多人(但不是每个人......)都提供了它。
写
write(2)
非常简单;由于缺少缓冲,并且这里代码中没有其他任何事情发生,因此此调用的输出应该出现在频谱的较快端(如果一切顺利)。在ktrace
或
strace
或其他类似的系统调用跟踪实用程序下运行这些脚本可能会很有教育意义。
// write.c -- `rm out; make write && ./write > out`
#include <stdio.h>
#include <unistd.h>
int main(void) {
char buf[] = "this is some output\n";
//while(1) {
write(STDOUT_FILENO, buf, sizeof(buf) / sizeof(char));
//}
return 0;
}
取消注释无限循环以获得更多输出,并添加sleep(3)
调用以减慢速度以帮助解决“文件系统显示什么?”测试。
缓冲中...缓冲中...缓冲中...
Real Programs(TM) 可能会使用缓冲 I/O,并且可能会发生太多其他事情(膨胀);我们在这里只讨论缓冲。首先,一个永远不会发出输出的程序:
// never.c -- `rm out; make never && ./never > out`
#include <stdio.h>
#include <unistd.h>
int main(void) {
setvbuf(stdout, 0, _IOFBF, 0);
fputs("x", stdout);
_exit(0);
}
这会选择完全缓冲,但随后退出而不刷新任何内容。还有许多其他方法可以实现这一点,例如人们对kill -9
、 段错误等很感兴趣。如果系统非常、非常、非常繁忙,例如当双处理器 Linux 时,通常快速输出可能会变成“几乎从不”系统的CPU负载为5,000左右...
这里的简单修改是更改为_exit
然后在之前exit
添加一个sleep
调用来模拟延迟输出到文件系统,然后添加一个带有睡眠的无限循环来延迟输出以填充缓冲区大小,等等。无论如何,通过缓冲文件系统上的输出大小可能会落后于程序写入的大小。
重命名临时文件
另一个技巧是写入临时文件,然后在 I/O 完成时将临时文件重命名为实际文件;这避免了读者看到写了一半的输出文件的问题。在这种情况下,输出文件仅在调用后才会反映新的大小rename(2)
。
// rename.c -- `rm out*; make rename && ./rename & sleep 1; ls -l out*`
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void) {
char *tmpbuf = strdup("out.XXXXXXXXXX");
char *tmpfile = mktemp(tmpbuf);
if (!tmpfile) err(1, "mktemp failed");
FILE *fh = fopen(tmpfile, "w");
if (!fh) err(1, "fopen failed");
fputs("this is some renamed output\n", fh);
fclose(fh);
sleep(9);
rename(tmpfile, "out");
//free(tmpbuf);
return 0;
}
在这里,out
文件应该仅在九秒左右后反映新的大小。或者,当系统非常非常繁忙时,还有很多很多秒。或者,永远不要在重命名完成之前发生某些事情。
内存映射
另一种执行 I/O 的方法是通过内存映射。然而,这并没有显示任何缓冲的迹象,尽管可能有标志来改变这种行为。
// mmap.c -- `make mmap && ./mmap & sleep 1; od -bc out`
#include <sys/mman.h>
#include <err.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void) {
char buf[] = "this is some mmap'd output\n";
size_t buflen = sizeof(buf) / sizeof(char);
int fd = open("out", O_RDWR | O_CREAT, 0666);
ftruncate(fd, buflen);
if (fd < 0) err(1, "open failed");
void *ptr = mmap(0, buflen, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) err(1, "mmap failed");
sleep(9);
memcpy(ptr, buf, buflen);
munmap(ptr, buflen);
close(fd);
return 0;
}
I/O 完成后,内存映射文件可以从临时文件重命名为预期文件(请参阅前面的技巧)。这样做可能有充分的理由。
其他
可能还有其他形式的可用 I/O,它们使用与上述不同的系统调用。strace
如果你知道要寻找什么,那么应该会揭示它们。它们可能具有缓冲,可能会延迟文件系统上输出的出现。编写简单的脚本将有助于显示调用的行为方式,这应该有助于显示在更大的程序中寻找什么strace
。希望有文档。
堆得更高更深
除了操作系统提供的缓冲之外,构建在标准库之上的语言可能还有自己的缓冲,尽管通常会有一个flush
或(finish-output)
多个此类调用来尝试将这些位传输到磁盘,也许还有一个sync
调用某种方式来调整默认行为,就像setvbuf(3)
让你做的那样。或不。某些语言或编程指南可能设置与操作系统不同的默认值。缓冲输出可能会显示到达文件系统的延迟,或者可能正在使用重命名技巧。