0Pricing
Linux Command Line & Bash Scripting Mastery · 课时

流式管道与命名管道吞吐

使用 FIFO 和进程替换在各阶段之间流式传输数据,无需中间文件。

流式管道与命名管道吞吐 是 CoddyKit 上的免费 Linux Command Line & Bash Scripting Mastery 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Linux Command Line & Bash Scripting Mastery 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。

为什么中间文件会降低吞吐量

当您使用 sort file.txt > tmp.txt && uniq tmp.txt > result.txt 这样的方式串联命令时,需要承担一项隐藏成本:磁盘写入、磁盘读取,以及流水线必须等第一阶段完全结束后下一阶段才能开始,因此会发生停顿。

流式流水线可以消除这项成本。数据在内存中直接从生产者流向消费者,并按阶段并发处理。这正是 Unix 管道背后的核心思想,而命名管道(FIFO)进一步扩展了这一思想。

  • 匿名管道(|):连接同一 Shell 命令行中相邻的两个命令。
  • 命名管道(FIFO):文件系统中的特殊文件,使互不相关的进程能够相互传输数据流。
  • 进程替换:让一个命令可以将另一个命令的输出当作文件使用。

本课将展示如何在实际 Bash 工作流中应用这三种方式,以最大化吞吐量。

流式流水线的结构

匿名管道将一个进程的标准输出连接到下一个进程的标准输入。内核会让两个进程同时运行,并使用固定大小的内存缓冲区(在 Linux 上通常为 64 KB)。

关键认识是:流水线的速度取决于最慢的阶段。如果生产者更快,它会因缓冲区已满而阻塞;如果消费者更快,它会因缓冲区为空而阻塞。这种反压是一种免费且自动的流量控制。

下面的示例会统计大型访问日志中的不重复 IP 地址,整个过程从不写入临时文件。每个阶段都会并发运行:

#!/usr/bin/env bash
# Stream a 2 GB access log — all stages run in parallel
grep '"GET' /var/log/nginx/access.log \
  | awk '{print $1}' \
  | sort \
  | uniq -c \
  | sort -rn \
  | head -20

使用 mkfifo 创建命名管道

使用 mkfifo 可以创建命名管道(FIFO — 先进先出)。它在文件系统中看起来像普通文件,但写入其中的数据不会存储在磁盘上,而是直接流向读取进程。

请记住以下关键行为:

  • 向 FIFO 写入时,会一直阻塞,直到有读取者打开它;反之亦然。
  • FIFO 条目会保留在文件系统中;使用完毕后必须用 rm 将其删除。
  • 允许有多个写入者,但无法保证它们之间的顺序。

下面的示例中,生产者会将数据压缩到 FIFO 中,同时消费者将其上传到 S3,不需要临时文件。

#!/usr/bin/env bash
mkfifo /tmp/stream_pipe

# Producer: compress in background
gzip -c /var/log/syslog > /tmp/stream_pipe &

# Consumer: read from FIFO (runs in foreground)
wc -l < /tmp/stream_pipe

wait
rm /tmp/stream_pipe

进程替换:将命令当作文件使用

进程替换使用 <(command) 或 >(command) 语法。Bash 会在幕后创建 FIFO(或 /dev/fd/N 文件描述符),并将路径传递给外部命令。

当工具需要的是 文件名参数而不是标准输入时,这种方式非常有用。如果不使用进程替换,您就需要临时文件;使用它后,数据可以直接进行流式传输。

  • <(cmd):外部命令从 cmd 的输出中读取。
  • >(cmd):外部命令向 cmd 的输入中写入。
#!/usr/bin/env bash
# diff two sorted streams without creating temp files
diff <(sort /etc/passwd) <(sort /etc/group)

# Compare live command output against a baseline
diff <(ls /usr/bin | sort) <(cat ~/bin_baseline.txt | sort)

tee:将数据流拆分给多个消费者

tee 从标准输入读取内容,并将其写入标准输出以及一个或多个文件。结合进程替换,您可以将一个数据流同时分发到多个处理流水线,而完全不接触磁盘。

当您希望同时记录原始数据并对其进行处理时,这种模式非常有用。

