题
我有兴趣编写单独的程序模块,这些模块作为独立的线程运行,我可以将它们与管道连接在一起。动机是我可以完全独立地编写和测试每个模块,甚至可能用不同的语言编写它们,或者在不同的机器上运行不同的模块。这里有各种各样的可能性。我已经使用管道一段时间了,但我不熟悉其行为的细微差别。
- 接收端似乎会阻塞等待输入,这是我所期望的,但是发送端有时会阻塞等待某人从流中读取数据吗?
- 如果我向流写入 eof,我可以继续写入该流,直到关闭它吗?
- 命名管道和未命名管道的行为有区别吗?
- 我首先使用命名管道打开管道的哪一端有关系吗?
- 不同 Linux 系统之间管道的行为是否一致?
- 管道的行为是否取决于我正在使用的外壳或我配置它的方式?
- 如果我想以这种方式使用管道,还有其他我应该问的问题或我应该注意的问题吗?
解决方案
哇,有很多问题。让我们看看我是否能涵盖所有内容......
似乎接收端将 块等待输入,而我 预想
您正确地期望实际的“读取”调用将被阻塞,直到有东西出现为止。然而,我相信有一些 C 函数可以让您“窥视”管道中等待的内容(以及数量)。不幸的是,我不记得这是否也被阻止。
有时发送端是否会阻塞 等待有人宣读 溪流
不,发送永远不应该被阻止。想象一下如果这是通过网络连接到另一台计算机的管道,会产生什么后果。您是否想要等待(可能存在高延迟)另一台计算机响应它收到的消息?现在,如果目标的读取器句柄已关闭,则情况会有所不同。在这种情况下,您应该进行一些错误检查来处理该问题。
如果我向数据流写入一个 eof,我能否 络绎不绝 直到我关闭
我认为这取决于您使用的语言及其管道的实现。在C语言中,我会说不。在 Linux shell 中,我会说是的。应该由更有经验的人来回答这个问题。
在行为上是否存在差异 命名和未命名的管道?据我所知,是的。但是,我对命名与未命名没有太多经验。我相信区别在于:
- 单向与双向通信
- 读取和写入线程的“输入”和“输出”流
管道的哪一端与我 先用命名管道打开?
通常不会,但是在尝试创建线程并将线程相互链接的初始化时可能会遇到问题。您需要有一个主线程来创建所有子线程并使它们各自的管道彼此同步。
管道的行为是否一致 不同的 linux 系统之间?
同样,这取决于什么语言,但通常是的。听说过 POSIX 吗?这就是标准(至少对于linux来说,Windows有自己的事情)。
管道的性能是否取决于 在我使用的外壳上,或者在我 配置好了吗?
这正进入一个灰色地带。答案 应该 否,因为 shell 本质上应该进行系统调用。然而,在那之前的一切都是有待争夺的。
还有其他问题吗? 要问
您提出的问题表明您对系统有一定的了解。继续研究并关注您将要从事的级别(shell、C 等)。不过,只要尝试一下,你就会学到更多东西。
其他提示
这一切都是基于类UNIX系统;我不熟悉最新版本的 Windows 的具体行为。
接收端似乎会阻塞等待输入,这是我所期望的,但是发送端有时会阻塞等待某人从流中读取数据吗?
是的,尽管在现代机器上这种情况可能不会经常发生。该管道有一个中间缓冲区 能 可能会被填满。如果是这样,管道的写入端确实会阻塞。但如果您考虑一下,就会发现并没有很多文件大到足以冒这种风险。
如果我向流写入 eof,我可以继续写入该流,直到关闭它吗?
嗯,你的意思是像 CTRL-D,0x04?当然,只要流是这样设置的。即。
506 # cat | od -c
abc
^D
efg
0000000 a b c \n 004 \n e f g \n
0000012
命名管道和未命名管道的行为有区别吗?
是的,但它们很微妙并且依赖于实现。最重要的是,您可以在另一端运行之前向命名管道写入数据;使用未命名管道,文件描述符在 fork/exec 进程期间被共享,因此在进程未启动的情况下无法访问瞬态缓冲区。
我首先使用命名管道打开管道的哪一端有关系吗?
没有。
不同 Linux 系统之间管道的行为是否一致?
在合理范围内,是的。缓冲区大小等可能会有所不同。
管道的行为是否取决于我正在使用的外壳或我配置它的方式?
不。当你创建一个管道时,在幕后发生的事情是你的父进程(shell)创建一个具有一对文件描述符的管道,然后像下面的伪代码一样执行 fork exec:
家长:
create pipe, returning two file descriptors, call them fd[0] and fd[1]
fork write-side process
fork read-side process
写端:
close fd[0]
connect fd[1] to stdout
exec writer program
读端:
close fd[1]
connect fd[0] to stdin
exec reader program
如果我想以这种方式使用管道,还有其他我应该问的问题或我应该注意的问题吗?
你想做的所有事情真的都会像这样排成一行吗?如果没有,您可能需要考虑更通用的架构。但是,希望有许多单独的进程通过管道的“狭窄”界面进行交互,这是一个很好的见解。
[更新:我一开始将文件描述符索引颠倒了。他们现在是正确的,看看 man 2 pipe
.]
正如 Dashogun 和 Charlie Martin 指出的那样,这是一个大问题。他们的回答有些不准确,所以我也来回答一下。
我有兴趣编写单独的程序模块,这些模块作为独立的线程运行,我可以将它们与管道连接在一起。
尝试使用管道作为单个进程的线程之间的通信机制时要小心。因为您将在单个进程中打开管道的读取和写入端,所以您永远不会得到 EOF(零字节)指示。
如果您确实指的是进程,那么这就是构建工具的经典 Unix 方法的基础。许多标准 Unix 程序都是过滤器,它们从标准输入中读取数据,以某种方式对其进行转换,然后将结果写入标准输出。例如, tr
, sort
, grep
, , 和 cat
都是过滤器,仅举几例。当您操作的数据允许时,这是一个很好的范例。并非所有数据操作都有利于这种方法,但有很多数据操作都有利于这种方法。
动机是我可以完全独立地编写和测试每个模块,甚至可能用不同的语言编写它们,或者在不同的机器上运行不同的模块。
好点。请注意,机器之间并不存在真正的管道机制,尽管您可以使用诸如以下的程序来接近它 rsh
或更好) ssh
. 。然而,在内部,此类程序可以从管道读取本地数据并将该数据发送到远程计算机,但它们通过套接字在计算机之间进行通信,而不是使用管道。
这里有各种各样的可能性。我已经使用管道一段时间了,但我不熟悉其行为的细微差别。
好的;提出问题是一种(好的)学习方式。当然,实验是另一回事。
接收端似乎会阻塞等待输入,这是我所期望的,但是发送端有时会阻塞等待某人从流中读取数据吗?
是的。管道缓冲区的大小有限制。传统上,这个值相当小 - 4096 或 5120 是常见值。您可能会发现现代 Linux 使用更大的值。您可以使用 fpathconf()
和 _PC_PIPE_BUF 来找出管道缓冲区的大小。POSIX仅要求缓冲区为512(即_POSIX_PIPE_BUF为512)。
如果我向流写入 eof,我可以继续写入该流,直到关闭它吗?
从技术上讲,没有办法将 EOF 写入流;关闭管道描述符以指示 EOF。如果您将 control-D 或 control-Z 视为 EOF 字符,那么就管道而言,它们只是常规字符 - 它们仅在以规范模式运行的终端上键入时才具有类似于 EOF 的效果(熟的) ,或正常)。
命名管道和未命名管道的行为有区别吗?
是的,也不是。最大的区别是无名管道必须由一个进程设置,并且只能由该进程以及共享该进程作为共同祖先的子进程使用。相比之下,命名管道可以由以前不关联的进程使用。下一个重大差异是第一个差异的结果。使用无名管道,您可以从单个函数(系统)调用中获取两个文件描述符 pipe()
, ,但是您使用常规打开 FIFO 或命名管道 open()
功能。(必须有人创建一个 FIFO mkfifo()
在打开之前致电;无名管道不需要任何此类预先设置。)但是,一旦打开文件描述符,命名管道和无名管道之间就几乎没有什么区别。
我首先使用命名管道打开管道的哪一端有关系吗?
不。第一个打开 FIFO 的进程(通常)会阻塞,直到有另一端打开的进程为止。如果您打开它进行读取和写入(常规但可能),那么您将不会被阻止;如果您使用 O_NONBLOCK 标志,则不会被阻止。
不同 Linux 系统之间管道的行为是否一致?
是的。我没有听说过或经历过任何我使用过的系统上的管道问题。
管道的行为是否取决于我正在使用的外壳或我配置它的方式?
不:管道和 FIFO 独立于您使用的 shell。
如果我想以这种方式使用管道,还有其他我应该问的问题或我应该注意的问题吗?
请记住,您必须在将要写入的进程中关闭管道的读取端,并在将要读取的进程中关闭管道的写入端。如果您希望通过管道进行双向通信,请使用两个单独的管道。如果您创建复杂的管道布置,请小心死锁 - 这是可能的。然而,线性管道不会死锁(尽管如果第一个进程从未关闭其输出,下游进程可能会无限期地等待)。
我在上面和对其他答案的评论中都观察到,管道缓冲区通常仅限于相当小的尺寸。@Charlie Martin 反驳说,某些版本的 Unix 有动态管道缓冲区,而且这些缓冲区可能非常大。
我不确定他想到的是哪一个。我在 Solaris、AIX、HP-UX、MacOS X、Linux 和 Cygwin / Windows XP 上使用了以下测试程序(结果如下):
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
static const char *arg0;
static void err_syserr(char *str)
{
int errnum = errno;
fprintf(stderr, "%s: %s - (%d) %s\n", arg0, str, errnum, strerror(errnum));
exit(1);
}
int main(int argc, char **argv)
{
int pd[2];
pid_t kid;
size_t i = 0;
char buffer[2] = "a";
int flags;
arg0 = argv[0];
if (pipe(pd) != 0)
err_syserr("pipe() failed");
if ((kid = fork()) < 0)
err_syserr("fork() failed");
else if (kid == 0)
{
close(pd[1]);
pause();
}
/* else */
close(pd[0]);
if (fcntl(pd[1], F_GETFL, &flags) == -1)
err_syserr("fcntl(F_GETFL) failed");
flags |= O_NONBLOCK;
if (fcntl(pd[1], F_SETFL, &flags) == -1)
err_syserr("fcntl(F_SETFL) failed");
while (write(pd[1], buffer, sizeof(buffer)-1) == sizeof(buffer)-1)
{
putchar('.');
if (++i % 50 == 0)
printf("%u\n", (unsigned)i);
}
if (i % 50 != 0)
printf("%u\n", (unsigned)i);
kill(kid, SIGINT);
return 0;
}
我很想从其他平台获得额外的结果。这是我找到的尺寸。我必须承认,所有结果都比我预期的要大,但是当谈到缓冲区大小时,查理和我可能正在争论“相当大”的含义。
- 8196 - IA-64 的 HP-UX 11.23(fcntl(F_SETFL) 失败)
- 16384 - Solaris 10
- 16384 - MacOS X 10.5(O_NONBLOCK 不起作用,但 fcntl(F_SETFL) 没有失败)
- 32768-AIX 5.3
- 65536 - Cygwin / Windows XP(O_NONBLOCK 不起作用,但 fcntl(F_SETFL) 没有失败)
- 65536 - SuSE Linux 10(和 CentOS)(fcntl(F_SETFL) 失败)
从这些测试中可以清楚地看出一点,O_NONBLOCK 在某些平台上适用于管道,而在其他平台上则不适用。
该程序创建一个管道和分叉。子进程关闭管道的写入端,然后进入睡眠状态,直到收到信号 - 这就是暂停()的作用。然后,父级关闭管道的读取端,并在写入描述符上设置标志,以便在尝试写入完整管道时不会阻塞。然后它会循环,一次写入一个字符,并为每个写入的字符打印一个点,并每 50 个字符打印一个计数和换行符。当它检测到写入问题时(缓冲区已满,因为子进程没有读取任何内容),它会停止循环,写入最终计数,然后杀死子进程。