资讯动态

pgrep 进程存活判断:告别 ps aux | grep 误报

发布时间:2026/9/30 4:09:33 来源:尧图企业网站定制
线上巡检脚本突然开始误报说某个 Java 服务挂了登上去一看进程活得好好的。捞代码一看判断存活用的是ps aux | grep java | grep -v grep而那天运维顺手在服务启动参数里加了-Dspring.profiles.activejava-test于是同一台机器上跑着的一个测试脚本也被算了进去数量对不上脚本就判了挂了。这种事故我见过太多次根子不在业务逻辑而在于用文本过滤工具去做进程身份识别。pgrep就是为这件事设计的工具它直接读内核暴露的进程信息用正则匹配进程名或完整命令行还能直接返回数量、按用户和父进程过滤配合返回码可以让 shell 脚本的存活判断变得非常干净。下面我把这套东西从参数语义、匹配边界、脚本封装到踩坑排查完整讲一遍适合已经会敲基本 Linux 命令、但还没把进程管理用利索的同学。1. 先把 pgrep 和 ps 管道流的差别掰开说清楚1.1 那句 ps aux | grep xxx 到底错在哪很多人写进程判断的肌肉记忆就是ps aux | grep nginx。这条命令不是不能用而是它把三件不同的事混在了一起列出所有进程、做文本过滤、再靠人眼或管道里的行数做判断。每一步都有噪声。首先是grep自身会出现在结果里所以你得再加一个grep -v grep管道越接越长。其次是ps aux输出的是格式化后的文本列用户列、CPU 列、命令列都可能碰巧包含你搜索的关键词比如一个叫nginx-exporter的监控进程会被grep nginx捞出来业务进程和监控进程混在一张表里。最后也是脚本判断最容易崩的一点管道中最后一条命令的退出码才是整条管道的退出码ps aux | grep xxx的退出码来自grep稍微改一下接上wc -l之类的退出码语义就彻底丢了。pgrep的做法完全不同它不解析任何格式化文本而是直接遍历/proc目录下的每个进程目录读取内核维护的进程元数据再用你给的正则去匹配。没有中间文本层也就没有列名碰巧撞上关键词的问题。它输出的是纯粹的 PID一行一个可以直接喂给kill、wait、数组赋值。1.2 pgrep 的返回码约定脚本判活的核心依据pgrep的退出码定义得跟grep一个思路但用在进程判断上极其顺手退出码含义脚本里的典型处理0至少匹配到一个进程认为服务存活1一个都没匹配到认为服务已退出2命令行语法错误脚本本身写错了需要修3致命错误比如无法读取进程信息环境异常需要人工介入这意味着判断存活根本不用解析输出一行就够if pgrep -x nginx /dev/null; then echo nginx is alive else echo nginx is gone fi我把 /dev/null加在这里是有意的因为存活判断只关心退出码不需要 PID 文本。有人喜欢写成[ -n $(pgrep -x nginx) ]功能上等价但多了一次命令替换和字符串比较在每秒执行多次的监控循环里不划算。更关键的是命令替换会吞掉退出码如果哪天想把判断逻辑换成基于返回码的写法很容易改出 bug。注意pgrep在不给任何模式参数时会打印用法说明并以退出码 2 结束它不会像ps那样无条件列出全部进程。想列全部进程还是得用ps别指望pgrep兜底。1.3 默认只认 15 个字符comm 字段的真实长度这是我认为最需要提前知道的一个事实不知道它的人迟早会踩。pgrep在不加-f的时候匹配的是进程的comm字段也就是内核里那个进程名。这个字段的长度上限是 16 字节其中最后一个字节留给字符串结束符所以实际能显示的进程名最多 15 个字符。超长的进程名会被内核截断。你可以自己验证# 查看某个进程内核里的名字 cat /proc/$(pgrep -n java)/comm # 对比它完整的启动命令 tr \0 /proc/$(pgrep -n java)/cmdline; echo如果你启动的是一个自定义程序二进制名叫payment-gateway-service那么pgrep payment-gateway-service大概率什么也匹配不到因为内核里存的名字已经被砍成了payment-gatewa。这种情况只有两个办法要么用-f去匹配完整命令行要么直接匹配被截断后的前缀。我个人的习惯是凡是自己写的服务二进制名尽量控制在 15 字符以内比如paygw、orderd。这不只是为了pgrep好写ps、top的输出也会整齐很多排查问题时一眼就能认出来。这是很便宜的一个工程习惯收益却持续很多年。2. 从拿到进程号到数清数量常用参数的分工2.1 默认、-l、-a 三种输出形态怎么选pgrep的输出形态有三个档位选哪个取决于你下一步要干什么。默认形态只输出 PID适合脚本消费pgrep sshd # 812 # 1204 # 3391加-l会在 PID 后面补上进程名人眼扫的时候好用pgrep -l sshd # 812 sshd # 1204 sshd加-a则会显示完整命令行这个选项在很多老版本的 procps 里没有需要相对较新的版本才支持pgrep -a sshd # 812 /usr/sbin/sshd -D # 1204 sshd: user1 [priv]-l和-a的选择逻辑很简单你想确认匹配到的到底是不是我要的那个进程就用-a因为完整命令行里有启动参数信息量最大只是想快速扫一眼有几个、名字对不对-l足够。脚本里永远用默认形态因为多出来的文本会让后续处理变复杂比如kill $(pgrep -l x)会直接把进程名当成参数传进去报一堆错。配-d可以自定义分隔符这个在把 PID 列表塞进某些工具时有用pgrep -d, nginx # 1204,1205,12062.2 -c 计数的两个反直觉行为-c是直接输出匹配到的进程数量看起来平平无奇但有两个行为必须知道。第一个是即使匹配数量为 0-c也会老老实实打印一个0但退出码仍然是 1。也就是说输出和退出码在这里是分裂的。很多人写监控时习惯用pgrep -c nginx的输出做数值比较同时又开了set -e结果在服务没启动的时候脚本直接退出连日志都没打。我一般的写法是把退出码和数值分开处理count$(pgrep -c nginx || true) echo nginx process count: $count那个|| true就是为了不让set -e在这个位置提前终止脚本。第二个是-c的效率明显高于pgrep nginx | wc -l。前者在匹配过程中直接累加计数不需要为每个 PID 生成输出行、再经过管道传递、再被wc逐行解析。在进程数量上千的机器上做高频巡检这个差异是能测出来的。而且管道版本的退出码永远来自wc -l恒为 0你根本没法同时拿到数量和是否存在两个信息。提示-c和-v反向匹配组合起来很好用pgrep -c -v -u root可以直接统计非 root 用户的进程数量写资源审计脚本时省事。2.3 -u、-P、-t 做维度过滤光靠进程名匹配有时候不够精准因为不同用户下可能跑着同名进程。这时候应当按照进程的其他属性做过滤pgrep支持好几个维度。-u按有效用户过滤这个在排查同一个服务被两个用户各起了一份时特别有用pgrep -u www-data nginx pgrep -u root,jenkins java这里有个容易混淆的点-u匹配的是 effective UID-U匹配的是 real UID。对于设置了 setuid 的程序这两个值不一样。绝大多数情况下你用-u就够了但如果你在审计权限相关的场景需要明确自己关心的是哪一个。-P按父进程 PID 过滤这是我在看服务启动链路时用得最多的选项。比如你想知道某个 master 进程下面挂了几个 workerpgrep -P 1204再比如容器里主进程通常是 PID 1想列出容器内所有直属子进程pgrep -P 1 -a-t按控制终端过滤pgrep -t pts/0能列出某个 SSH 会话里跑着的所有前台后台进程。它还有一个约定俗成的写法pgrep -t -用来匹配没有控制终端的进程也就是那些被 daemon 化、被 systemd 接管或者被 nohup 丢到后台的进程。这个写法看着奇怪但确实是文档里定义的行为值得记住。2.4 -n 与 -o只想要一个 PID 的时候有些场景你只关心最新的那一个。最典型的是日志轮转脚本需要找到刚启动的那个进程发信号kill -HUP $(pgrep -n nginx)-n取最新-o取最旧两者互斥不能同时给。这里的新和旧在实现上是按 PID 数值大小比较的因为 PID 是递增分配的绝大多数情况下等价于启动顺序。但要留个心眼PID 是会回绕的在一台长时间运行、PID 已经翻过一圈的机器上这两个选项的含义会变得不可靠。如果业务对最新启动严格敏感更稳的做法是用ps按启动时间排序或者直接读/proc/PID/stat里的启动时间字段自己比。这个坑平时不容易碰到但一旦碰到就是很难解释的诡异行为。3. 进程名匹配里的边界问题\b 到底管什么用3.1 为什么 pgrep nginx 会多出一堆结果pgrep默认把模式当作扩展正则表达式来做子串匹配不是精确匹配。所以pgrep nginx会把nginx、nginx-exporter、nginx-conf-watcher、openresty-nginx全部匹配出来只要进程名的某个位置出现了连续的这几个字符。我自己踩过一次真实的坑一台机器上同时跑着node和node_exporter巡检脚本里的pgrep node永远返回两个 PID导致基于数量的告警一直抖动。当时的错误做法是去调告警阈值正确做法是收窄匹配条件。收窄的手段有三种效果和适用场景不一样手段写法语义适用场景单词边界pgrep \bnginx\b只匹配独立成词的 nginx进程名前后可能接短横线、点号、数字锚点pgrep ^nginx$从开头到结尾必须完全一致进程名确定且不含特殊字符精确匹配pgrep -x nginx整个进程名与模式完全相等最推荐语义最清晰3.2 \b 单词边界的原理与写法\b表示单词边界它的判定规则是一侧是单词字符另一侧不是。在正则的语境里单词字符指的是字母、数字和下划线除此之外的字符短横线、点号、斜杠、空格、冒号都算非单词字符。所以\bnginx\b的意思是nginx 左边要么是字符串开头、要么紧挨着一个非单词字符右边同理。放到进程名上nginx-exporter里的nginx右邻是短横线短横线是非单词字符所以\b会在这里成立\bnginx\b依然会匹配到nginx-exporter。这是很多人误解的一点以为加了\b就等于精确匹配了。实际上\b只能拦住mynginx这种前面直接贴着字母数字的情况拦不住用分隔符连接的场景。那\b什么时候真正有价值主要是在-f模式下匹配完整命令行的时候。命令行里有空格、斜杠、点号用\b能精准锁定参数级别的位置pgrep -f \bnginx\b这条命令能匹配到/usr/sbin/nginx -c /etc/nginx/nginx.conf因为 nginx 出现在路径的末尾左边是斜杠右边是空格或字符串结尾两侧都是边界。它也能匹配到nginx: worker process因为右边是冒号。再补一个转义的细节这个细节坑过不少人。在 bash 里写pgrep \bnginx\b是可行的因为\b不是 shell 双引号里的转义序列b不是特殊字符所以反斜杠会被原样保留传给 pgrep。但一旦你写成pgrep \\bnginx\\b传过去的就是两个反斜杠加 b正则引擎会把它理解成别的意思直接匹配失败。养成用单引号包正则的习惯最省心pgrep \bnginx\b注意\b是 GNU 正则实现提供的扩展写法并不属于 POSIX 扩展正则的正式语法。在 glibc 环境的发行版上都能用但在用 musl 或 busybox 的精简环境里可能不支持或者行为不一致。给嵌入式设备写脚本时老老实实用^...$锚点更保险。3.3 一条命令验证你的模式写得对不对正则写错了不容易发现因为它只是匹配不到或者匹配多了不会报错。我习惯在正式写进脚本之前先用-a把匹配结果打出来看一眼pgrep -a \bnginx\b-a会同时显示完整命令行这样你能直观判断是不是捞进了不该捞的进程。这个步骤花不了十秒钟但能省掉后面几小时的排查。写完确认没问题再把这个模式固化到脚本里同时留一行注释说明为什么要用这个模式比如为什么不能用-x因为进程名被截断了或者因为-f模式下要匹配参数。如果是先跑通了后面又突然开始多匹配多半是环境里新部署了名字相似的进程。这种时候我建议直接把验证命令写进巡检脚本的日志里每次执行都打印一次匹配到的完整命令行出了问题翻日志一目了然。3.4 别把 -w 当成单词边界选项网上有些文章会把pgrep -w说成按单词匹配这是错的。在 procps-ng 的实现里-w是--lightweight的短选项作用是把线程 ID 也列出来跟单词边界一点关系都没有。这两个字母碰巧一样误导了不少人。真正跟整体匹配相关的选项是-x。我的建议是能用-x的地方优先用-x它做的是字符串完全相等比较语义最直白不需要你懂正则只有在-x无法满足比如进程名被截断、或者需要匹配命令行的一部分时才退回到正则加锚点或单词边界的方案。4. -f 全命令行匹配威力大误伤也大4.1 -f 匹配的是什么加-f之后pgrep不再只看那 15 个字符的进程名而是读取/proc/PID/cmdline把里面用 NUL 分隔的参数拼成空格分隔的字符串然后拿你的正则去匹配整个字符串。这意味着你可以匹配到启动参数级别的东西pgrep -f java.*order-service pgrep -f python3 /opt/scripts/sync.py pgrep -f --config/etc/app/prod.yaml这是-f最大的价值也是它最危险的地方。因为完整命令行往往很长包含路径、参数、临时文件名任何一段被意外匹配到都会造成误伤。最经典的场景是你在一个 shell 里执行pgrep -f some_pattern而这个 shell 自己的命令行里恰好就含有这串字符于是它会把自己也匹配进去。虽然pgrep会主动排除自己这个进程但排除不了你的登录 shell 或者包着你的那些父进程。4.2 用 \b 给 -f 兜底-f模式下我强烈建议把模式写具体一些不要只给一个裸词。裸词的风险在于它可能出现在路径的中间。比如pgrep -f app会匹配到/home/deploy/apps/legacy-thing因为它包含 app 这三个字母。把\b加上并且尽量带上参数的上下文pgrep -f \bapp\b.*\bprod\.yaml\b这样匹配的就必须是独立成词的 app后面接一段任意字符再接独立成词的 prod.yaml。约束越多误伤越少。当然也不能走极端把模式写到只有精确命中启动命令才能匹配那样配置一旦调整脚本就失效。我一般的分寸是锁定入口脚本或二进制路径加一个特征参数剩下的用.*通配。4.3 排除自己-A 与祖先进程新版本的 procps-ng 提供了-A也就是忽略祖先进程。它的作用是把自己以及自己的所有父进程从匹配结果里剔除。这个选项在pkill场景下是救命的在pgrep场景下也很有用能省掉手动过滤的麻烦pgrep -f -A sync.py如果你的环境版本比较老没有-A那就得自己过滤。一个实用写法是把当前 shell 的进程树排除掉mypid$$ pgrep -f sync.py | grep -v -w $mypid不过说实话这种手动过滤很容易漏因为祖先链条上不止一个进程。所以我更推荐的做法是尽量不用-f做宽泛匹配把模式写紧从源头上避免匹配到无关进程。5. 把 pgrep 写进 shell 脚本的三种典型套路5.1 存活判断不要解析输出只看返回码前面提过最干净的方式是直接用退出码。我给一个完整的封装函数这个函数我在很多脚本里都复用is_running() { local pattern$1 pgrep -x $pattern /dev/null 21 } if is_running nginx; then echo ok else echo not running fi这个函数的要点有三个。第一用-x做精确匹配避免子串误伤。第二重定向 stdout 和 stderr保证调用方不会被多余输出干扰。第三函数返回的是pgrep的退出码调用方if直接判断即可不需要中间变量。如果要判断的是带参数的 Java 服务把-x换成-f同时模式里带上足够特征比如主类名或者 jar 包名。5.2 数量统计-c 与 wc -l 的口径差异需要数量的时候-c和pgrep | wc -l的差别不只是性能还有口径。-c统计的是匹配到的进程数一个进程算一个。而pgrep默认输出的是主线程的 PID如果加上了-w选项输出里会包含线程 ID这时候再用wc -l去数得到的就是线程数而不是进程数。这两个数字在 Java 这类多线程程序上能差出几十倍。所以我的建议是做进程数量统计要么用-c要么用pgrep不带-w的输出接wc -l二者结果一致。做线程数量统计明确用pgrep -w -c并且清楚自己在数什么。脚本里的变量名最好也体现出来叫proc_count还是thread_count一眼就能看懂。补一个实操中的细节-c的输出是纯数字加换行可以直接参与算术运算wc -l的输出在很多实现里会带前导空格虽然$(( ))能自动处理但如果你要做字符串拼接展示最好先做一次清洗。5.3 等待与超时轮询直到进程消失停止服务的时候发完停止信号通常需要等一会儿再确认。这段等待逻辑写不好就是各种诡异的时序 bug。我一般用轮询加超时的方式stop_and_wait() { local name$1 local timeout${2:-30} local waited0 pkill -x $name 2/dev/null || true while pgrep -x $name /dev/null 21; do if [ $waited -ge $timeout ]; then echo timeout waiting for $name to exit 2 return 1 fi sleep 1 waited$((waited 1)) done return 0 }这段逻辑里有几个刻意的设计。pkill后面接|| true是因为进程本来就不存在时pkill会返回 1在set -e环境下会导致脚本中断而这个场景其实是正常的。循环条件是pgrep每轮 sleep 1 秒把等待时间累加。超时后再返回错误码而不是无限等下去。提示如果进程在退出过程中会有一段时间处于僵尸状态pgrep仍然能匹配到它这时候简单的存活判断会一直为真。遇到这种情况需要额外检查进程状态读/proc/PID/stat的状态字段判断是不是 Z。6. 几个真实踩过的坑与排查链路6.1 -c 输出 0 但退出码是 1这个坑前面提过这里展开讲一下排查链路因为它的表现很有迷惑性。现象是脚本半夜偶尔失败退出日志里只留下一句nginx process count: 0看起来数量判断是对的但脚本就是停了。排查顺序是这样的。第一步看脚本有没有开set -e大概率是开了。第二步看报错位置如果是在赋值语句之后立即退出那基本可以确定是命令替换里的退出码传出来了。第三步验证手动执行pgrep -c 不存在的进程名; echo $?会看到输出 0退出码 1。到这里根因就清楚了pgrep -c在无匹配时选择输出 0 但返回失败这个设计逻辑上是合理的数量是 0同时没有找到这个语义用退出码表达但对脚本不友好。修复方案就是在命令替换后面补上|| true或者改成先判断存活再统计。我个人更倾向于前者改动最小语义也清楚。顺带说一个相关的坑$(...)命令替换会捕获 stdout如果pgrep因为某些原因往 stderr 打了警告而你没处理这个警告会直接出现在终端上看起来像是脚本出了随机错误。养成在命令替换里同时处理 stderr 的习惯。6.2 杀进程杀到自己pkill -f 的事故复盘有个同事在远程会话里执行了一条pkill -f ssh本意是清理一个卡住的 SSH 隧道辅助进程结果把自己当前这条 SSH 会话的进程也匹配上了会话当场断开。更糟的是他是在一个跳板机上操作上游的会话也一起没了。这个事故的排查链路很简单但事后复盘的价值在于提前预防。pkill -f匹配的是完整命令行你当前 shell 的父进程链条上很可能就有一两个进程的命令行包含你搜索的关键词。pkill会排除自己但排除不了父进程。预防手段有三条我按推荐度排序。第一尽量用-x替代-f绝大多数清理场景其实是知道准确进程名的。第二如果必须用-f先用pgrep -f -a把要杀的目标完完整整列出来看一遍确认没有多余的行再执行pkill。第三先杀一个试水pgrep -f pattern | head -1 | xargs kill确认目标正确再批量处理。这种事故的共同特征是不可逆所以在按下回车之前的十秒确认价值远大于事后补救。6.3 容器与 PID 命名空间下的看不见在容器里用pgrep有个天然限制它看到的是当前 PID 命名空间里的进程。容器里的 PID 1 是容器的主进程宿主机上的其他进程在容器里通常不可见反过来宿主机上默认也看不到容器内部的进程除非在宿主机的命名空间里操作。这带来几个实际影响。第一在容器里执行pgrep sshd大概率什么都找不到因为容器里根本没有 sshd这很正常不要误判成工具坏了。第二在宿主机上想找容器内某个进程用pgrep -f匹配到的可能是containerd-shim之类的外层进程而不是业务进程本身。第三如果你写了一个统一的巡检脚本同时在宿主机和容器里运行同一段判断逻辑在两边看到的进程集合是完全不同的需要在脚本里明确区分运行环境。排查这类问题时先用ps -ef看一眼当前命名空间里到底有什么再决定用什么模式匹配就行了。如果发现匹配结果跟预期完全对不上先确认命名空间再怀疑正则。7. 什么时候该用 pgrep什么时候该换工具把前面这些串起来我给一张选型对照表方便按场景直接查场景推荐做法理由脚本判断服务是否存活pgrep -x name看退出码无文本解析返回码语义明确统计进程数量pgrep -c name一次性拿数量效率高于管道进程名超过 15 字符pgrep -f 特征参数comm 字段被内核截断名字相似的进程混在一起pgrep -x或pgrep ^name$避免子串误伤只想要最新启动的那个pgrep -n注意 PID 回绕实现上是取 PID 最大值清理进程先pgrep -a确认再pkill避免不可逆的误杀需要 CPU、内存、启动时间等详细信息用ps或直接读/procpgrep 只负责筛选不做信息展示这张表的核心逻辑是pgrep的定位是筛选器它负责在成千上万个进程里找出你要的那几个把 PID 交出来剩下的事情交给别的工具。把它当筛选器用它会非常可靠把它当信息展示工具或者判断逻辑的万能钥匙就会开始出问题。我用这套东西管理过几百台机器上的服务生命周期最深的体会是进程识别这件事宁可把匹配条件写得窄一点、多花几秒确认也不要贪图一个宽泛的模式省事。因为匹配太宽导致的后果是误杀和误判而这两件事在自动化脚本里都是放大器——一次错误的判断会被循环执行几十遍。至于\b这个语法把它理解成用来切分路径和参数的自然边界就够了指望它实现精确匹配还是老老实实换成-x更踏实。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