9

我有兴趣编写单独的程序模块,这些模块作为独立线程运行,我可以与管道挂钩。动机是我可以完全独立地编写和测试每个模块,甚至可以用不同的语言编写它们,或者在不同的机器上运行不同的模块。这里有各种各样的可能性。我已经使用管道一段时间了,但我不熟悉其行为的细微差别。

  • 似乎接收端会阻塞等待输入,这是我所期望的,但是发送端有时会阻塞等待某人从流中读取吗?
  • 如果我将 eof 写入流,我可以继续写入该流直到我关闭它吗?
  • 命名管道和未命名管道的行为是否存在差异?
  • 我首先用命名管道打开管道的哪一端有关系吗?
  • 不同 Linux 系统之间管道的行为是否一致?
  • 管道的行为是否取决于我使用的 shell 或我配置它的方式?
  • 如果我想以这种方式使用管道,是否还有其他问题我应该问或我应该注意的问题?
4

3 回答 3

4

哇,这是很多问题。让我们看看我是否可以涵盖所有内容...

似乎接收端会阻止等待输入,这是我所期望的

您正确地期望实际的“读取”调用将阻塞,直到出现某些东西。但是,我相信有一些 C 函数可以让您“窥视”管道中等待的内容(和数量)。不幸的是,我不记得这是否也阻止了。

发送端是否有时会等待某人从流中读取

不,发送永远不应该阻塞。如果这是通过网络连接到另一台计算机的管道,请考虑一下后果。您是否要等待(通过可能的高延迟)另一台计算机响应它收到它?现在,如果目标的读取器句柄已关闭,则情况不同。在这种情况下,您应该进行一些错误检查来处理它。

如果我将 eof 写入流,我可以继续写入该流直到我关闭它

我认为这取决于您使用的语言及其管道的实现。在 C 中,我会说不。在 linux shell 中,我会说是的。其他有更多经验的人必须回答这个问题。

命名管道和未命名管道的行为是否存在差异?据我所知,是的。但是,我在命名与未命名方面没有太多经验。我相信区别在于:

  • 单向与双向通信
  • 读取和写入线程的“输入”和“输出”流

我首先用命名管道打开管道的哪一端有关系吗?

通常不会,但您可能会在尝试创建线程并将线程彼此链接时遇到初始化问题。您需要有一个主线程来创建所有子线程并将它们各自的管道相互同步。

不同Linux系统之间管道的行为是否一致?

同样,这取决于什么语言,但通常是的。听说过 POSIX 吗?这是标准(至少对于 linux,Windows 是自己做的)。

管道的行为是否取决于我使用的 shell 或我配置它的方式?

这进入了一个灰色地带。答案应该是否定的,因为 shell 本质上应该是进行系统调用。然而,在那之前的一切都是可以争夺的。

还有其他问题我应该问吗

您提出的问题表明您对该系统有相当的了解。继续研究并专注于您将在哪个级别上工作(shell、C 等)。不过,只要尝试一下,您就会学到更多。

于 2008-12-12T17:11:19.330 回答
4

这一切都基于类 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 或我配置它的方式?

不。当你创建一个管道时,在幕后发生的是你的父进程(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。]

于 2008-12-12T17:16:19.790 回答
4

正如 Dashogun 和 Charlie Martin 所指出的,这是一个大问题。他们的某些部分答案不准确,所以我也将回答。

我有兴趣编写单独的程序模块,这些模块作为独立线程运行,我可以与管道挂钩。

小心尝试使用管道作为单个进程的线程之间的通信机制。因为您将在单个进程中同时打开管道的读取和写入端,所以您永远不会得到 EOF(零字节)指示。

如果您真的指的是进程,那么这就是经典 Unix 构建工具的方法的基础。许多标准的 Unix 程序都是过滤器,它们从标准输入中读取,以某种方式对其进行转换,然后将结果写入标准输出。例如,trsortgrepcat都是过滤器,仅举几例。当您操作的数据允许时,这是一个很好的范例。并非所有的数据操作都有助于这种方法,但有很多。

