资讯详情

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

📅 2026/10/10 16:48:17 | 华诺云谱 👁 阅读
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:80grep -c timeout返回 1grep -o timeout | wc -l返回 2。别看差别小写监控脚本的时候非常关键。比如判断某个接口在最近日志里超时次数是否超过阈值如果用grep -c一次请求里重复出现多次超时就被漏掉了数据失真。如果你要统计多个文件里的总次数可以这样grep -o timeout /var/log/app/*.log | wc -lgrep -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 | uniqawk 自己也能当计数器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 patternerror {n gsub(pattern, pattern)} END {print n} app.loggsub返回替换的次数这里把匹配到的内容替换成自身不会改变原行但能拿到次数。这个技巧在文件特别大、不想起多个管道进程时很管用。1.3 在多文件、整个目录里统计总次数有时候要统计一个目录下所有日志文件中某个关键词的总次数而不是单文件。查找加统计一起做常用 find 配合 grepfind /data/logs -type f -name *.log -exec grep -o -h error {} | wc -l这里加了-h--no-filename去掉文件名前缀避免干扰统计。如果不用-h每个匹配项前面会带文件路径:errorwc -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和-0find /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 --colorauto python这一行如果你不加处理就把第二列的 PID 挑出来杀掉容易把 grep 命令自己杀了。一般无伤大雅但会让脚本后续逻辑变得混乱。一个小技巧是用grep [p]ython这样 grep 进程的命令行是grep [p]ython正则表达式匹配不到它自己。第二更推荐用pgrep来找进程。pgrep专门按名称或命令行匹配 PIDpgrep -a python-a表示同时显示 PID 和完整命令行先看再杀比ps aux | grep干净得多。不过默认pgrep只匹配进程名不匹配参数。如果你要按完整命令行匹配比如启动命令里包含--worker1就要加-fpgrep -f -- --worker1一般我们用“服务名”或“命令行关键字”找进程的场景都要用到-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 killxargs -r表示如果 pgrep 没有输出xargs 不执行避免报错。或者一行搞定pkill -f python app.pypkill和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是 SIGKILLkill -15是 SIGTERM。先发 SIGTERM给进程几秒钟善后再发 SIGKILL是通用礼貌原则。直接-9虽然粗暴有效但可能导致数据文件损坏、连接状态残留。我的习惯是优先kill单次失败再kill -9。2.3 父子进程和僵尸进程的注意事项单个进程终止后如果它还有子进程子进程不一定会跟着死而是被系统顶层进程收养。某些场景下我们想连子进程一起清理比如父进程是调度器杀掉父进程后一堆 worker 还在继续跑继续占资源。这时候可以用pkill -P 父PID按父进程 PID 匹配所有子进程pkill -P 12345最好是先杀子进程再杀父进程避免父进程检测到子进程退出后重新拉起新的子进程。僵尸进程不能用 kill 杀死因为它本身已经死了只是父进程还没有调用 wait 来回收它的进程表项。处理僵尸进程的正确方式是杀掉它的父进程或者让父进程退出后由系统回收。我之前遇到过某个常驻脚本产生一堆僵尸子进程排查时先找到僵尸进程的 PPIDps -eo pid,ppid,stat,cmd | grep Z确认是哪个常驻服务再决定是否重启那个服务而不是对着僵尸 PID 一顿 kill。说到误杀我自己有一次在清理测试环境时想杀掉所有和某开发框架相关的进程直接用了pkill -f 框架名。结果一瞬间把自己的终端断连了原因是当前终端所在的环境里也有匹配该字符串的进程更关键的是我在命令行输入的命令本身也包含这段字符串pkill 匹配到了当前 shell。那之后我给自己定了三条原则批量杀之前先pgrep -a看完整命令。pkill 的匹配字符串尽量用更长的、唯一的路径或参数比如pkill -f ^/usr/bin/python.*app.py。不在涉及生产环境的自动化脚本里直接 pkill先输出候选 PID 再人工确认。3. 清空文件truncate、重定向与日志轮转的选择3.1 几种清空方式本质上的区别清空一个日志文件最常用的写法有 app.log、: app.log、cat /dev/null app.log。它们本质都一样以截断方式打开文件把文件大小变成 0。区别在于 app.logshell 重定向如果文件不存在会创建存在则截断。: app.log内建命令:什么都不干但重定向依然生效在某些脚本里看起来更语义化。cat /dev/null app.log实际读一下空文件再写效果相同但多启动一个进程不推荐。还有一个工具叫truncatetruncate -s 0 app.logtruncate 直接把文件大小调整为 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 -luniq -c统计频率结果不对uniq 只能合并连续相同行没先 sort先sort再uniq -cpgrep 找不到某个进程默认按进程名匹配没按完整命令行匹配使用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_ALLC grep -o error big.log | wc -l我实测过一个大文件在默认 locale 下 grep 要跑十几秒加LC_ALLC后只需要几秒。如果你的日志不包含中文放心用。第二个细节是避免无谓的管道进程。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 PATTERNOutOfMemoryError THRESHOLD10 # 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_RUN1判断真正跑之前打印出即将执行的进程列表或文件列表。这个习惯帮我避免过好多次事故也希望你能用上。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