首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >在UNIX管道中,如何将第一阶段的用户工具交互连接到下一阶段?

在UNIX管道中,如何将第一阶段的用户工具交互连接到下一阶段?
EN

Stack Overflow用户
提问于 2022-08-05 20:09:02
回答 1查看 77关注 0票数 -2

“UNIX编程环境”( the UNIX Programming Environment)一书第32页对UNIX管道作了深刻的说明:

管道中的程序实际上是同时运行的,而不是一个接一个地运行。这意味着管道中的程序可以是交互式的;内核负责所有的调度和同步工作。

哇!通过使用管道,我可以免费获得并行处理!

“我必须向我的同事们展示这种超棒的能力。”我想。我将实现一个演示:创建一个简单的交互式工具来模拟我在命令窗口输入的内容。我将把这个工具命名为mimic。使用管道将其连接到另一个工具,该工具计算行数和字符数。我将把这个工具命名为wc (遗憾的是,我正在使用wc,它没有UNIX程序,所以我必须实现自己的程序)。这两个工具将在这样的命令行上运行:

代码语言:javascript
复制
mimic | wc

mimic的输出通过管道传输到wc

好吧,就是这个主意。

我用一个非常简单的Flex实现了mimic工具,如下所示。编译生成的mimic.exe

当我从命令行运行mimic.exe时,它确实模仿了我输入的内容:

代码语言:javascript
复制
> mimic
hello world
hello world
greetings
greetings
ctrl-c

我使用AWK实现了wc。AWK程序(wc.awk)如下所示。当我从命令行运行wc时,它确实会计算行数和字符数:

代码语言:javascript
复制
> echo Hello World | awk -f wc.awk
lines 1     chars 13

然而,当我把它们和管子放在一起时,它们并不像我想象的那样工作。下面是一个示例运行:

代码语言:javascript
复制
> mimic | awk -f wc.awk
hello world
greetings
ctrl-c

嗯,没什么。不要模仿。线不算。没有点点滴滴。

怎么不管用了?我做错什么了,拜托?

我能做些什么来使它如我所期望的那样运作呢?我希望它能像这样工作:我在命令行中输入了一些内容。mimic重复它,将其发送到管道,后者将其发送给wc,后者报告行数和字符数。我在命令行输入下一个内容。mimic重复它,将其发送到管道,后者将其发送给wc,后者报告行数和字符数。以此类推。这就是我认为我正在实施的行为。

这里是我的简单Flex (模拟):

代码语言:javascript
复制
%option noyywrap
%option always-interactive
%% 
%%
int main(int argc, char *argv[])
{ 
    yyin = stdin;
    yylex();
    return 0;
}

这里是我的简单AWK程序(wc):

代码语言:javascript
复制
    { nchars = nchars + length($0) + 1 }
END { printf("lines %-10d chars %d\n", NR, nchars) }
EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2022-08-05 23:10:38

这个问题与Flex、Bison、Awk几乎没有关系,实际上也与Unix无关(因为您正在试验Windows)。

我手头没有Windows,但根本的问题是stdio缓冲区,所以它在Unix上也是可以复制的。

为了简化起见,我只实现了mimic,这是我直接实现的,而不是使用Flex (这显然是过分的):

代码语言:javascript
复制
#include <stdio.h>
int main(void) {
  for (int ch; (ch = getchar()) != EOF; ) putchar(ch);
  return 0;
}

由于使用了%always-interactive (这迫使Flex每次使用fgetc()读取一个字符),所以除了将一个字节的fwrite简化为等效的putchar之外,它基本上具有与程序相同的标准库调用序列。

当然,它具有同样的执行特性:

代码语言:javascript
复制
$ ./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出现在控制台之外):

代码语言:javascript
复制
$ ./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,所以我可能希望能够将其导入自身并获得相同的结果。但是输出有一点不同:

代码语言:javascript
复制
$ ./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终止程序,我根本就得不到任何输出:

代码语言:javascript
复制
$ ./mimic | ./mimic
Here we go round the mulberry bush,
the mulberry bush, the mulberry bush.
^C

好的,这就是数据。现在我们需要一个解释。这一解释与C标准库流的缓冲方式有关,您可以在setvbuf手册(以及web上的许多地方)中看到这些内容。我将总结如下:

  • C标准指定所有输入函数执行“好像”是通过对fgetc的重复调用来实现的,所有输出函数“似乎”都是通过对fputc的重复调用来实现的。请注意,这意味着单个printffwrite编写的字节块没有什么特殊之处。库没有做任何事情来确保调用序列是原子的。如果有两个进程同时写入stdout,则消息可以交织在一起,这将不时发生.

  • --由fputc编写的数据(因此,所有stdio输出函数)实际上被放置在输出缓冲区中。此缓冲区不是操作系统的一部分(操作系统可能会添加另一层缓冲)。它严格地说是stdio库函数的一部分,这些函数是普通的userland函数。你可以自己写(这是一个很好的练习)。该缓冲区的内容不时被发送到相应的操作系统接口,以便传输到输出设备。

  • “不时”是故意不具体的。有三种标准缓冲模式,尽管标准没有要求特定的库实现都使用它们,它也不限制库实现使用不同的缓冲模式(尽管这三种指定的模式基本上涵盖了有用的可能性)。但是,您可能使用的大多数C库实现都实现了所有这三个,正如标准中所描述的那样。因此,将下面的内容作为对一种常见实现技术的描述,但是要注意,在特定的特殊平台上,它可能并不准确。

三种缓冲模式是:

代码语言:javascript
复制
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.
代码语言:javascript
复制
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.)
代码语言:javascript
复制
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发送任何东西,直到当stdoutmain返回隐式关闭时,缓冲区被刷新。

此外,如果我杀死进程(例如,通过输入Ctrl,或者通过向它发送SIGKILL信号),那么输出缓冲区永远不会发送到操作系统,控制台上什么也不会出现。

如果您正在使用标准C库调用编写控制台应用程序,了解stdio输出缓冲如何影响您看到的输出序列是非常重要的。Windows上的情况和Unix上的情况一样。(当然,如果使用本机Windows或Posix I/O接口,则不适用。)

票数 4
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/73254838

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档