ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux重定向与管道:从数据流到命令流水线的实战指南

Linux重定向与管道:从数据流到命令流水线的实战指南 在 Linux 下干活重定向和管道是绕不开的两个基本功。我发现很多新手手里捏着一堆命令却总觉得它们像散装零件没法拼成一台能转起来的机器。其实只要把重定向和管道搞明白你就能像流水线一样把命令串起来让每一段只做一件事。这篇文章会从最底层的三个数据流讲起把、、21、、和|一个个拆开再穿插我实际踩坑后总结出来的排查方法。适合刚接触 Linux 的同学也适合那些命令行用了很久但遇到重定向顺序还是要打开搜索页面的人。文章里的示例我都在 CentOS 和 Ubuntu 的真实环境跑过不同发行版在 bash 和 sh 下的行为略有差异我会特别标注。读完你会发现这些操作不是背出来的而是顺理成章推导出来的。1. 不搞懂三个数据流重定向和管道永远是死记硬背1.1 每个命令都有两条出口和一条入口当你在终端输入一条命令并按下回车时Linux 内核为这个进程准备了三条默认通道。文件描述符 0 是标准输入stdin通常对应键盘文件描述符 1 是标准输出stdout通常对应屏幕文件描述符 2 是标准错误stderr也对应屏幕。所以你在终端看到的打印信息其实混着两个来源正常的输出和错误提示。命令本身不一定知道自己是在终端里还是被脚本调用它只负责往 1 号通道写正常结果、往 2 号通道写错误信息。这里有个非常典型的例子$ ls /etc/hostname /no/such/file /etc/hostname ls: cannot access /no/such/file: No such file or directory第一条路径存在所以正常结果走 stdout第二条路径不存在所以错误走 stderr。你看屏幕觉得都是“输出”但对程序来说这两条路完全独立。你可以把命令想象成一家餐厅0 号窗口接收客人点单1 号窗口上正常菜品2 号窗口负责处理退菜和投诉。默认情况下 1 号和 2 号窗口都对着大厅客人分不清哪个是哪个。重定向要做的就是把其中一个窗口接到后厨、另一个接到留言板由你决定信息到底去哪。很多人一开始没有意识到 stdout 和 stderr 是分开的结果写脚本时把所有输出混到一个文件里出了问题才发现错误日志被正常日志淹没了。比如编译 C 程序时gcc main.c产生的代码错误和警告都会走 stderr如果你只想保存警告文件就需要把它们单独重定向出来。理解这三条通道是后续所有操作的基础一点也不夸张。1.2 重定向是“文件”管道是“命令”判断一个场景该用重定向还是管道我自己的标准非常简单看右边接的是文件还是命令。command file右边是文件把输出写入文件。command | next_command右边是命令把输出交给另一个程序继续处理。这个区别看起来显而易见但实际操作里我见过不少把两者混用的例子。比如有人想保存/tmp目录列表顺手写了ls -l /tmp | /tmp/list.txt然后得到一堆Permission denied或者is a directory。原因就是管道符右侧必须是一条可执行命令不是文件名。正确写法是ls -l /tmp /tmp/list.txt。不过重定向和管道之间还有一座桥叫命名管道FIFO。用mkfifo创建出来的文件看起来像一个普通文件但它的数据并不落盘而是由内核里的管道缓冲区直接转发给另一个读取进程。这和我们平时用|创建的匿名管道不太一样匿名管道由 shell 临时创建用完就消失命名管道是文件系统里长期存在的节点适合跨终端、跨进程通信。举个例子终端 A 先读取mkfifo mypipe cat mypipe终端 B 写入echo hello mypipe终端 A 就会收到hello。这里写入端用的“大于号”其实是重定向但目标不是普通文件而是一个 FIFO 节点。所以说重定向和管道不是非此即彼它们会在命名管道这里汇合。理解这条桥对你后面处理复杂脚本会很有帮助。2. 输出重定向从 到 再到 21一次讲透2.1 和 覆盖还是追加这是第一个选择最基本的输出重定向就是和。前者把文件整个覆盖掉后者在文件末尾追加。这个知识点大家都会背但真正用起来有几个容易被忽略的细节。先看覆盖echo start log.txt cat log.txt # start echo next log.txt cat log.txt # next第二次执行时log.txt原来的内容直接没了。这是因为 shell 在打开文件时默认使用O_TRUNC标志文件只要存在就会被截断。很多新手第一次写脚本时不了解这一点不小心把重要的配置或数据清空了追悔莫及。追加模式就安全得多echo start log.txt echo next log.txt cat log.txt # start # next会打开文件并定位到末尾不存在则创建。日常我写构建脚本时常用这种方式把时间戳、编译结果逐步累积到一个日志文件date %F %T /tmp/build.log make /tmp/build.log 21这里先表示“本次构建从零开始记录”后面用不断追加21则把编译错误也一并写进去。你可能已经看到了这里混用了两种语法下一节重点拆21。另外分享一个实用技巧如果只是想快速清空一个日志文件但不想删除文件本身可以直接用 app.log。这样文件还在但长度变为 0正在写文件的进程不会因为 fd 失效而中断。唯一要注意的是千万别在配置文件上随手敲 xxx.conf一旦清空再想找回就麻烦了。2.2 21为什么这个顺序不能乱很多后台运行或日志记录的推荐写法都是command file 21意思是把标准输出和标准错误都重定向到同一个文件。但这条命令有个经典变体command 21 file看起来只是换了个顺序效果却完全不一样。我见过不少人在这个坑里栽过跟头。关键在于shell 处理重定向时是从左到右依次设置文件描述符的。 file 21的流程是先把 stdoutfd 1指向 file再把 stderrfd 2指向“当前的 stdout”也就是 file。所以两者都进文件。而21 file的流程是先把 stderr 指向“当前的 stdout”也就是屏幕再把 stdout 指向 file。结果 stdout 进了文件stderr 还是打在屏幕上。很多教程只会丢给你“最好用21”但没说顺序背后的含义。你看完这段应该就彻底明白了。写法stdout 去向stderr 去向实际效果cmd filefile屏幕最常见的错误cmd file 21filefile推荐写法cmd 21 filefile屏幕顺序反了坑cmd filefilefilebash 简写cmd 21 | grep err管道管道合并后过滤 file是 bash 提供的简写等价于 file 21。但它不是 POSIX 标准在sh或某些精简环境下会报语法错误所以我更推荐写全 file 21通用性最好。如果你想把正常输出和错误输出分别存到两个文件也不难find / -name *.conf /tmp/conf_found.txt 2 /tmp/conf_error.txt这条命令里正常结果写入conf_found.txt权限不足等错误写入conf_error.txt两者互不干扰。分析日志时用这种方式可以快速对比“哪些路径找到了”和“哪些路径没权限查”。2.3 、、输入也能重定向说完了输出再看输入。可以把文件内容当作命令的标准输入在管道概念出现之前这是命令之间传递数据的常用方式。sort unsorted.txt tr a-z A-Z file.txt有个容易忽略的差别wc -l file和wc -l file的输出格式不一样。前者因为命令是从 stdin 读取不知道文件名只输出行数后者会带上文件名。写脚本做数值运算时通常用wc -l file因为结果更干净不会多出个文件名字段。是 Here Document主要用于在脚本里内嵌多行内容然后把它喂给某个命令。最典型的场景是生成配置cat /etc/nginx/conf.d/example.conf EOF server { listen 80; server_name example.com; } EOF这里EOF是分隔符可以换成任意字符串但要保持前后一致。如果分隔符加了单引号EOF内容中的$VAR、反引号等不会做展开原样写入文件。如果没有加引号shell 会把里面的变量替换掉。我做部署脚本时经常用这个特性在运行时把环境变量渲染进配置文件cat /tmp/config.yml EOF url: ${APP_URL} token: ${APP_TOKEN} EOF是 Here String比更轻量直接把一个变量或字符串作为标准输入。比如grep -i error $LOG这条命令等价于echo $LOG | grep -i error但不用额外开一个管道进程在循环里调用时会清爽很多。我个人很喜欢用配合bc做数值计算bc scale2; 10 / 3输出3.33不回车不换行非常干净。3. 管道把命令串成流水线3.1 管道是怎么做到“隐形连接”的管道符|的底层原理并不神秘。shell 看到cmd1 | cmd2时会调用pipe()创建一个内核管道缓冲区然后让两个进程同时运行cmd1的 stdout 指向管道写端cmd2的 stdin 从管道读端读取。数据不会落到磁盘而是在内核内存里流动。管道缓冲区满时写端会阻塞缓冲区空时读端会等待。这天然实现了背压控制所以不用担心一串命令会把内存撑爆。我经常跟别人打一个比方管道就是车间里的传送带。前面的工人把加工完的零件放到传送带上后面的工人拿起来继续加工两边可以同时干活。这个比喻虽然简单但能解释很多现象比如你执行ping -c 3 127.0.0.1 | grep time实际上ping和grep是并行启动的不是等ping全跑完才交给grep。区别在于普通文件的重定向是“把整本笔记本丢给你”而管道是“一边写一边读”。所以当我们写cat huge.log | grep error时grep不需要等cat读完整个文件就能输出第一批匹配结果这在大日志分析中是实打实的效果。命名管道FIFO和匿名管道的区别也可以在这里讲清楚。匿名管道没有文件名只在创建它的 shell 和他的子进程之间有效。命名管道会出现在文件系统里任何拥有权限的进程都能通过文件路径来访问它。前面我演示过用重定向往 FIFO 里写数据这里再补充一个注意事项如果先执行echo x mypipe而读端还没就绪这条命令会阻塞住。所以使用 FIFO 时最好先让读端运行或者把写端放到后台。这是新手最容易卡住的地方。3.2 高频命令组合日志分析、进程管理和参数终结者管道的价值在于组合而组合能力来源于对常用命令的熟悉程度。我挑三个最常见的场景来演示。第一个是统计日志里的客户端 IP 排名awk {print $1} access.log | sort | uniq -c | sort -rn | head -20我解析 nginx 访问日志时几乎天天用。awk提取第一列 IP →sort把所有 IP 排到一起 →uniq -c统计每项连续出现的次数 →sort -rn按次数从大到小排 →head -20只看前二十。很多人会忽略sort这一步但uniq只能合并相邻重复行不排序就会统计出多个相同 IP 的行。第二个是找进程并终止ps -ef | grep nginx | grep -v grep | awk {print $2} | xargs kill这里有个反模式grep -v grep目的是把grep nginx自身那条进程过滤掉但写起来很啰嗦。我更推荐用正则技巧ps -ef | grep [n]ginx。因为[n]ginx匹配的是nginx而不会匹配到带有[n]ginx字样的 grep 进程本身。当然现代系统里pgrep -f nginx更简洁但有时候你还是需要在ps输出里保留其他信息这个技巧依然有用。第三个是批量删除文件。当文件数量特别多时参数列表会过长find直接接rm会报Argument list too long。此时xargs是很好的搭档find . -type f -name *.bak -print0 | xargs -0 rm -f-print0让文件名以 null 字符分隔-0让xargs按 null 而不是空格或换行切分这样文件名里再大的空格、引号也不会出错。这是处理批量文件时的必备组合。3.3 管道与重定向混搭一边过滤一边保存管道和重定向不是互斥关系把它们混在一行里才能写出真正实用的命令。最常见的格式是“前面重定向中间管道最后重定向”find /var -name *.log 2/dev/null | grep system /tmp/system_logs.txt这条命令的意思是忽略find遇到的无权限错误把找到的日志文件名交给grep过滤最后把包含system的行写入/tmp/system_logs.txt。注意2/dev/null只影响find不会影响管道后面的grep因为重定向是针对当前命令本身设置的。还有一种很常见的场景是把程序的标准错误也并入管道让管道后面的命令能统一处理java -jar app.jar 21 | grep Exception如果没有21grep只能看到 java 的正常日志错误栈全部漏掉。这里和前面讲的21 file顺序问题不一样因为管道是在整个命令级别创建的。java -jar app.jar 21 | grep的实际含义是先把 stderr 重定向到 stdout然后 stdout 进入管道所以 stderr 也进管道。这行命令如果写成java -jar app.jar | grep也没有语法错但拦不到错误信息。如果你既想把输出保存到文件又想实时在终端看到那就轮到tee出场了。这也是管道和重定向混搭的一个经典延伸后面单独讲。4. 实战中的坑与排查技巧实录4.1 一边保存一边看tee 的正确打开方式tee这个名字来源于 T 型三通一个输入两个输出一份到标准输出一份到文件。我最早用它是因为要做部署日志既想在终端实时看进度又想把完整日志留到文件里方便事后排查./deploy.sh 21 | tee deploy.log这行的效果是脚本的输出会在终端滚动显示同时完整写入deploy.log。如果没有tee你只能二选一要么让日志直接铺满屏幕要么静默进文件。tee默认覆盖文件如果希望追加到历史日志后面要加-a./test.sh 21 | tee -a test.log更进阶的玩法是把tee放在管道中间把日志存下来之后继续传给下一个命令./test.sh 21 | tee test.log | grep -q PASS echo 测试通过这样一份日志同时完成“展示、存档、条件判断”三件事。在 CI 脚本里这种写法能省不少临时文件。不过有一点要注意tee会把输入原样送到 stdout 和文件如果前面的命令输出量极大终端刷屏会让你看不清重点。我一般会配合tail或grep只显示关键行./build.sh 21 | tee build.log | tail -20这样终端只显示最后 20 行完整日志依然保存在文件里。4.2 管道退出码脚本里最容易翻车的地方管道虽然好用但有一个非常隐蔽的坑管道返回的退出码默认是最后一条命令的退出码而不是所有命令的。比如grep something /var/log/syslog | head -1 echo $?这条管道最后的head -1基本不会失败所以$?大概率是 0。可如果前面的grep因为没匹配到任何内容返回了 1这个失败信息就被后面的命令掩盖了。在自动化脚本里这可能导致错误被忽略整个任务继续往下跑最后出来的结果千奇百怪。要查看管道中每一段的退出码bash 提供了一个数组变量PIPESTATUSfalse | true echo ${PIPESTATUS[0]} ${PIPESTATUS[1]}输出是1 0清楚地告诉你第一段失败、第二段成功。这个变量在排查问题时很实用。举个例子我见过有人用curl ... | grep ...来判断接口是否正常如果curl因为网络问题根本没连上grep自然匹配不到但$?可能返回 1然后脚本就跟着报错。通过打印PIPESTATUS[0]才能定位到真正原因是网络而不是内容。更稳妥的做法是在脚本开头设置set -o pipefail。这个选项让管道中任何一个命令失败整个管道就返回失败避免最后一条命令“掩盖罪证”。比如set -o pipefail curl -s https://example.com/api | grep success如果curl失败整个管道的退出码就是非 0。这个选项只在 bash 里支持写成sh脚本时可能不生效所以我的习惯是在脚本第一行写#!/bin/bash。4.3 变量丢失、缓冲延迟、文件乱名常见问题速查这些年我积累了不少和重定向、管道相关的翻车案例整理成一张速查表方便你对照排查。症状原因解决方法重定向后文件为空屏幕却有错误只重定向了 stdoutstderr 没处理cmd file 21cmd 21 file后错误仍打在屏幕重定向顺序错误换成 file 21管道里 while 循环修改变量结束后变量没变管道会启动子 shell变量只在子 shell 里有效使用进程替换while ... done (cmd)tail -f管道到 grep 输出延迟grep 在管道里变全缓冲用grep --line-buffered文件名带空格xargs处理出错默认按空格切分输入find ... -print0加xargs -0后台任务nohup cmd 21 仍输出到终端只重定向了 stdoutstderr 没有合并nohup cmd /dev/null 21 脚本里管道grep没匹配到导致整体失败默认管道退出码只看最后一段用PIPESTATUS或set -o pipefail第一个变量丢失问题很多人会踩得莫名其妙。看这段代码count0 printf a\nb\n | while read line; do count$((count1)) done echo $count你可能会以为输出2但实际输出0。原因是管道右侧的while运行在子 shell 里它修改的是子 shell 的count父 shell 的count根本没动。解决方法是去掉管道用进程替换count0 while read line; do count$((count1)) done (printf a\nb\n) echo $count这里的 (printf ...)是先做进程替换再通过输入重定向把结果喂给while整个过程都发生在当前 shell所以变量能保留下来。注意写法中间有空格写成 (...)不要和搞混。第二个缓冲问题出现在实时日志处理里。比如tail -f access.log | grep error在使用管道时grep默认采用全缓冲可能积攒一批数据才输出看起来就像卡住了。加上--line-buffered就能让每读一行立刻输出tail -f access.log | grep --line-buffered error这也是我在实时监控场景里必用的参数。这些坑没有一个是看文档能记得住的全是真实环境里被坑出来的。尤其是21的顺序、管道退出码和子 shell 变量问题如果没有亲自动手调过很可能会在某个凌晨被线上脚本折磨到怀疑人生。希望这篇偏实操的记录能帮你少走点弯路。
返回列表