为什么stdout在重定向到文件时需要显式刷新?

时间:2012-12-18 12:23:31

标签: c linux stdout

printf()的行为似乎取决于stdout的位置。

  1. 如果将stdout发送到控制台,则printf()将进行行缓冲,并在打印换行符后刷新。
  2. 如果将stdout重定向到文件,则除非调用fflush(),否则不会刷新缓冲区。
  3. 此外,如果在将printf()重定向到文件之前使用stdout,则后续写入(对文件)是行缓冲的,并在换行后刷新。
  4. 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");
    }
    

3 个答案:

答案 0 :(得分:33)

stdout的刷新由其缓冲行为决定。缓冲可以设置为三种模式:_IOFBF(完全缓冲:如果可能,等待fflush()),_IOLBF(行缓冲:换行触发自动刷新)和_IONBF (直接写总是使用)。 “对这些特性的支持是实现定义的,可能会受到setbuf()setvbuf()函数的影响。” [C99:7.19.3.3]

“在程序启动时,预定义了三个文本流,无需明确打开 - 标准输入(用于读取传统输入),标准输出(用于写入) 常规输出)和标准错误(用于写入诊断输出)。最初 打开后,标准错误流未完全缓冲;标准输入和标准 当且仅当可以确定流不被引用时,输出流被完全缓冲 到交互式设备。“[C99:7.19.3.7]

观察行为的说明

因此,实现的是该实现执行某些特定于平台的事情来决定stdout是否将进行行缓冲。在大多数libc实现中,此测试在首次使用流时完成。

  1. 行为#1很容易解释:当流用于交互式设备时,它是行缓冲的,printf()会自动刷新。
  2. 现在还需要案例#2:当我们重定向到文件时,流被完全缓冲,除了fflush()之外不会被刷新,除非您向其写入gobloads数据。
  3. 最后,我们也了解案例#3对于仅对底层fd执行一次检查的实现。因为我们强制stdout的缓冲区在第一个printf()中初始化,所以stdout获得了行缓冲模式。当我们换出fd转到文件时,它仍然是行缓冲的,因此数据会自动刷新。
  4. 一些实际的实现

    每个libc都有自己解释这些要求的自由度,因为C99没有指定“交互式设备”是什么,POSIX's stdio entry也没有扩展它(除了要求stderr打开阅读之外)。

    1. 的Glibc。见filedoalloc.c:L111。这里我们使用stat()来测试fd是否为tty,并相应地设置缓冲模式。 (这是从fileops.c调用的。)stdout最初有一个空缓冲区,它是在第一次使用流时根据fd 1的特性分配的。

    2. BSD libc。非常相似,但要遵循更清晰的代码!请参阅this line in makebuf.c

答案 1 :(得分:3)

您错误地将缓冲和非缓冲IO功能组合在一起。必须非常小心地完成这种组合,特别是当代码必须是便携式时。 (编写不可移植的代码是不好的...)
当然最好避免在同一个文件描述符上组合缓冲和非缓冲IO。

缓冲IO: fprintf()fopen()fclose()freopen() ......

无缓冲IO: write()open()close()dup() ...

使用dup2()重定向标准输出时。该函数不知道由fprintf()填充的缓冲区。因此,当dup2()关闭旧描述符1时,它不会刷新缓冲区,并且内容可以刷新到不同的输出。在您的情况2a中,它被发送到/dev/null

解决方案

在您的情况下,最好使用freopen()代替dup2()。这解决了所有问题:

  1. 它刷新原始FILE流的缓冲区。 (案例2a)
  2. 根据新打开的文件设置缓冲模式。 (案例3)
  3. 以下是您的函数的正确实现:

    void RedirectStdout2File(const char* log_path) {
        if(freopen(log_path, "a+", stdout) == NULL) err(EXIT_FAILURE, NULL);
    }
    

    不幸的是,对于缓冲IO,您无法直接设置新创建文件的权限。您必须使用其他调用来更改权限,或者您可以使用不可移植的glibc扩展。请参阅fopen() man page

答案 2 :(得分:0)

您不应该关闭文件描述符,因此如果您希望仅在文件中打印消息,请删除close(fd)并关闭 stdout_bak_fd