“UNIX编程环境”( the UNIX Programming Environment)一书第32页对UNIX管道作了深刻的说明:
管道中的程序实际上是同时运行的,而不是一个接一个地运行。这意味着管道中的程序可以是交互式的;内核负责所有的调度和同步工作。
哇!通过使用管道,我可以免费获得并行处理!
“我必须向我的同事们展示这种超棒的能力。”我想。我将实现一个演示:创建一个简单的交互式工具来模拟我在命令窗口输入的内容。我将把这个工具命名为mimic。使用管道将其连接到另一个工具,该工具计算行数和字符数。我将把这个工具命名为wc (遗憾的是,我正在使用wc,它没有UNIX程序,所以我必须实现自己的程序)。这两个工具将在这样的命令行上运行:
mimic | wcmimic的输出通过管道传输到wc。
好吧,就是这个主意。
我用一个非常简单的Flex实现了mimic工具,如下所示。编译生成的mimic.exe
当我从命令行运行mimic.exe时,它确实模仿了我输入的内容:
> mimic
hello world
hello world
greetings
greetings
ctrl-c我使用AWK实现了wc。AWK程序(wc.awk)如下所示。当我从命令行运行wc时,它确实会计算行数和字符数:
> echo Hello World | awk -f wc.awk
lines 1 chars 13然而,当我把它们和管子放在一起时,它们并不像我想象的那样工作。下面是一个示例运行:
> mimic | awk -f wc.awk
hello world
greetings
ctrl-c嗯,没什么。不要模仿。线不算。没有点点滴滴。
怎么不管用了?我做错什么了,拜托?
我能做些什么来使它如我所期望的那样运作呢?我希望它能像这样工作:我在命令行中输入了一些内容。mimic重复它,将其发送到管道,后者将其发送给wc,后者报告行数和字符数。我在命令行输入下一个内容。mimic重复它,将其发送到管道,后者将其发送给wc,后者报告行数和字符数。以此类推。这就是我认为我正在实施的行为。
这里是我的简单Flex (模拟):
%option noyywrap
%option always-interactive
%%
%%
int main(int argc, char *argv[])
{
yyin = stdin;
yylex();
return 0;
}这里是我的简单AWK程序(wc):
{ nchars = nchars + length($0) + 1 }
END { printf("lines %-10d chars %d\n", NR, nchars) }发布于 2022-08-05 23:10:38
这个问题与Flex、Bison、Awk几乎没有关系,实际上也与Unix无关(因为您正在试验Windows)。
我手头没有Windows,但根本的问题是stdio缓冲区,所以它在Unix上也是可以复制的。
为了简化起见,我只实现了mimic,这是我直接实现的,而不是使用Flex (这显然是过分的):
#include <stdio.h>
int main(void) {
for (int ch; (ch = getchar()) != EOF; ) putchar(ch);
return 0;
}由于使用了%always-interactive (这迫使Flex每次使用fgetc()读取一个字符),所以除了将一个字节的fwrite简化为等效的putchar之外,它基本上具有与程序相同的标准库调用序列。
当然,它具有同样的执行特性:
$ ./mimic
Here we go round the mulberry busy,
Here we go round the mulberry busy,
the mulberry bush, the mulberry bush.
the mulberry bush, the mulberry bush.在上面,我通过为第三行输入Ctrl来表示输入的结束.在Windows上,我必须输入Ctrl,然后输入Enter才能获得同样的效果。如果我通过输入Ctrl来终止执行,则得到大致相同的结果(除了Ctrl出现在控制台之外):
$ ./mimic
Here we go round the mulberry bush,
Here we go round the mulberry bush,
the mulberry bush, the mulberry bush.
the mulberry bush, the mulberry bush.
^C现在,由于mimic只是将stdin复制到stdout,所以我可能希望能够将其导入自身并获得相同的结果。但是输出有一点不同:
$ ./mimic | ./mimic
Here we go round the mulberry bush,
the mulberry bush, the mulberry bush.
Here we go round the mulberry bush,
the mulberry bush, the mulberry bush.再次,我通过键入Ctrl发出输入结束的信号。只有在我输入了Ctrl之后,才出现了任何输出;此时,两行都得到了回响。如果我突然用Ctrl终止程序,我根本就得不到任何输出:
$ ./mimic | ./mimic
Here we go round the mulberry bush,
the mulberry bush, the mulberry bush.
^C好的,这就是数据。现在我们需要一个解释。这一解释与C标准库流的缓冲方式有关,您可以在setvbuf手册(以及web上的许多地方)中看到这些内容。我将总结如下:
fgetc的重复调用来实现的,所有输出函数“似乎”都是通过对fputc的重复调用来实现的。请注意,这意味着单个printf或fwrite编写的字节块没有什么特殊之处。库没有做任何事情来确保调用序列是原子的。如果有两个进程同时写入stdout,则消息可以交织在一起,这将不时发生.。
fputc编写的数据(因此,所有stdio输出函数)实际上被放置在输出缓冲区中。此缓冲区不是操作系统的一部分(操作系统可能会添加另一层缓冲)。它严格地说是stdio库函数的一部分,这些函数是普通的userland函数。你可以自己写(这是一个很好的练习)。该缓冲区的内容不时被发送到相应的操作系统接口,以便传输到输出设备。。
三种缓冲模式是:
1. **Unbuffered**. In this mode, each byte written is transferred to the operating system (and, it is hoped, to the actual output device) as soon as possible.2. **Fully buffered**. In this mode, there is a buffer of a predetermined size (often 8 kilobytes, but different library implementations have different defaults for different platforms). If you want to, you can supply your own buffer (of an arbitrary size) for a particular output stream, using the `setvbuf` standard library function (q.v.). Fully-buffered output might stay in the output buffer until it is full (although a given implementation may release output earlier). The buffer will, however, be sent to the operating system if you call `fflush` or `fclose` on the stream, or if `fclose` is called automatically when `main` returns. (It's not sent if the process dies, though.)3. **Line-buffered**. In this mode, the stream again has an output buffer of a predetermined size, which is usually exactly the same as the buffer used in "fully-buffered" mode. The difference is that the buffer is also sent to the operating system when a new line character (`'\n'`) is written. (If the buffer gets full before an end-of-line character is written, then it is sent to the operating system, just as in Fully-Buffered mode. But most of the time, lines will be fairly short, so the buffer will be sent to the OS at the end of each line.)最后,您需要知道stdout在默认情况下是完全缓冲的,除非标准库可以确定stdout连接到某种类型的控制台设备,在这种情况下,它不是完全缓冲的。(在Unix上,它通常是行缓冲的。)相比之下,stderr没有完全缓冲.(在Unix上,通常是不缓冲的。)您可以在对流进行第一次写入操作之前,更改缓冲模式和缓冲区大小(如果相关)。
上述默认设置是鼓励您将错误消息写入stderr而不是stdout的原因之一:由于stderr没有缓冲,错误消息将尽快出现。另外,通常应该将\n放在输出行的末尾;如果标准输出是一个终端,因此是行缓冲的,那么\n将确保该行实际上是输出的,而不是在输出缓冲区中萎靡不振。
在上面的例子中,您可以看到所有这些都在起作用。当我只运行./mimic,将stdout映射到终端时,每次输入一行时,输出都会显示出来。(这也与终端驱动程序处理终端输入的方式有关,后者是另一壶鱼。)
但是,当我将mimic管道导入自身时,第一个mimic的标准输出将被重定向到管道。管道不是终端,因此mimic的stdout在默认情况下是完全缓冲的。由于缓冲区比总输入长,整个程序运行时不向stdout发送任何东西,直到当stdout被main返回隐式关闭时,缓冲区被刷新。
此外,如果我杀死进程(例如,通过输入Ctrl,或者通过向它发送SIGKILL信号),那么输出缓冲区永远不会发送到操作系统,控制台上什么也不会出现。
如果您正在使用标准C库调用编写控制台应用程序,了解stdio输出缓冲如何影响您看到的输出序列是非常重要的。Windows上的情况和Unix上的情况一样。(当然,如果使用本机Windows或Posix I/O接口,则不适用。)
https://stackoverflow.com/questions/73254838
复制相似问题