news 2026/10/10 16:48:07

Linux运维实战:grep统计、pkill杀进程与truncate清空日志的避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux运维实战:grep统计、pkill杀进程与truncate清空日志的避坑指南

在服务器上报障排障的时候,有一类需求出现频率非常高:查询文件中指定内容出现了多少次、批量杀掉一批进程、把手头快写满的日志文件清空。这三件事拆开看都很基础,但真到生产环境,每一件都有不少容易被忽视的细节。比如统计次数时,是“按行统计”还是“按匹配次数统计”,结果能差出好几倍;批量杀进程时,一个模糊匹配可能把整个服务全带走;清空日志时,用 rm 还是用 truncate,又直接影响磁盘空间有没有真的释放。这篇文章就是记录我自己的日常命令和踩坑经验,给同样做运维、后端开发,或者刚接触 Linux 命令行的朋友一个参考。内容不搞花活,都是直接在终端里能跑、能抄走的东西。

1. 统计文本出现次数:grep、sort、uniq 的用法与坑

1.1 先分清“按行计数”和“按出现次数计数”

大部分人的第一反应是grep -c '关键词' 文件。grep -c的 c 是 count,但它统计的是“匹配的行数”,不是“匹配次数”。也就是说,一行里就算出现三次关键词,grep -c也只给你算一次。如果你只想看这个关键词到底出现了多少次,就要用grep -o '关键词' 文件 | wc -l。grep -o会把每个匹配到的片段单独输出一行,wc -l数行数,结果才是真实出现次数。

举个例子,有一条连接日志:

timeout 10.1.2.3:80 timeout 10.2.0.4:80

grep -c 'timeout'返回 1,grep -o 'timeout' | wc -l返回 2。别看差别小,写监控脚本的时候非常关键。比如判断某个接口在最近日志里超时次数是否超过阈值,如果用grep -c,一次请求里重复出现多次超时就被漏掉了,数据失真。

如果你要统计多个文件里的总次数,可以这样:

