<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>管道 · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/%E7%AE%A1%E9%81%93/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/%E7%AE%A1%E9%81%93/index.xml" rel="self" type="application/rss+xml"/><item><title>管道让进程彼此流式连接</title><link>https://www.xbcatm.com/zh/posts/shell-pipes-stream-between-processes/</link><pubDate>Tue, 28 Sep 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/shell-pipes-stream-between-processes/</guid><description>管道把前一个命令的标准输出接到下一个的标准输入，数据随到随走，长生产者因此能与消费者并行推进。</description><content:encoded>&lt;p&gt;shell 管道把一个命令的标准输出连接到下一个命令的标准输入。各段命令可以并发运行，生产者不必全部完成后消费者才开始处理。这种连接支持增量处理，却不保证任一程序真的采用增量算法。用户态缓冲、提前结束以及各阶段的退出状态，都会影响整项操作是否达到预期。理解管道时，需要同时观察数据流、文件描述符生命周期和进程结果，不能只看最后一行输出。&lt;/p&gt;
&lt;h2 id="有限容量的字节流"&gt;有限容量的字节流&lt;/h2&gt;
&lt;p&gt;管道传输的是字节，不是自带边界的记录。读者不能假设每次读取恰好得到一行，也不能假设一次读取对应生产者的一次写入。需要消息边界的程序必须自己定义和解析。标准错误默认独立于管道连接，因此，上游诊断可能显示在终端，却不会进入下游过滤器。是否合并诊断和数据应当明确决定，避免错误文本污染本来供机器读取的内容。&lt;/p&gt;
&lt;p&gt;内核缓冲区容量有限。对于阻塞操作，管道满时写者等待；管道空但仍有写端打开时，读者等待。这种反压限制生产者在这个管道上领先消费者的距离。容量取决于操作系统和配置，并不是普遍固定为六十四千字节。程序自身仍可能维护不断增长的数据结构，因此，使用管道不等于整个处理链的内存已经有界，必须另外检查各段算法。&lt;/p&gt;
&lt;p&gt;用户态缓冲还可能引入额外等待。某个程序向终端逐行刷新，改成管道输出后却可能先积累较大的块。此时消费者看起来没有动静，并不代表内核丢了数据；数据可能尚未离开生产者的用户态缓冲。应先检查程序提供的流式处理和刷新控制，再考虑调整内核限制。完整排序等转换本身也需要先看到更多输入，添加随意的睡眠无法改变这种依赖。&lt;/p&gt;
&lt;p&gt;原子写入与缓冲容量是不同属性。Linux 对不超过 PIPE_BUF 的适当写入提供不与其他写者交错的保证，并有阻塞模式条件；这仍然不会为读者建立消息边界。几次短写入可能被一次读取取得，一次写入也可能分几次才读完，不能把某次观察到的块大小当作解析协议。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;只有所有指向写端的文件描述符都关闭后，读者才会看到文件结束。如果某个子进程继承了不再使用的写端，即使看起来生产者已经退出，消费者也可能一直等不到结束。反方向上，所有读端关闭后继续写入会产生 SIGPIPE；忽略这个信号时，写操作会以 EPIPE 错误失败。因此，消费者主动提前结束，也可能让原本正常的生产者以非零状态退出。&lt;/p&gt;
&lt;p&gt;Bash 默认返回最后一段命令的状态。上游失败、下游 cat 成功读完部分数据时，整体可能仍然成功。开启 pipefail 后，整体返回从左到右最靠右的非零状态，并不是时间上最先失败的进程，也不是最左侧失败命令。PIPESTATUS 数组短暂保留每段结果，应立即保存，因为后面一次赋值或打印就可能替换这份观察记录。&lt;/p&gt;
&lt;p&gt;Bash 管道中的阶段通常运行在子 shell 环境。在管道右侧循环里给变量赋值，通常不会修改调用者的变量。lastpipe 选项在关闭作业控制时可以改变最后一段的执行位置，因此依赖状态保留的代码必须明确 shell 配置。需要保留循环状态时，也可以让循环直接通过重定向读取输入，避免把它放进管道带来的作用域变化。&lt;/p&gt;
&lt;p&gt;前台 Bash 管道会等待各阶段结束，并不是消费者读到第一行就立即返回。如果生产者捕获断管错误后继续执行其他工作，整体完成时间仍会被拖延。因此，管道应被理解为多个具有独立结果、通过描述符连接的进程，不能假设它们总会像一个不可分割操作一样共同成功或共同停止。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;下面实验需要 Bash、cat、yes 和 head。它先用主动返回七的生产者比较默认状态和 pipefail，然后构造两段都失败的管道，最后检查提前关闭的消费者。仅在收集预期非零结果时关闭 errexit，取得结果后立即重新开启，再执行断言。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bash --noprofile --norc &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;BASH&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set -e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set +o pipefail
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set +e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;(printf &amp;#39;partial\n&amp;#39;; exit 7) | cat &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;plain=(&amp;#34;$?&amp;#34; &amp;#34;${PIPESTATUS[@]}&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set -e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ ${plain[*]} == &amp;#39;0 7 0&amp;#39; ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set -o pipefail
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set +e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;(printf &amp;#39;partial\n&amp;#39;; exit 7) | cat &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;strict=(&amp;#34;$?&amp;#34; &amp;#34;${PIPESTATUS[@]}&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set -e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ ${strict[*]} == &amp;#39;7 7 0&amp;#39; ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set +e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;(exit 7) | (exit 4)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;multiple=(&amp;#34;$?&amp;#34; &amp;#34;${PIPESTATUS[@]}&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set -e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ ${multiple[*]} == &amp;#39;4 7 4&amp;#39; ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set +e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;yes | head -n 1 &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;early=(&amp;#34;$?&amp;#34; &amp;#34;${PIPESTATUS[@]}&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set -e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ ${early[0]} -ne 0 &amp;amp;&amp;amp; ${early[1]} -ne 0 &amp;amp;&amp;amp; ${early[2]} -eq 0 ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;printf &amp;#39;default: %s\npipefail: %s\nrightmost: %s\nearly close: %s\n&amp;#39; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; &amp;#34;${plain[*]}&amp;#34; &amp;#34;${strict[*]}&amp;#34; &amp;#34;${multiple[*]}&amp;#34; &amp;#34;${early[*]}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;printf &amp;#39;PASS: aggregate and individual stage outcomes\n&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;BASH&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;每个打印数组的第一项是管道整体状态，后面按源码顺序列出各段状态。前三组预期分别为零七零、七七零、四七四。最后一组中，head 成功，而 yes 返回非零；断言没有硬编码具体信号编号，避免把平台表示方式混入行为要求。生产环境应区分这种主动提前停止与必需输出丢失，不能看到 pipefail 非零就无条件重试全部操作。吞吐量、缓冲延迟和内存峰值仍需独立测量，这些退出状态断言并没有验证性能。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Pipelines.html"&gt;GNU Bash：管道&lt;/a&gt;，进程执行和整体状态规则，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man7/pipe.7.html"&gt;Linux man-pages：pipe(7)&lt;/a&gt;，字节流、描述符生命周期、容量与断管信号，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Special-Parameters.html"&gt;GNU Bash：特殊参数&lt;/a&gt;，最近命令的退出状态，已核验 2026-09-06。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>