为什么stdout在redirect到文件时需要显式刷新?
printf()
的行为似乎取决于stdout
的位置。
- 如果
stdout
被发送到控制台,则printf()
是行缓冲的,并在打印换行符后被刷新。 - 如果
stdout
被redirect到一个文件,除非fflush()
被调用,否则缓冲区不会被刷新。 - 而且,如果在将
stdout
redirect到文件之前使用printf()
,则后续写入(对该文件)将被行caching,并在换行之后进行刷新。
什么时候stdout
行缓冲,什么时候fflush()
需要被调用?
每个的最小示例:
void RedirectStdout2File(const char* log_path) { int fd = open(log_path, O_RDWR|O_APPEND|O_CREAT,S_IRWXU|S_IRWXG|S_IRWXO); dup2(fd,STDOUT_FILENO); if (fd != STDOUT_FILENO) close(fd); } int main_1(int argc, char* argv[]) { /* Case 1: stdout is line-buffered when run from console */ printf("No redirect; printed immediately\n"); sleep(10); } int main_2a(int argc, char* argv[]) { /* Case 2a: stdout is not line-buffered when redirected to file */ RedirectStdout2File(argv[0]); printf("Will not go to file!\n"); RedirectStdout2File("/dev/null"); } int main_2b(int argc, char* argv[]) { /* Case 2b: flushing stdout does send output to file */ RedirectStdout2File(argv[0]); printf("Will go to file if flushed\n"); fflush(stdout); RedirectStdout2File("/dev/null"); } int main_3(int argc, char* argv[]) { /* Case 3: printf before redirect; printf is line-buffered after */ printf("Before redirect\n"); RedirectStdout2File(argv[0]); printf("Does go to file!\n"); RedirectStdout2File("/dev/null"); }
刷新stdout
是由其缓冲行为决定的。 缓冲可以设置为三种模式: _IOFBF
(完全缓冲:如果可能的话,一直等到fflush()
),_ _IOLBF
(行缓冲:换行触发自动刷新)和_IONBF
(直接写入总是使用)。 “对这些特性的支持是实现定义的,可能会受到setbuf()
和setvbuf()
函数的影响。” [C99:7.19.3.3]
“在程序启动时,三个文本stream是预定义的,不需要明确打开 – 标准input(用于读取常规input),标准输出(用于写入常规输出)和标准错误(用于写入诊断输出)。标准错误stream没有被完全缓冲;标准input和标准输出stream被完全缓冲,当且仅当stream可以被确定为不涉及交互设备时。 [C99:7.19.3.7]
观察行为的解释
那么,会发生什么呢是这个实现做了一些特定于平台的事情来决定stdout
是否将被行缓冲。 在大多数libc实现中,这个testing在stream首次使用时完成。
- 行为#1很容易解释:当stream为交互设备时,它是行缓冲的,并且
printf()
被自动刷新。 - 情况#2也是现在的预期:当我们redirect到一个文件时,stream被完全缓冲,除非使用
fflush()
,否则不会被刷新,除非您向其中写入gobloads的数据。 - 最后,我们也了解了情况3,对于只执行底层fd检查的实现也是如此。 因为我们强制stdout的缓冲区在第一个
printf()
初始化,stdout获得了行缓冲模式。 当我们换出fd去文件,它仍然是行缓冲的,所以数据自动刷新。
一些实际的实现
每个libc都有自己的解释这些需求的自由度,因为C99没有指定什么是“交互设备”, POSIX的stdio条目也没有扩展(除了要求stderr被打开阅读)。
-
glibc的。 请参阅filedoalloc.c:L111 。 这里我们使用
stat()
来testingfd是否是tty,并相应地设置缓冲模式。 (这是从fileops.c中调用的。)stdout
最初有一个空的缓冲区,并根据fd 1的特性在第一次使用stream时分配它。 -
BSD libc。 非常相似,但更简洁的代码要遵循! 在makebuf.c中看到这一行
你错误地结合了缓冲和无缓冲的IOfunction。 这样的组合必须非常小心,尤其是当代码必须是可移植的。 (写不可移植的代码是不好的)
最好避免在同一个文件描述符上结合缓冲和非缓冲IO。
缓冲IO: fprintf()
, fopen()
, fclose()
, freopen()
…
Unbuffered IO: write()
, open()
, close()
, dup()
…
当你使用dup2()
来redirect标准输出。 该函数不知道由fprintf()
填充的缓冲区。 所以当dup2()
closures旧描述符1时,它不会刷新缓冲区,并且内容可能被刷新到不同的输出。 在你的情况2a它发送到/dev/null
。
解决scheme
在你的情况下,最好使用freopen()
而不是dup2()
。 这解决了所有的问题:
- 它刷新原始
FILE
stream的缓冲区。 (情况2a) - 它根据新打开的文件设置缓冲模式。 (情况3)
这里是你的函数的正确实现:
void RedirectStdout2File(const char* log_path) { if(freopen(log_path, "a+", stdout) == NULL) err(EXIT_FAILURE, NULL); }
不幸的是,对于缓冲IO,您不能直接设置新创build的文件的权限。 您必须使用其他调用来更改权限,或者您可以使用不可移植的glibc扩展。 请参阅fopen() man page
。
您不应该closures文件描述符,所以如果您希望只在文件中打印消息,请移除close(fd)
并closuresstdout_bak_fd
。