#!/usr/bin/env bash
# Generate 100000 random numbers, then simultaneously:
#  1. compute the sum
#  2. find the maximum
#  3. count lines (saved to a variable)
seq 1 100000 \
  | tee >(awk '{s+=$1} END{print "Sum:", s}') \
        >(awk 'BEGIN{m=0} $1>m{m=$1} END{print "Max:", m}') \
  | wc -l | xargs echo "Count:"

扇出模式:一个生产者,多个消费者

当一个数据源必须为多个独立消费者提供数据时,可以将 tee 与多个 >() 进程替换结合使用。每个消费者都会获得完整的数据流,并发运行。

这样可以避免多次读取源文件。对于一个 10 GB 的文件,这种差异非常显著:只需读取磁盘一次,而不是 N 次。

#!/usr/bin/env bash
# Read a large CSV once; simultaneously:
#  - count rows
#  - extract column 2 to a file
#  - pass column 3 to a stats script
cat large_data.csv \
  | tee \
      >(wc -l > /tmp/row_count.txt) \
      >(cut -d',' -f2 > /tmp/col2.txt) \
      >(cut -d',' -f3 | awk '{sum+=$1} END{print sum}' > /tmp/col3_sum.txt) \
  > /dev/null

echo "Rows:" $(cat /tmp/row_count.txt)
echo "Col3 sum:" $(cat /tmp/col3_sum.txt)

扇入模式:多个生产者,一个消费者

扇出的相反模式是扇入:多个独立数据源将数据流传入一个消费者。使用命名 FIFO 可以轻松实现这一点。

一个常见用例是实时合并多个服务器的日志流,或汇总并行工作者产生的部分结果。

请注意,存在多个写入者时,消费者看到的输出会彼此交错。对于每行自成一体的面向行数据,这通常没有问题;但如果顺序很重要,则必须自行处理排序。

#!/usr/bin/env bash
mkfifo /tmp/fanin_pipe

# Three producers write concurrently into the same FIFO
for host in web1 web2 web3; do
  ssh "$host" 'tail -n 500 /var/log/app.log' > /tmp/fanin_pipe &
done

# Single consumer reads all merged output
grep 'ERROR' /tmp/fanin_pipe | sort | uniq -c | sort -rn

wait
rm /tmp/fanin_pipe

使用 mkfifo 进行并行压缩

FIFO 最实用的用途之一是并行压缩。pigz(并行 gzip)或 pbzip2 等工具可以读取数据流;您可以直接传输原始数据,而无需先生成未压缩文件。

下面的模式会归档一个目录,使用全部 CPU 核心进行压缩,并将结果流式传输到远程主机,所有步骤同时进行:

#!/usr/bin/env bash
# Tar + parallel compress + stream to remote — no temp files
# Requires: pigz (parallel gzip)
tar cf - /data/large_dir \
  | pigz -p 4 \
  | ssh backup-host 'cat > /backups/large_dir.tar.gz'

# Verify the remote file exists
ssh backup-host 'ls -lh /backups/large_dir.tar.gz'

控制缓冲区大小与阻塞

管道具有内核缓冲区(通常为 64 KB)。缓冲区已满时,写入者会阻塞;缓冲区为空时,读取者会阻塞。这通常正是您想要的行为,但在某些情况下,阻塞可能导致死锁。

死锁风险:如果进程 A 向 FIFO1 写入并从 FIFO2 读取,而进程 B 向 FIFO2 写入并从 FIFO1 读取,那么双方都可能阻塞,等待对方先完成读取。

解决方案:

  • 将至少一侧置于后台(&),使其不会阻塞 Shell。
  • 使用 mbuffer 或 pv 在各阶段之间添加更大的内存缓冲区。
  • 使用 pv -q -B 128m 插入一个 128 MB 的缓冲区,以平滑吞吐量峰值。
#!/usr/bin/env bash
# pv adds a 64 MB buffer and shows throughput
# Useful when producer and consumer have bursty speeds
dd if=/dev/urandom bs=1M count=200 \
  | pv -B 64m \
  | gzip \
  | wc -c

实际示例:实时日志聚合器

下面是一个完整且真实的模式:跟踪多个日志文件,通过命名管道合并数据流,筛选错误,并写入实时摘要,整个过程不使用任何中间文件,所有阶段均并行运行。

