关于管道如何在 Bash 中工作的简单解释是啥?

Posted

技术标签:

【中文标题】关于管道如何在 Bash 中工作的简单解释是啥?【英文标题】:What is a simple explanation for how pipes work in Bash?关于管道如何在 Bash 中工作的简单解释是什么? 【发布时间】:2012-04-07 17:04:06 【问题描述】:

我经常在 Bash 中使用管道,例如:

dmesg | less

虽然我知道这个输出是什么,但它需要dmesg 并让我用less 滚动浏览它,我不明白| 在做什么。是不是和>正好相反?

对于| 的作用是否有简单或隐喻的解释? 在一条管线中使用多个管道时会发生什么情况? 在 Bash 脚本中出现的所有管道的行为是否一致?

【问题讨论】:

【参考方案1】:

关于管道的效率问题:

命令可以在之前的管道命令完成之前访问和处理其输入的数据,这意味着如果资源可用,计算能力利用效率。 管道不需要在下一个命令访问其输入之前将命令的输出保存到文件(两个命令之间没有 I/O 操作),这意味着减少了昂贵的 I/O 操作和磁盘空间效率。

【讨论】:

【参考方案2】:

简而言之,如上所述,需要注意三个关键的“特殊”文件描述符。 shell默认发送键盘到stdin,发送stdoutstderr到屏幕:

管道只是一种方便的外壳,它将一个进程的stdout 直接附加到下一个进程的stdin

它的工作原理有很多微妙之处,例如,stderr 流可能不会像您期望的那样通过管道传输,如下所示:

我花了相当长的时间尝试写一个详细但对初学者友好的 Bash 管道解释。完整内容在:

https://effective-shell.com/docs/part-2-core-skills/7-thinking-in-pipelines/

【讨论】:

【参考方案3】:

所有这些答案都很棒。我想提一下,bash 中的管道(与 unix/linux 或 windows 命名管道具有相同的概念)就像现实生活中的管道一样。 如果您将管道之前的程序视为水源,将管道视为水管,将管道之后的程序视为使用水的东西(程序输出为水),那么您就非常了解管道工作。 请记住,管道中的所有应用都是并行运行的。

【讨论】:

【参考方案4】:

在 Linux(和一般的 Unix)中,每个进程都有三个默认文件描述符:

    fd #0 代表进程的标准输入 fd #1 代表进程的标准输出 fd #2 表示进程的标准错误输出

通常,当您运行一个简单的程序时,这些文件描述符默认配置如下:

    从键盘读取默认输入 标准输出配置为监视器 标准错误也被配置为监视器

Bash 提供了几个操作符来改变这种行为(例如,看看 >、>> 和 合作时,一个程序使用另一个程序的输出作为其输入。为了使这种协作变得容易,Bash 提供了管道操作员|。请注意使用协作而不是链接。我避免使用这个术语,因为实际上管道不是连续的。带有管道的普通命令行具有以下方面:

    > program_1 | program_2 | ... | program_n

上面的命令行有点误导:用户可能认为一旦 program_1 完成执行,program_2 就会得到它的输入,这是不正确的。事实上,bash 所做的是并行启动 ALL 程序并相应地配置输入输出,以便每个程序从前一个程序获取其输入并将其输出传递给下一个程序(在命令中行建立顺序)。

以下是来自Creating pipe in C 的一个简单示例,用于在父进程和子进程之间创建管道。重要的部分是对 pipe() 的调用以及父级如何关闭 fd1(写入端)以及子级如何关闭 fd1(写入端)。请注意,管道是单向通信通道。因此,数据只能沿一个方向流动:fd1 流向 fd[0]。有关更多信息,请查看 pipe() 的手册页。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(void)

    int     fd[2], nbytes;
    pid_t   childpid;
    char    string[] = "Hello, world!\n";
    char    readbuffer[80];

    pipe(fd);

    if((childpid = fork()) == -1)
    
            perror("fork");
            exit(1);
    

    if(childpid == 0)
    
            /* Child process closes up input side of pipe */
            close(fd[0]);

            /* Send "string" through the output side of pipe */
            write(fd[1], string, (strlen(string)+1));
            exit(0);
    
    else
    
            /* Parent process closes up output side of pipe */
            close(fd[1]);

            /* Read in a string from the pipe */
            nbytes = read(fd[0], readbuffer, sizeof(readbuffer));
            printf("Received string: %s", readbuffer);
    

    return(0);

最后但并非最不重要的一点是,当您在表单中有命令行时:

> program_1 | program_2 | program_3

整行的返回码设置为last命令。在这种情况下,program_3。如果您想获得中间返回码,您必须设置 pipefail 或从 PIPESTATUS 中获取。

【讨论】:

【参考方案5】:

这样的管道非常简单。

你有一个命令的输出。您可以将此输出作为输入提供给使用管道的另一个命令。您可以通过管道传输任意数量的命令。

例如: ls | grep 我的 | grep 文件

这首先列出了工作目录中的文件。此输出由 grep 命令检查单词“my”。这个输出现在进入第二个 grep 命令,它最终搜索单词“files”。而已。

【讨论】:

【参考方案6】:

如果您将每个 unix 命令视为独立模块, 但您需要他们使用文本作为一致界面相互交谈, 如何做呢?

cmd                       input                    output

echo "foobar"             string                   "foobar" 
cat "somefile.txt"        file                     *string inside the file*
grep "pattern" "a.txt"    pattern, input file      *matched string*

你可以说|是在接力马拉松中传递接力棒的隐喻。 它的形状甚至像一个!cat -&gt; echo -&gt; less -&gt; awk -&gt; perl 类似于cat | echo | less | awk | perl

cat "somefile.txt" | echocat 将其输出传递给echo 使用。

当有多个输入时会发生什么?cat "somefile.txt" | grep "pattern" 对于grep,有一条隐含规则说“将其作为输入文件而不是模式传递”。 你会慢慢培养出通过经验知道哪个参数是哪个参数的眼睛。

【讨论】:

"对于 grep,有一条隐含的规则说“将其作为输入文件而不是模式传递”。是我一直在寻找的东西。我在哪里可以找到这方面的文档?【参考方案7】:

管道运算符获取第一个命令的输出,并通过连接 stdin 和 stdout 将其“管道”到第二个命令。 在您的示例中,不是 dmesg 命令的输出进入标准输出(并将其扔到控制台上),而是直接进入您的下一个命令。

【讨论】:

管道不将输出作为参数传递。管道将 STDOUT 连接到 STDIN。某些命令必须被明确指示查看 STDIN(通常通过给出连字符而不是文件名)才能在管道中使用。 重要的是要注意它也会流式传输。右边的进程不需要等待左边的进程完成就可以开始工作了。所以像yes | rm -r * 这样的东西作为rm -rf * 的替代品也可以工作,尽管yes 永远不会完成执行【参考方案8】:

Unix 中的每个标准进程都至少有三个文件描述符,它们有点像接口

标准输出,这是进程打印其数据的地方(大部分时间是控制台,即您的屏幕或终端)。 标准输入,这是它从中获取数据的地方(大多数情况下,它可能类似于您的键盘)。 标准错误,这是错误和有时其他带外数据的地方。现在它并不有趣,因为管道通常不会处理它。

管道连接左边进程的标准输出和右边进程的标准输入。您可以将其视为一个专用程序,负责复制一个程序打印的所有内容,并将其提供给下一个程序(管道符号后面的那个)。不完全是这样,但这是一个足够充分的类比。

每个管道都在两个方面进行操作:来自其左侧的标准输出和预期在其右侧的输入流。它们中的每一个都可以附加到单个进程或管道的另一位,多管道命令行中就是这种情况。但这与管道的实际操作无关;每个管道都有自己的。

重定向操作符 (&gt;) 做了一些相关的事情,但更简单:默认情况下,它将进程的标准输出直接发送到文件。如您所见,它不是管道的对立面,而是实际上是互补的。 &gt; 的反面是 &lt;,它获取文件的内容并将其发送到进程的标准输入(将其视为一个逐字节读取文件并在进程中键入的程序)你)。

