每天和服务器打交道,我见得最多的场景不是写代码,而是“看文件”。看日志、看配置、看数据文件格式,十有八九绕不开两个命令:Linux里的 tail 和 head。很多新手觉得这两个命令简单到不值得学,但实际排查问题的时候,“tail -f 和 tail -F 到底差在哪”“head -n -5 里的负号是干什么的”这种问题,又能问住一批所谓的老手。这篇文章不打算把参数表念一遍,而是从实际使用的角度,把这两个命令的定位、细节、组合用法和我在生产环境踩过的坑一起讲清楚。
1. 为什么不是 cat,而是 tail 和 head
1.1 不是所有文件都适合用 cat 看
很多人刚开始接触 Linux 时,第一种看文件的方式就是 cat,cat 本身没有错,处理小文件、快速确认配置内容时很顺手。但它有一个天然问题:cat 会把整个文件从头到尾都读一遍,然后原样输出到终端。
日志文件在服务器上经常是几十 MB、几百 MB 甚至几个 GB。如果你在 SSH 会话里执行 cat app.log,后果不外乎两种:一是文件内容刷屏到你根本找不到目标内容,二是终端卡死,甚至因为一次性输出太多字符导致 SSH 客户端崩溃。更关键的是,cat 全量读取会产生不必要的磁盘 IO,多台服务器并发读大文件时,可能直接把业务 IO 拉高。
tail 和 head 解决的就是这类“只想看一部分”的需求。head 从文件开头取,tail 从文件末尾取,思路一致,方向相反。它们不会把整个文件塞给你的屏幕,而只取你关心的那部分,处理效率和资源占用都更可控。
1.2 观察“文件两端”是一种高频需求
实际工作中你慢慢会发现,故障信息、错误堆栈、最近写入的记录,绝大多数都出现在文件末尾。日志的特性就是“越新的内容越靠后”,所以排查问题时第一反应总是 tail -n 100 看最后一百行。但有些场景你又需要看文件开头,比如确认 CSV 文件的表头有没有异常、检查配置文件的第一个段落、判断一份导出的数据文件是否有 BOM 头。
把 tail 和 head 放在一起学,一方面是因为它们天然是对称的,参数思维几乎一样;另一方面,它们在管道组合里经常成对出现。学会了对称记忆,你记参数的压力会小很多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. tail 的细节里,藏着真正的工作效率
2.1 -n 参数的正负号,很多人都没彻底搞懂
tail -n 100 app.log 表示查看文件最后 100 行,这是最常用、也最不容易出错的写法。但 tail 还有另一种很容易混淆的用法:tail -n +100 app.log。
这个 +100 表示的不是“最后 100 行”,而是“从第 100 行开始,一直显示到文件末尾”。如果你刚开始接触时没注意这个加号,后面很可能会在使用时翻车。我记得有一次帮同事看脚本,他把 tail -n +1 当成“最后一行”,结果脚本输出从第一行开始到最后一行,自然什么都看不出来。
搞清楚这个之后,很多场景就会非常方便。比如你处理一个分片日志,已经确认前面 200 行都是程序启动时的冗余信息,你想跳过这 200 行直接看后面的新日志,写成 tail -n +200 app.log 就可以了,不需要先去统计总行数,再算差值。
2.2 -f 和 -F:一个字母之差,日志轮转时差别巨大
tail -f 可能是所有运维人用得最多的实时跟踪参数。tail -f app.log 启动后,屏幕会停在那里,文件有新内容写入时自动显示出来。它适合调试接口、观察服务启动过程、确认定时任务是否正常跑完。
但我必须提醒你:如果是长期运行的监控脚本,或者你接管了一个会用 logrotate 按天切分的日志目录,那 tail -f 会坑你。
举例:
bash复制tail -f /var/log/myapp/app.log
假设系统在凌晨 00:00 执行 logrotate,把当前 app.log 重命名为 app.log.20250415,然后新建一个 app.log。这时你用 tail -f 跟在后面,十有八九会发现:新文件里已经有内容了,但你的终端就是不再输出任何东西。
原因在于,tail -f 默认跟踪的是“文件的 inode”,或者说文件描述符指向的那个物理文件。原文件被改名后,新创建的 app.log 对应的是另一个 inode,旧文件已经没人写入,tail -f 自然就等不到内容了。
这种情况应该用 tail -F:
bash复制tail -F /var/log/myapp/app.log
tail -F 等价于 --follow=name --retry,它会按照文件名来跟踪。文件被重命名、删除、重建后,tail 能识别到新的文件并继续输出。所以凡是日志会切割、按天归档的环境,我建议默认用 -F,而不是 -f。
提示:tail -f 更适合临时调试会话,tail -F 更适合长期后台监控。两者看起来差不多,但在日志轮转场景里,用错了确实会少看一大段报错。
2.3 -c 按字节取,适合处理非行格式文件
有时候日志或数据文件不是按行组织的,比如一些二进制日志、带格式的消息记录。tail -c 允许你按字节来截取尾部内容:
bash复制tail -c 1024 app.log
这条命令输出 app.log 最后 1024 字节的内容。它对应的另一个写法是 tail -c +1024 app.log,含义是从第 1024 个字节开始显示到文件末尾。这里的加减号逻辑和 -n 完全一致。
按字节操作最容易被忽略的问题是多字节字符。比如文件是 UTF-8 编码,中文一个字符占三个字节,你用 tail -c 1 去取末尾内容,大概率会取到一个不完整的字符,终端里就会显示成 �。后面我会再展开讲这个坑。
2.4 tail -f 并不代表“自己会滚动”
很多刚用 Linux 的人会把 tail -f 理解成类似 Windows 里的“滚动日志窗口”,但它的实现原理其实没那么复杂。tail -f 并不会无限重读整个文件,它会记录当前读取到的位置,等到文件有新数据时再增量读取。没有新数据时,进程基本处于等待状态,占用的 CPU 很小。
理解这一点对排查有一定帮助。如果你发现 tail -f 卡住不输出,可以先确认目标文件是不是真的没在增长,再确认文件是否被改名、删除、重建。如果文件被 logrotate 切过,用 tail -F 重新启动基本就能解决。
3. head 不只是“看前几行”,它还是掐尾巴的利器
3.1 基础用法一眼就会,但要注意默认值
head 的普通用法确实没太多可讲的:
bash复制head -n 20 app.log
查看文件前 20 行。
不过有一个小细节:head 默认显示前 10 行,tail 默认显示后 10 行。早期很多版本还允许直接省略 -n 写成 head -20 app.log,虽然现在 GNU 版本仍然兼容这种写法,但我建议新脚本里统一用 -n,可读性和可移植性都更好。
3.2 head -n -5 的负号写法,可以排除最后 N 行
head 有一个很冷门但很实用的扩展能力:head -n -5 file 表示输出“除了最后 5 行以外的所有内容”。注意,这里不是只看前 5 行,而是把文件尾部排除掉。
看一个实际场景。假设你有一个很大的文本数据文件,最后几行是汇总信息或者追加的干扰行,你希望过滤掉它们,生成一份可以继续处理的数据文件:
bash复制head -n -5 data.csv > data.clean.csv
如果文件只有几十行,用 sed 也一样:
bash复制sed -n '1,$-5p' data.csv > data.clean.csv
但如果文件非常大,head -n -5 的优势在于它本身采用流式处理思路,一边读一边写,不会在内存里维护完整的大数组。对于临时处理任务,head 不会给你带来多余的等待。
3.3 用 head 掐掉最终内容时,要注意它提前退出的特性
head 在读取到足够数量之后就会退出。这个特性在管道里非常有用,但它也会带来一个上游进程被提前结束的现象。
举例:
bash复制yes 你好 | head -n 3
正常情况下 yes 会无限输出,但 head 只要取到前 3 行就会关闭管道退出,系统再给 yes 发送一个 SIGPIPE 信号,yes 也因此停止。这样整段命令能很快结束,不会卡死。
这个机制的好处是省资源,坏处是某些对 SIGPIPE 处理不友好的程序可能收到一个被中断的写入状态。比如用一个压缩命令生成超大输出,再通过 head -c 截取前 100KB,压缩命令可能在中途报 broken pipe 退出。如果你并不想真正中断上游任务,就不要用 head 直接从管道里裁剪,建议改成先落文件再处理。
3.4 用 head 取中间某一段内容,是组合思路的核心
head 和 tail 最常见的组合就是取文件中间某个区间。如果你需要查看某个大日志从第 100 行到第 200 行的内容,最简单的办法是用 sed:
bash复制sed -n '100,200p' app.log
但如果你的环境不支持 sed,或者你只是想用一条管道快速干活,head/tail 也可以完成:
bash复制head -n 200 app.log | tail -n +100
这条命令先取前 200 行,然后从“第 100 行”开始输出,得到第 100 到第 200 行的内容。
另一种方向则是先跳过前面 99 行,再从剩余内容里取前 101 行:
bash复制tail -n +100 app.log | head -n 101
这两种写法结果相同,但适用场景略有差异。前一种更适合你知道截止行号的情况,比如只看前 200 行里的中段;后一种更适合你从某个起点出发并限制后续输出量,比如报错发生后取某个位置之后的 100 行。
4. 实战场景拆解:tail 和 head 是怎么组合打天下的
4.1 实时监控日志并过滤关键字
最常见的实时排查场景是:
bash复制tail -f app.log | grep ERROR
但是我必须指出,这条命令很可能不会立即输出任何内容。原因出在管道缓冲上。当你把 grep 的输出接到管道里,而管道的另一端不是终端时,grep 会启用块缓冲;数据没有积累到一定量之前,grep 不会往 stdout 写结果。
解决办法是给 grep 加上 --line-buffered:
bash复制tail -f app.log | grep --line-buffered ERROR
这会告诉 grep 每遇到一行就立刻刷新输出。如果你需要在日志流里筛出 5xx 状态码,可以这样写:
bash复制tail -F /var/log/nginx/access.log | grep --line-buffered -E '" 5[0-9][0-9] '
这样只要 nginx 访问日志里出现 HTTP 500、501、502、503 之类的状态码,屏幕上就会实时弹出对应的完整请求行。
4.2 从最后一次报错现场向后继续取证
有一次线上服务报错,但报错信息只在一瞬间闪过。我手头的大日志文件里,已经积累了超过 20 万行记录。直接打开文件找是不现实的,后来我用了这条思路:
先通过 grep 找到报错关键字所在的行号:
bash复制grep -n "OutOfMemory" app.log | tail -n 5
这里用 tail -n 5,是为了只看最近几次报错而不是最开始的。拿到行号后,再结合 tail 和 head 定位报错前后的上下文:
bash复制tail -n +189300 app.log | head -n 120
第 189300 行正好是最后一次报错出现的位置,这条命令输出第 189300 行开始往后的 120 行,我就能看到从报错发生到后续恢复的完整记录。
4.3 批量查看多个服务日志的首尾状态
排障时经常要同时确认多个服务的运行状态。比如一个部署了 12 个微服务的机器,我需要在入口处一次性检查每个日志文件是否有内容、是否在更新。
可以用一个简单循环:
bash复制for log in /var/log/service/*.log; do
echo "==== $log ===="
head -n 1 "$log"
tail -n 5 "$log"
done
这个脚本会输出每个文件的日志第一行和最后五行。第一行通常能反映服务启动时间、启动参数,最后五行则能反映最近一次请求或异常。配合 cron 或其他监控系统,可以快速发现“某个服务日志已经停更”这类问题。
4.4 构造超大测试文件,并验证结尾内容
写数据管道或并发测试时,经常需要一个临时大文件。可以用 seq 构造函数:
bash复制seq 1 1000000 > bigfile.txt
生成之后,想知道这个文件最后写到了哪个数字,不需要 cat 整个文件:
bash复制tail -n 1 bigfile.txt
如果你想确认文件的前几行是不是按预期写入:
bash复制head -n 3 bigfile.txt
这两条命令的执行速度和磁盘 IO 都很小,因为你取出的数据量非常有限。
4.5 实时统计日志里的请求来源 Top 10
再看一个偏分析方向的场景。线上服务怀疑被某个来源 IP 刷量,需要快速从访问日志里统计访问量最高的 10 个 IP:
bash复制tail -n 100000 /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -n 10
这里 tail -n 100000 负责把样本范围限制在最近 10 万条记录内,后面的管道负责统计,最后 head -n 10 只输出 Top 10。如果没有 tail 把总量限制住,sort 和 uniq 面对整个日志文件不仅慢,还可能把内存打满。
5. 这几类坑,我在生产环境里都踩过
5.1 用 tail -f 接 grep 后无输出,最容易被误判成“日志没在更新”
我第一次碰到 tail -f app.log | grep ERROR 不出结果时,第一反应是日志真的没有报错。但打开另一个终端验证,发现报错一直在刷。问题就出在 grep 的管道缓冲上,数据攒在缓冲区里没写出来。
从那以后,凡是把 tail 接给 grep 做实时过滤,我都会强制加 --line-buffered。如果是用 awk 做过滤,同理也要在逻辑里调用 fflush(),或者使用 stdbuf -oL 调整标准输出的缓冲策略:
bash复制tail -f app.log | stdbuf -oL grep ERROR
这类即时反馈问题,处理不好会直接误导排障方向。
5.2 日志轮转后,tail -f 悄悄沉默
有一次维护一个线上任务,我用 tail -f 把任务日志实时打印到后台屏幕,观察很久都没输出,我以为任务卡死了。结果一查,发现凌晨日志已经被 logrotate 切成新文件,旧进程还跟在旧 inode 上,任务本身一直正常跑,只是“看客”没跟上。
后来我把脚本统一改成:
bash复制tail -F /data/logs/task/task.log
这里也要提醒一句,tail -F 在文件被移除时会周期性地重试,所以在某些场景下会比 tail -f 多一点系统调用开销。对于普通日志追踪场景完全不用担心,但如果你在一个目录里同时 tail 几百个文件,且这些文件频繁轮转,可以观察一下系统负载。
5.3 从管道里用 head 截取内容,会让上游程序“意外死亡”
head 提前退出导致的 SIGPIPE,大多数情况是好事,比如 tail -f 无限日志 | head -n 1 就可以“只取当前最新的一行,拿完立刻结束”。但也要警惕副作用。
举个例子,你想通过管道查看一个大压缩文件的前几百字节内容,可以执行:
bash复制cat huge.sql.gz | head -c 200
或者更稳妥一点:
bash复制gzip -dc huge.sql.gz | head -c 200
取到 200 字节后 head 退出,gzip 会在下一次写入时收到一个管道关闭的信号。这个退出对 gzip 这类命令一般没有影响,但如果是某些需要完整落盘的写数据库程序,管道断裂可能让程序进入未完成的回滚状态。所以在脚本里使用 head 截取较大任务的输出时,要考虑你的上游程序是否适合被提前中断。
5.4 用 tail -c 或 head -c 切多字节字符,输出乱码
有一次我需要查看一个日期的最后 100 字节,直接用了 tail -c 100 app.log。出来的内容里出现了几个带问号的方块。原因很简单:日志里有中文字符,而 UTF-8 的中文字符是 3 字节编码,按字节位置切割时正好把一个完整字符拦腰切断,终端无法解析成合法字符,就只能显示乱码。
如果你确定目标文件是纯英文或单字节编码,按字节截取没有问题。但日志里混了中文、表情符号或者其他多字节编码时,建议优先按行截取,也就是用 -n,而不是 -c。如果一定得按字节截,终端显示乱码时不要慌,文件本身没有被破坏,只是展示层面出现了不可解析字节。
5.5 跨系统使用时,相同命令可能不同脾气
Linux 发行版里一般默认的是 GNU coreutils,tail/head 的扩展参数都比较全。但如果你在 macOS 开发机上写脚本,再丢到 Linux 服务器执行,或者反过来,就要注意两个环境之间行为差异。
比如 head -n -5 这类带负数的扩展写法,在部分 BSD 系实现里就不一定支持;tail -n +5 的兼容性相对好一些,但不同平台对选项解析的要求也可能不完全一样。如果你写的脚本需要在多套系统里运行,尽量用最标准的 POSIX 参数形态,不要过度依赖 GNU 扩展。遇到复杂的中段截取需求,用 sed、awk 这类通用工具完成会更稳。
5.6 处理被截断或被清空的大文件
生产环境中,有些日志因为磁盘满了会被运维用 cat /dev/null > app.log 的方式快速清空,或者用 truncate -s 0 截断。如果你正用 tail -f 看这个文件,GNU 版本的 tail 通常会发现“文件被截断”,然后从新的空内容继续跟踪。而如果你用的方式不对,有时会看到一大段“历史残留”被重新输出一遍,让人误以为服务在疯狂刷错误。
遇到这种日志内容被清空的运维操作,最稳妥的处理方式是先重启你的 tail 进程,再用 tail -F 去跟新文件。这样既能避开文件截断瞬间的重复输出,也能确保后续轮转时不会丢内容。
最后再分享一点个人习惯
我现在排查问题,很少会直接对生产环境的大日志执行 cat 了。需要快速了解文件全貌时,我会用 head -n 30 看文件头、用 tail -n 50 看最新状态,再用 grep 精准定位关键字。tail 和 head 看着不起眼,却是整个 Linux 命令体系里最值得先掌握的一对基础工具。
如果你正在系统地学 Linux,建议从这对命令开始做练习:随便找一个服务日志,试着用 tail -n + 跳过启动阶段信息,再试着把它的某个时间区间单独截出来。多练几次之后,你会慢慢习惯“每次只取必要数据”的排查思路。这个思路带来的效率提升,比多背十个命令都明显。
