FILE * "/dev/stdout" 和 stdout 之间的区别

12

让我们来看看这个Hello World程序。

#include <stdio.h>
int main(int argc, char ** argv) {
    printf("Hello, World!");

    const char* sFile = "/dev/stdout"; // or /proc/self/fd/0
    const char* sMode = "w";
    FILE * output = fopen(sFile, sMode);
    //fflush(stdout) /* forces `correct` order */
    putc('!', output); // Use output or stdout from stdio.h

    return 0;
}

使用output文件描述符编译时,输出结果为:

!Hello, World!

使用stdio.h提供的stdout文件描述符进行编译,输出结果如预期:

Hello, World!!
我想当使用后者作为参数来调用putc时,它将直接打印到stdout中,而当使用/dev/stdout的文件描述符时,它将打开一个管道并将内容打印到管道中。但我并不确定。
这种行为更加有趣,因为它不会覆盖“Hello”单词的第一个字符,而是将自己推到已经推送字符串前面的行缓冲区的第一个位置。
从逻辑上讲,这是非常出乎意料的。
有人能解释一下这里到底发生了什么吗?
我正在使用cc(Ubuntu 4.8.2-19ubuntu1)4.8.2和使用gcc 4.8.2编译的3.13.0-52 Linux内核。
编辑:我对两个程序进行了strace,以下部分很重要:
没有使用fflush(stdout)的情况下,outputfopen("/dev/stdout","w"))产生:
...
open("/dev/stdout", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
fstat(3, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f62f21e9000
write(3, "!", 1!)                        = 1
write(1, "Hello, World!", 13Hello, World!)           = 13
exit_group(0)                           = ?

使用 fflush(stdout) 可以产生并强制执行正确的顺序:

...
open("/dev/stdout", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
write(1, "Hello, World!", 13Hello, World!)           = 13
fstat(3, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5ad4557000
write(3, "!", 1!)                        = 1
exit_group(0)                           = ?

stdout(来自 stdlib.h )情况会产生:

...
write(1, "Hello, World!!", 14Hello, World!!)          = 14
exit_group(0)                           = ?

看起来 FILE * output = fopen("/dev/stdout") 流使用的文件描述符与 stdout 不同。 同时,printf 使用 stdout。 因此,在第三种情况下,字符串在推送到流之前被组装。


stdout 是一个 FILE *,而不是文件描述符。同样,output 也不是文件描述符。它们每个都有一个底层的文件描述符,如果你直接向其写入内容,你将看不到这种行为。(直接向文件描述符写入内容会绕过缓冲区。) - William Pursell
最大的区别在于每个 FILE* 都使用自己的缓冲区,彼此之间没有关联。 - Some programmer dude
2个回答

18

这两个流(stdoutoutput)都是带缓冲的。直到刷新它们之前,没有任何东西实际被写出来。由于您没有显式地刷新它们,也没有安排自动刷新,因此它们只有在关闭时才会被自动刷新。

您也没有显式地关闭它们,所以它们被标准库的 on_exit 钩子关闭(并且被刷新)。正如 William Pursell 正确指出的那样,关闭缓冲 I/O 流的顺序没有指定。

查看 fflush(3)fclose(3)setbuf(3) 手册页,了解如何控制何时以及如何刷新输出。


3
重点是关闭的顺序未指定。 - William Pursell
在“输出”场景中,在putc(...)之前添加fflush(stdout)不会改变行为。 - MrPaulch
1
对我来说,它非常可靠。我建议在生成的可执行文件上运行 strace 命令,以查看所产生的系统调用序列。这样做可以很好地了解缓冲 I/O 函数与系统的交互方式。 - Gil Hamilton
虽然官方没有指定顺序,但 on_exit 钩子的调用顺序是相反的:后创建的先被调用。 - user3458
@MrPaulch,您的编辑中有fflush(output),但应该是fflush(stdout) - nos
@nos 很好的发现!谢谢。刷新stdout确实解决了问题! - MrPaulch

3

/dev/stdout/proc/self/fd/0不是同一个东西。实际上,如果您没有足够的权限,您将无法将内容写入/dev/stdout,因为/dev/stdout在Linux中不是任何标准字符设备,并且尝试使用"w"选项打开它将会在该目录下创建一个实际的常规文件。您要查找的字符设备是/dev/tty

在C语言中,stdout是一个初始化的全局变量,类型为FILE *,它指向标准输出文件,即其描述符为1的文件。stdout只存在于C命名空间中,与名为"stdout"的任何实际文件都没有关系。


2
我不知道你使用的是哪个发行版,但 /dev/stdout 是一个符号链接,指向 /proc/self/fd/1,而后者又是我的 /dev/pts/XX 的符号链接。因此它们是可以互换的。最终,那甚至不是问题的所在 :) - MrPaulch
3
值得一提的是,/dev/stdout、/proc/self/fd 和 /dev/fd 都没有标准化,而 /dev/tty 则有。 - Random832

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接