这类流水线通常会作为生产服务器上的后台监控脚本运行。

#!/usr/bin/env bash
FIFO=/tmp/log_aggregator
mkfifo "$FIFO"

cleanup() { rm -f "$FIFO"; }
trap cleanup EXIT INT TERM

# Fan-in: tail multiple logs into the FIFO
tail -F /var/log/syslog /var/log/auth.log > "$FIFO" &
TAIL_PID=$!

# Consumer: filter and timestamp errors in real time
grep --line-buffered -i 'error\|fail\|crit' "$FIFO" \
  | while IFS= read -r line; do
      printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$line"
    done

kill "$TAIL_PID" 2>/dev/null

对比流水线与临时文件的基准测试

您可以使用 time 测量流式方案与临时文件方案之间真实的吞吐量差异。对于大型数据集,流水线方案更快,原因如下:

  • 各阶段并发运行,CPU 与 I/O 可以重叠执行。
  • 中间数据无需进行磁盘 I/O,只有最终输出会写入磁盘。
  • 无论输入大小如何,内存占用都保持不变(传输的是数据流,而不是缓冲全部数据)。

下面是一个用于比较两种方案的简单基准测试:

#!/usr/bin/env bash
# Approach 1: Temp file (sequential)
time bash -c '
  seq 1 5000000 > /tmp/nums.txt
  sort -n /tmp/nums.txt > /tmp/sorted.txt
  uniq /tmp/sorted.txt | wc -l
  rm /tmp/nums.txt /tmp/sorted.txt
'

echo '---'

# Approach 2: Streaming pipeline (concurrent)
time bash -c 'seq 1 5000000 | sort -n | uniq | wc -l'

知识检查:命名管道的阻塞行为

请考虑以下脚本:

mkfifo /tmp/mypipe
echo 'hello' > /tmp/mypipe
echo 'done'

如果在没有任何后台进程或读取者的情况下运行此脚本,会发生什么?

回顾:流式流水线与命名管道

在本课中,您探索了如何在进程之间高效传输数据,而无需使用中间文件:

  • 匿名管道(|)连接相邻命令,并发运行所有阶段,同时自动实现反压。
  • 命名管道(mkfifo)会创建一个 FIFO 文件系统条目,使彼此无关的进程或后台进程能够相互进行流式传输——在有读取端之前,写入操作会阻塞。
  • 进程替换(<(cmd)、>(cmd))让原本需要文件名的命令能够透明地读取或生成数据流。
  • tee + >()可以将一个数据流分发给多个并发消费者,而无需重新读取源数据。
  • 扇入通过共享 FIFO 将多个生产者合并到一个消费者。
  • 反压与阻塞是功能而不是错误——但请始终至少将 FIFO 对中的一端放到后台运行,以避免死锁。
  • 当各阶段以突发方式运行时,请使用 pv 或 mbuffer 增加缓冲区并监控吞吐量。

这些技术是高吞吐量 Bash 数据工程的基础:以恒定内存处理 GB 规模的数据,并最大限度地实现 CPU/IO 并行。

常见问题解答

「流式管道与命名管道吞吐」课时是免费的吗?

是的 — 「流式管道与命名管道吞吐」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Linux Command Line & Bash Scripting Mastery 课程的其余内容,请升级到 CoddyKit PRO。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。

「流式管道与命名管道吞吐」这节课中我会学到什么?

使用 FIFO 和进程替换在各阶段之间流式传输数据,无需中间文件。 你通过在浏览器中直接运行的动手代码来练习 Linux Command Line & Bash Scripting Mastery,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Linux Command Line & Bash Scripting Mastery 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Linux Command Line & Bash Scripting Mastery 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「流式管道与命名管道吞吐」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Linux Command Line & Bash Scripting Mastery 课中编写并运行代码吗?

能。每节 Linux Command Line & Bash Scripting Mastery 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 分析脚本性能并避免无用的子 Shell
  2. 使用 xargs -P 与后台任务实现并行
  3. 使用 GNU parallel 编排工作负载
  4. 流式管道与命名管道吞吐
← 返回 Linux Command Line & Bash Scripting Mastery