【问题标题】:Piping as interprocess communication管道作为进程间通信
【发布时间】:2010-09-26 16:00:35
【问题描述】:

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

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

【问题讨论】:

    标签: unix piping


    【解决方案1】:

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

    似乎接收端会 阻止等待输入,我会 期待

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

    发送端有时会阻塞吗 等待有人从 流

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

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

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

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

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

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

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

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

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

    管道的行为是否取决于 在我正在使用的外壳或我的方式上 配置了吗?

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

    我还有其他问题吗? 问一下

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

    【讨论】:

    • 使用 stat() 查看管道的内容在所有平台上并不可靠。
    • 如果管道缓冲区已满,写入端可能会阻塞 - 它不是很大。
    • 跨机管道...不存在?最接近的方法可能是套接字,但这与管道不同。
    • 命名管道和未命名管道都是单向的。我不确定“读取和写入线程的“输入”和“输出”流是什么意思,但“线程”很奇怪。
    【解决方案2】:

    这一切都基于类 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。]

    【讨论】:

    • 问题1:不能将EOF写入文件或管道;您关闭文件或管道以指示 EOF(或者,对于文件,您可能会截断它)。 Control-D 不是 EOF,除非在终端的上下文中。
    • 问题 2:在有接收者之前不能写入命名管道,即使使用 O_NONBLOCK 打开也是如此。
    • 问题 3:在大多数系统上,管道缓冲区的大小非常小,例如 4096 或 5120 字节。填充它并不需要大量的输出。
    • 在 Unix 上,当管道已满时写入块。
    • Jonathan,回复:1,好吧,你告诉我他所说的“将 EOF 写入管道”是什么意思。 re: 3、一些unices动态分配管道缓冲区大小,或者参数化;它可以很大。
    【解决方案3】:

    正如 Dashogun 和 Charlie Martin 所说,这是一个大问题。他们的回答有些地方不准确,所以我也来回答一下。

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

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

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

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

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

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

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

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

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

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

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

    命名管道和未命名管道的行为有区别吗?

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

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

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

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

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

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

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

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

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


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

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

    【讨论】:

    • CentOS - fcntl(F_SETFL) 失败; Ubuntu - 只是在 ...65500 之后阻塞。
    • 谢谢,J F 塞巴斯蒂安。这也是我在 SuSE 中看到的。推断 65536 的限制。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-09-10
    • 1970-01-01
    • 2013-04-01
    • 2016-04-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多