grep -o 'timeout' /var/log/app/*.log | wc -l

grep -o在多个文件时也会带文件名前缀,例如/var/log/app/a.log:timeout,这种情况下直接用wc -l没问题,因为每一行都对应一个匹配项。如果还想知道每个文件分别有多少次,用grep -c 'timeout' /var/log/app/*.log,输出格式是文件名:次数,一目了然。

还有一个常见的坑:关键词里带点号、星号这类特殊字符时,会被 grep 当成正则表达式解析。比如你想统计日志里app.jar这个文件名出现了多少次,grep 'app.jar'里的点号会匹配任意字符,可能把appajar这种也统计进去。正确做法是加-F参数,固定字符串匹配:

grep -F -o 'app.jar' app.log | wc -l

我见过不止一次因为没加-F导致统计结果虚高,最后排查半天才发现是点号通配的问题。类似的情况还有匹配带反斜杠的路径、匹配带方括号的字符串,统统建议用-F。

1.2 统计完还要排个名:sort | uniq -c 组合

在服务器上分析日志时,光知道某个词有多少次还不够,更常问的是“哪个 IP 访问最多”“哪个接口报错最多”。这时候需要把结果分组计数并排序,最经典的组合是:

awk '{print $1}' /var/log/access.log | sort | uniq -c | sort -rn | head -20

这条命令的意思是:用 awk 取出第一列(比如 IP 地址),sort 排序,uniq -c 把连续相同行合并并计数,sort -rn 按数字逆序排,最后 head 取前 20 行。

很多人会漏掉中间的第一步 sort,直接awk '{print $1}' file | uniq -c。这里有个隐藏问题:uniq只能合并“连续”的重复行。如果相同 IP 分散在文件不同位置,不先 sort 的话,计数会碎成好几行,结果完全错误。所以使用 uniq 之前,排序是必须的。

如果不想用 sort | uniq,awk 自己也能当计数器:

awk '{count[$1]++} END {for (k in count) print count[k], k}' /var/log/access.log | sort -rn | head -20

这种写法在单次扫描里累计,最后只需要排序已经压缩后的计数结果。对几十行的小日志无所谓,但对几千万行的大日志,性能要明显好于管道里反复 sort。我实际比较过,一个 2GB 的访问日志,用 sort | uniq 大约要跑 3 分钟,用 awk 计数后 sort 只需要 40 秒左右。如果你的日志量很大,建议优先选 awk 方案。

需要按特定分隔符取列时,用-F指定。比如 CSV 文件按逗号分割,取第一列:

awk -F',' '{print $1}' data.csv | sort | uniq -c | sort -rn

还有一个更冷门的 awk 技巧:直接统计某个关键词在整行里出现的总次数,不需要 grep -o 管道。用gsub的返回值:

awk -v pattern='error' '{n += gsub(pattern, pattern)} END {print n}' app.log

gsub返回替换的次数,这里把匹配到的内容替换成自身,不会改变原行,但能拿到次数。这个技巧在文件特别大、不想起多个管道进程时很管用。

1.3 在多文件、整个目录里统计总次数

有时候要统计一个目录下所有日志文件中某个关键词的总次数,而不是单文件。查找加统计一起做,常用 find 配合 grep:

find /data/logs -type f -name "*.log" -exec grep -o -h 'error' {} + | wc -l

这里加了-h(--no-filename)去掉文件名前缀,避免干扰统计。如果不用-h,每个匹配项前面会带文件路径:error,wc -l 照样能数出次数,但如果你想继续用 sort | uniq 统计关键词频率,路径前缀会让结果变得一团糟。所以建议统计总次数时加上-h。

更直观的写法是:

grep -r -o --include="*.log" -h 'error' /data/logs | wc -l

-r递归目录,--include限制扩展名。这个命令可读性好,但要注意-r和--include组合时,后面必须是指定的目录,如果指定的是单个文件,--include不生效。另外文件名带空格时,用管道加 xargs 可能会出问题:

find /data/logs -name "*.log" | xargs grep -o 'error'

如果文件名里有空格,xargs 默认按空格拆分,会把一个文件名拆成两个,出现 “No such file” 错误。稳妥写法是用-print0和-0:

find /data/logs -type f -name "*.log" -print0 | xargs -0 grep -o -h 'error' | wc -l

或者直接用 find 的-exec,它不会拆分文件名。我的习惯是能用-exec就用-exec,省得记-print0的规则。

2. 批量杀进程:从 pgrep 查找到 pkill 精确终止

2.1 查找进程就是避免误杀的第一步

很多同学一上来就是ps aux | grep python,然后照着输出杀进程。我不反对,但有三个细节必须先处理好。

第一,ps aux的输出里会有一行 grep 自己的进程。直接ps aux | grep python,结果里会包含grep --color=auto python这一行,如果你不加处理就把第二列的 PID 挑出来杀掉,容易把 grep 命令自己杀了。一般无伤大雅,但会让脚本后续逻辑变得混乱。一个小技巧是用grep '[p]ython',这样 grep 进程的命令行是grep [p]ython,正则表达式匹配不到它自己。

第二,更推荐用pgrep来找进程。pgrep专门按名称或命令行匹配 PID:

pgrep -a python

-a表示同时显示 PID 和完整命令行,先看再杀,比ps aux | grep干净得多。不过默认pgrep只匹配进程名,不匹配参数。如果你要按完整命令行匹配,比如启动命令里包含--worker=1,就要加-f:

pgrep -f -- '--worker=1'

一般我们用“服务名”或“命令行关键字”找进程的场景,都要用到-f。

第三,按用户找进程也有现成命令:

pgrep -u deploy

这个会列出所有属于 deploy 用户的进程。批量杀某用户所有进程可以用pkill -u deploy,但一定要慎用,因为可能把该用户下的 sshd 会话、cron 任务一起杀掉,直接造成服务断连。

2.2 pkill、killall、kill 怎么配合用

找到 PID 之后,可以直接kill PID,也可以pkill -f按命令行批量杀。我推荐的批量“套餐”是:

pgrep -f "python app.py" | xargs -r kill

xargs -r表示如果 pgrep 没有输出,xargs 不执行,避免报错。或者一行搞定:

pkill -f "python app.py"

pkill和pgrep共享同一套匹配语法,pkill默认发送 SIGTERM 信号,让进程自己处理退出逻辑,保存状态。如果进程没有响应,等几秒再用 SIGKILL 强制杀。下面这种写法适合放在脚本里:

for pid in $(pgrep -f "python app.py"); do kill "$pid"; done sleep 5 for pid in $(pgrep -f "python app.py"); do kill -9 "$pid"; done

这里有一个我自己踩过的坑:如果脚本本身的内容里含有python app.py这一段字符串,pgrep -f会匹配到当前正在执行脚本的 shell 进程,因为 shell 的命令行参数里包含整段脚本内容。结果第一个 for 循环把自己的父 shell 杀了,脚本直接中断。避坑写法是对 PID 做排除:

for pid in $(pgrep -f "python app.py"); do [ "$pid" -ne "$$" ] && kill "$pid" done

但实际场景下脚本可能不是直接执行的,$$不一定代表 pgrep 匹配到的那个解释器进程。所以我更建议在脚本里不要直接 pkill,而是先把 pgrep 的结果打印出来人工确认,确认无误后再单独执行 pkill。

killall按进程名杀,比如killall nginx会把所有叫 nginx 的进程都杀掉。注意killall在部分系统上也会匹配完整命令行参数,使用时要小心。如果系统没有killall,可以用pkill -x nginx,-x表示精确匹配进程名。

另外一个容易忽略的参数是信号等级:kill -9是 SIGKILL,kill -15是 SIGTERM。先发 SIGTERM,给进程几秒钟善后,再发 SIGKILL,是通用礼貌原则。直接-9虽然粗暴有效,但可能导致数据文件损坏、连接状态残留。我的习惯是优先kill,单次失败再kill -9。

2.3 父子进程和僵尸进程的注意事项

单个进程终止后,如果它还有子进程,子进程不一定会跟着死,而是被系统顶层进程收养。某些场景下我们想连子进程一起清理,比如父进程是调度器,杀掉父进程后一堆 worker 还在继续跑,继续占资源。这时候可以用pkill -P 父PID,按父进程 PID 匹配所有子进程:

pkill -P 12345

最好是先杀子进程,再杀父进程,避免父进程检测到子进程退出后重新拉起新的子进程。

僵尸进程不能用 kill 杀死,因为它本身已经死了,只是父进程还没有调用 wait 来回收它的进程表项。处理僵尸进程的正确方式是杀掉它的父进程,或者让父进程退出后,由系统回收。我之前遇到过某个常驻脚本产生一堆僵尸子进程,排查时先找到僵尸进程的 PPID:

ps -eo pid,ppid,stat,cmd | grep Z

确认是哪个常驻服务,再决定是否重启那个服务,而不是对着僵尸 PID 一顿 kill。

说到误杀,我自己有一次在清理测试环境时,想杀掉所有和某开发框架相关的进程,直接用了pkill -f 框架名。结果一瞬间把自己的终端断连了,原因是当前终端所在的环境里也有匹配该字符串的进程,更关键的是我在命令行输入的命令本身也包含这段字符串,pkill 匹配到了当前 shell。那之后我给自己定了三条原则:

  1. 批量杀之前,先pgrep -a看完整命令。
  2. pkill 的匹配字符串尽量用更长的、唯一的路径或参数,比如pkill -f '^/usr/bin/python.*app.py'。
  3. 不在涉及生产环境的自动化脚本里直接 pkill,先输出候选 PID 再人工确认。

3. 清空文件:truncate、重定向与日志轮转的选择

3.1 几种清空方式本质上的区别

清空一个日志文件,最常用的写法有> app.log、: > app.log、cat /dev/null > app.log。它们本质都一样:以截断方式打开文件,把文件大小变成 0。区别在于:

> app.log

shell 重定向,如果文件不存在会创建,存在则截断。

: > app.log

内建命令:什么都不干,但重定向依然生效,在某些脚本里看起来更语义化。

cat /dev/null > app.log

实际读一下空文件再写,效果相同,但多启动一个进程,不推荐。

还有一个工具叫truncate:

truncate -s 0 app.log

truncate 直接把文件大小调整为 0,不需要重新打开文件。如果你想保留末尾一小段内容,还可以用负号从尾部减掉一些,比如truncate -s -100M app.log,但要注意文件如果不足 100M 会报错。

相比rm加touch,用重定向或 truncate 有一个关键优势:文件 inode 和权限、属主都不变。生产环境里日志文件可能有特定属主和权限,比如系统服务日志属主是某个专用用户,你rm掉再touch,新文件会归当前操作用户所有,权限也可能变化,服务进程重新打开日志文件时直接权限不足。所以清空文件我基本只用> file或truncate -s 0,从不用rm。

还有一个细节:如果文件有硬链接,> file会把所有硬链接指向的同一份文件内容截断,因为截断操作直接作用于 inode;而rm file只是删掉一个硬链接,原文件内容还被其他链接保留。理解这一点,能帮你解释一些“为什么日志还在变”的诡异现象。

3.2 批量清空大文件前的安全操作

有时候日志目录里已经堆了十几个超大文件,磁盘快满了。你想批量清空超过 1GB 的日志,常见做法:

find /data/logs -type f -name "*.log" -size +1G -print -exec truncate -s 0 {} \;

这条命令会找到超过 1GB 的 .log 文件,打印路径并清空。但直接执行有风险,建议先只打印不执行:

find /data/logs -type f -name "*.log" -size +1G -print

在真正运行前,审视一遍列表,确认没有数据库文件、临时数据文件或系统关键文件混在里面。清空是破坏性操作,要比删除更谨慎。

如果文件数量很多,-exec ... {} +会比-exec ... {} \;快很多,因为它会把批量文件名合并成一次命令调用。GNU 的 truncate 支持一次处理多个文件,所以可以写:

find /data/logs -type f -name "*.log" -size +1G -exec truncate -s 0 {} +

如果你担心某条 find 语法在目标系统上不支持,先跑一遍-print确认列表,再用 xargs 转交,但记得用-print0和-0处理文件名空格。

批量清空之前,还需要想清楚日志是否需要留存审计。如果只是临时清理磁盘,可以考虑先压缩再删除,而不是直接清空。实际过程中,我在生产服务器上清空日志前,会先执行:

du -sh /data/logs

看看目录总大小,清空后再执行一次,对比空间是否真的释放。如果发现磁盘空间没降下来,就要考虑下面要说的文件描述符问题。

3.3 清空正在被写入的文件时,磁盘空间为什么没释放

很多人在生产环境碰到的第一起“神秘事件”是:明明> app.log把文件清空了,du一看磁盘占用还是没变。这通常不是命令没生效,而是进程的文件写入偏移量没有归零。

当一个进程以追加模式(O_APPEND)打开日志文件时,每次写入都会自动把偏移量移到文件末尾。你截断文件后,文件末尾变成 0,下一次写入就继续从 0 开始,磁盘空间会正常释放。大多数成熟的日志框架默认采用追加模式,所以直接 truncate 问题不大。

但有些程序不是以追加模式打开文件的。它可能在启动时用普通写模式打开,然后一直维护自己的文件偏移量。你 truncate 之后,文件大小变成 0,但进程的文件偏移量还停在你截断前的位置。下一次进程写入时,内核会根据偏移量把数据写到很远的地方,于是文件大小又变成“旧位置 + 新数据长度”,ls 看文件大小可能还是几个 G,磁盘占用根本没释放。

这种情况的处理办法不是反复截断,而是要通知进程重新打开日志文件。常见做法是给进程发送信号,比如很多服务支持kill -USR1重新打开日志,或者直接重启服务。如果你不确定进程是否支持,最稳妥的就是重启服务,让文件重新以正确的模式打开。

另外一个更常见的场景是:日志文件被删除后磁盘空间不释放。假设有人用rm删掉了一个正在被写入的日志文件,进程的文件描述符仍然指向已经被删除的 inode,数据会继续写到那个不可见的文件里,直到进程退出。用lsof +L1可以查看到这类“已删除但还被占用”的文件:

lsof +L1 | grep deleted

看到之后,找到对应进程 PID,重启或让它重新打开文件,磁盘空间才会真正释放。这也是我为什么一直强调:清空正在被写入的日志,优先用 truncate 而不是 rm。

3.4 logrotate 的 copytruncate 更适合长期维护

生产环境里手动清空日志只能应急,日复一日地手工操作迟早会忘。更标准的方式是用日志轮转工具,比如 logrotate。它的配置里有一个copytruncate选项,专门解决“清空不重启进程”的问题:

/data/logs/app.log { daily rotate 7 missingok notifempty compress delaycompress copytruncate }

意思是:每天轮转一次,保留 7 份归档,如果文件为空就不转,归档压缩,延迟一天再压缩,利用 copytruncate 复制当前日志内容到备份后截断原文件。这个方案的优势是服务进程不需要感知日志文件被移动,不会出现文件句柄指向已删除文件的空间浪费问题。

不过 copytruncate 也有代价:在复制和截断之间,可能有少量日志丢失,因为进程仍然在写原文件,复制完再截断的瞬间,刚才写入的内容可能已经进入了备份,也可能在截断后丢了一小段。对于绝大多数业务日志来说,这种极小的丢失可以接受。如果要求绝对完整,更严谨的方式是让服务进程支持重新打开日志文件,配合create或copy来做轮转,但那样往往需要配置信号钩子。

我的建议是:能上 logrotate 就上 logrotate,手动清空只用来应急。如果你只是偶尔清空一个临时文件,记住用 truncate,不要用 rm。

4. 常见问题与排查技巧实录

4.1 六个高频问题速查表

下面这些是我在实际操作中反复遇到、也经常被同事问的问题,整理成一张速查表:

问题现象根本原因解决办法
grep -c统计结果比预期小很多一个匹配行里有多次出现,grep -c 按行计数使用grep -o pattern file | wc -l
统计带点号的app.jar次数虚高点号被当作正则通配符使用grep -F -o 'app.jar' file | wc -l
uniq -c统计频率结果不对uniq 只能合并连续相同行,没先 sort先sort再uniq -c
pgrep 找不到某个进程默认按进程名匹配,没按完整命令行匹配使用pgrep -f "完整命令片段"
pkill 后自己的 shell 掉线pkill 匹配到了当前命令行字符串先pgrep -a确认,使用更精确正则
清空日志文件后磁盘空间没释放进程文件偏移量还在原位置,或文件被删除但仍被占用用lsof +L1排查,重启或发信号让进程重新打开文件

速查表之外的补充:kill -9杀不掉的进程,可能是处于不可中断睡眠状态,比如正在等待磁盘 IO。这种进程在/proc/<pid>/status里的状态是 D,你只能等待它结束,或者从根本上排查底层 IO 问题,杀掉效果为零。

4.2 提升统计性能的几个不起眼细节

处理大文件时,命令写法对性能影响非常大。首先是 locale,默认 grep 会按 UTF-8 处理多字节字符,如果日志主要是 ASCII,指定 C locale 能快不少:

LC_ALL=C grep -o 'error' big.log | wc -l

我实测过,一个大文件在默认 locale 下 grep 要跑十几秒,加LC_ALL=C后只需要几秒。如果你的日志不包含中文,放心用。

第二个细节是避免无谓的管道进程。cat file | grep xxx这种写法多启动一个 cat 进程,完全没有必要,直接grep xxx file。单次命令不觉得,放到循环里或调度任务里,就是实实在在的额外开销。

第三个细节是:如果只需要判断文件里有没有某个关键词,不需要统计次数,用grep -m1 keyword file,匹配到第一个结果就停止读取,文件再大也只是瞬间返回。下面的写法比grep | wc -l高效得多:

if grep -q -m1 'OutOfMemoryError' app.log; then echo "发现内存异常关键字" fi

第四个细节是关于 awk 统计的内存占用。awk 用数组计数时,所有 key 都存在内存里。如果统计的维度特别多,比如按整行日志去重,可能有几百 GB 内存风险。这种情况下最佳方案是先 sort 外排,再用 uniq 计数,用磁盘换内存。所以“awk 一定比 sort | uniq 快”也不是绝对的,要看数据量和唯一值数量。

4.3 把统计、杀进程、清空文件串成脚本骨架

这三个操作经常出现在同一个排障流程里:先统计日志里错误关键词的次数,次数超过阈值就检查相关进程,确认异常后清理进程,然后清空日志释放空间。下面这个脚本骨架可以复用,但我不建议在无人值守时自动执行杀伤性操作:

#!/bin/bash LOG_FILE="/var/log/app/app.log" PATTERN="OutOfMemoryError" THRESHOLD=10 # 1. 统计关键词出现次数 count=$(grep -o "$PATTERN" "$LOG_FILE" | wc -l) echo "关键词出现次数: $count" # 2. 超过阈值,输出候选进程,但不自动杀 if [ "$count" -gt "$THRESHOLD" ]; then echo "超过阈值,检查相关进程..." pgrep -a -f "app-server" || echo "未找到 app-server 进程" # 3. 手动确认后,再执行清理 # pkill -f "app-server" # sleep 5 # pkill -9 -f "app-server" fi # 4. 最后清理日志 # truncate -s 0 "$LOG_FILE"

我一般会在脚本里把杀伤性命令注释掉,只保留查找和统计。线上服务器跑定时任务时,宁可多花 30 秒人工确认,也不要让脚本自作主张把服务杀了。

最后分享一个习惯:任何可能误伤的批量操作,我都会先写一个“dry-run”模式。比如把要执行的命令先 echo 出来,加上DRY_RUN=1判断,真正跑之前打印出即将执行的进程列表或文件列表。这个习惯帮我避免过好多次事故,也希望你能用上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 16:45:38

养老院管理系统源码解析:SSM+Vue+Android+MySQL部署与二次开发避坑指南

简介&#xff1a;基于安卓的养老院管理系统是一套覆盖后端、前端和移动端的完整项目源码&#xff0c;以Java语言结合SSM框架、Vue前端及MySQL数据库实现&#xff0c;面向Java Web与移动端开发学习者&#xff0c;也适合需要快速搭建养老院管理后台的开发者。系统包含老人档案、床…

作者头像 李华
网站建设 2026/10/10 16:44:35

毛绒玩具打样不满意?毛绒绒平台的样品修改服务说明

收到样品后发现与预期存在差距&#xff0c;是定制流程中的常见情况。毛绒绒平台为每位客户提供样品修改服务&#xff0c;支持针对脸型、配色、毛感等细节进行调整&#xff0c;基础修改包含在打样服务费用内。这项服务的边界在哪里&#xff0c;哪些调整属于基础范围&#xff0c;…

作者头像 李华
网站建设 2026/10/10 16:44:25

信用风险评分卡建模全流程:从WOE编码到分数映射实战

简介&#xff1a;基于机器学习的信用风险等级评分系统&#xff0c;聚焦信用卡申请审批与信贷风控场景&#xff0c;面向银行、消费金融及互联网金融从业者&#xff0c;通过对申请人历史数据进行预处理、特征工程与建模&#xff0c;输出可解释的风险等级评估结果&#xff0c;辅助…

作者头像 李华
网站建设 2026/10/10 16:40:42

哈里斯鹰优化VMD-CNN的轴承故障诊断全流程解析

简介&#xff1a;面向轴承故障诊断与卷积神经网络结合的工程资源&#xff0c;适合信号处理方向的研究生、工程师及机器学习初学者。方法采用高阶变分模态分解对西储大学不同转速下的驱动端振动信号进行多层次分解&#xff0c;提取本征模式函数并削弱噪声&#xff0c;再由CNN自动…

作者头像 李华
网站建设 2026/10/10 16:36:17

自动续约条款:合同到期前不看一眼,容易被“默默续上“

有个做行政的朋友跟我抱怨&#xff1a;一份办公室保洁合同&#xff0c;本来只想签一年&#xff0c;结果第二年期满了才发现合同还在继续跑。原因很简单&#xff1a;合同里写了"期满前30日未提出异议&#xff0c;自动续期一年"。他们没人注意到这条。自动续约条款长什…

作者头像 李华
网站建设 2026/10/10 16:31:55

【PIA】电信领域个人信息保护影响评估

我怎么理解一张个人信息保护影响评估表刚开始看这张表的时候&#xff0c;我的第一感觉其实很简单&#xff1a;内容很多。表格从评估对象、责任单位、责任部门、负责人等基础信息开始&#xff0c;到具体的评估场景、评估分类、评估项&#xff0c;再到评估结果、责任人、风险简述…

作者头像 李华