【讨论】:

【参考方案9】:

|将左侧命令的STDOUT放到右侧命令的STDIN中。

如果您使用多个管道,它只是一个管道链。第一个命令输出设置为第二个命令输入。第二个命令输出设置为下一个命令输入。以此类推。

它在所有基于 Linux/widows 的命令解释器中都可用。

【讨论】:

【参考方案10】:

管道获取进程的输出,输出是指标准输出(UNIX 上的stdout)并将其传递到另一个进程的标准输入(stdin)。它与简单的右重定向 &gt; 并不相反,其目的是将输出重定向到另一个输出。

例如,在 Linux 上使用 echo 命令,它只是在标准输出上打印传入参数的字符串。如果您使用简单的重定向,例如:

echo "Hello world" > helloworld.txt

shell 将重定向最初打算在标准输出上的正常输出并将其直接打印到文件helloworld.txt 中。

现在,以这个涉及管道的例子为例:

ls -l | grep helloworld.txt

ls命令的标准输出会在grep的入口处输出,那么这是如何工作的呢?

诸如grep 之类的程序在没有任何参数的情况下使用时只是在读取并等待在其标准输入(stdin) 上传递某些内容。当他们捕捉到某些东西时,例如 ls 命令的输出,grep 会通过查找您正在搜索的内容的出现来正常运行。

【讨论】:

【参考方案11】:

Unix 管道将第一个进程的 STDOUT(标准输出)文件描述符连接到第二个进程的 STDIN(标准输入)。然后发生的情况是,当第一个进程写入其 STDOUT 时,第二个进程可以立即(从 STDIN)读取该输出。

使用多个管道与使用单个管道没有什么不同。每个管道都是独立的,只是简单地链接了相邻进程的 STDOUT 和 STDIN。

您的第三个问题有点模棱两可。是的,管道,因此,在 bash 脚本中的任何地方都是一致的。但是,管道字符| 可以代表不同的事物。双管道(||),例如表示“或”运算符。

【讨论】:

注意“立即”这个词!我指出这一点是因为我们使用 Bash 编写临时脚本的人倾向于认为我们的命令是同步的,我们的脚本是完全顺序的。我们希望管道执行左侧命令,并将其输出传递给以下命令。但是管道使用分叉,并且这些命令实际上是并行执行的。对于许多命令,这个事实在功能上是无关紧要的,但是sometimes it matters。例如,查看以下输出:ps | cat 连接本身是如何实现的?我可以编写一个程序,读取一个程序的 STDOUT,然后将其写入另一个程序的 STDIN,并带有缓冲区,所以管道基本上是在 shell 中实现的?

以上是关于关于管道如何在 Bash 中工作的简单解释是啥?的主要内容,如果未能解决你的问题,请参考以下文章

关于库如何在 C 中工作的问题

关于 sprintf 函数如何在 C 中工作的困惑

关于 connection.end() 如何在 node.js mySQL 模块中工作的困惑

通过慢速网络连接在 git 中工作的最快方法是啥?

unicode 大写在 .NET 6 中工作的先决条件是啥?

为啥在超级终端中工作的永久别名在 vs-code bash 终端中不起作用?