首页 文章

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

提问于
浏览
26

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

  • 如果 stdout 被发送到控制台,则 printf() 是行缓冲的,并在打印换行符后刷新 .

  • 如果将 stdout 重定向到文件,则不会刷新缓冲区,除非调用 fflush() .

  • 此外,如果在将 stdout 重定向到文件之前使用 printf() ,则后续写入(对文件)将进行行缓冲并在换行后刷新 .

什么时候 stdout 行缓冲,什么时候需要调用_1790759?

每个的最小例子:

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 回答

  • 33

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

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

    观察行为的说明

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

    • 行为#1很容易解释:当流用于交互式设备时,它是行缓冲的,并且 printf() 会自动刷新 .

    • 情况#2现在也是预期的:当我们重定向到文件时,流被完全缓冲并且除了 fflush() 之外不会被刷新,除非你向它写入gobloads数据 .

    • 最后,对于仅对底层fd执行一次检查的实现,我们也理解案例#3 . 因为我们强制stdout的缓冲区在第一个 printf() 中被初始化,所以stdout获得了行缓冲模式 . 当我们换出fd转到文件时,它仍然是行缓冲的,因此数据会自动刷新 .

    一些实际的实现

    每个libc都有自己解释这些要求的自由度,因为C99没有指定"interactive device"是什么,POSIX's stdio entry也没有扩展它(超出要求stderr打开阅读) .

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

    • BSD libc . 非常相似,但要遵循更清晰的代码!见this line in makebuf.c

  • 0

    您错误地将缓冲和非缓冲IO功能组合在一起 . 必须非常小心地完成这种组合,特别是当代码必须是便携式时 . (编写不可移植的代码很糟糕......)
    It is certainly best to avoid combining buffered and unbuffered IO on the same file descriptor.

    Buffered IO: fprintf()fopen()fclose()freopen() ...

    Unbuffered IO: write()open()close()dup() ...

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

    解决方案

    在您的情况下,最好使用 freopen() 而不是 dup2() . 这解决了所有问题:

    • 它刷新原始 FILE 流的缓冲区 . (案例2a)

    • 根据新打开的文件设置缓冲模式 . (案例3)

    以下是您的函数的正确实现:

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

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

  • 3

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

相关问题