动机是我可以完全独立地编写和测试每个模块,甚至可以用不同的语言编写它们,或者在不同的机器上运行不同的模块。

好点。请注意,机器之间并没有真正的管道机制,尽管您可以使用诸如rshor (better)之类的程序来接近它ssh。然而,在内部,这些程序可能会从管道读取本地数据并将该数据发送到远程机器,但它们通过套接字在机器之间进行通信,而不是使用管道。

这里有各种各样的可能性。我已经使用管道一段时间了,但我不熟悉其行为的细微差别。

好的; 提问是一种(好的)学习方式。当然,实验是另一回事。

似乎接收端会阻塞等待输入,这是我所期望的,但是发送端有时会阻塞等待某人从流中读取吗?

是的。管道缓冲区的大小是有限制的。传统上,这非常小 - 4096 或 5120 是常见值。您可能会发现现代 Linux 使用了更大的值。您可以使用fpathconf()_PC_PIPE_BUF 来找出管道缓冲区的大小。POSIX 只要求缓冲区为 512(即 _POSIX_PIPE_BUF 为 512)。

如果我将 eof 写入流,我可以继续写入该流直到我关闭它吗?

从技术上讲,没有办法将 EOF 写入流;您关闭管道描述符以指示 EOF。如果您将 control-D 或 control-Z 视为 EOF 字符,那么就管道而言,它们只是常规字符 - 它们仅在以规范模式运行的终端(熟,或正常)。

命名管道和未命名管道的行为是否存在差异?

是的,没有。最大的区别是未命名管道必须由一个进程设置,并且只能由该进程和作为共同祖先共享该进程的子进程使用。相比之下,命名管道可以由以前未关联的进程使用。下一个重大差异是第一个的结果。使用未命名管道,您可以从单个函数(系统)调用返回两个文件描述符,但您使用常规函数pipe()打开 FIFO 或命名管道。open()(在打开它之前,必须有人使用调用创建一个 FIFO mkfifo();未命名管道不需要任何此类事先设置。)但是,一旦您打开了文件描述符,命名管道和未命名管道之间几乎没有区别。

我首先用命名管道打开管道的哪一端有关系吗?

不,第一个打开 FIFO 的进程(通常)会阻塞,直到有一个进程打开另一端。如果您打开它进行读写(非常规但可能),那么您将不会被阻止;如果您使用 O_NONBLOCK 标志,您将不会被阻止。

不同 Linux 系统之间管道的行为是否一致?

是的。在我使用过的任何系统上,我都没有听说过或遇到过任何管道问题。

管道的行为是否取决于我使用的 shell 或我配置它的方式?

否:管道和 FIFO 独立于您使用的外壳。

如果我想以这种方式使用管道,是否还有其他问题我应该问或我应该注意的问题?

请记住,您必须在将要写入的进程中关闭管道的读取端,并在将要读取的进程中关闭管道的写入端。如果要通过管道进行双向通信,请使用两个单独的管道。如果您创建复杂的管道布置,请注意死锁 - 这是可能的。然而,线性管道不会死锁(尽管如果第一个进程从不关闭其输出,则下游进程可能会无限期地等待)。


我在上面和对其他答案的评论中都观察到管道缓冲区通常仅限于非常小的尺寸。@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 - 索拉里斯 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 在某些平台上与管道一起工作,而在其他平台上则不工作。

该程序创建一个管道和分叉。孩子关闭管道的写入端,然后进入睡眠状态,直到它收到信号——这就是 pause() 所做的。然后父级关闭管道的读取端,并在写入描述符上设置标志,这样它就不会在尝试在完整管道上写入时阻塞。然后它循环,一次写一个字符,为每个写的字符打印一个点,每 50 个字符打印一个计数和换行符。当它检测到写入问题(缓冲区已满,因为孩子没有读东西)时,它会停止循环,写入最终计数并杀死孩子。

于 2008-12-13T07:31:41.560 回答