在日常运维和开发机管理里,pkill 可能是被使用频率最高、但也是最容易被误用的命令之一。它能在不依赖手动遍历 ps 输出的情况下,直接按进程名、命令行参数或运行用户等信息去匹配进程并发送信号。很多人刚接触时习惯先ps aux | grep xxx找到 PID,再kill -9处理,单个进程还好,一旦要批量结束几十个 worker 或者服务动态生成的子进程,这套逻辑立刻变得低效又危险。pkill 的核心价值,就是用非常短的条件把一批符合规则的进程“捞出来”,再统一发信号处理,相当于把人工逐个瞄准换成了自动筛选。
我第一次认真研究 pkill,是在一次业务服务批量重启的场景。当时服务器上跑着几十个 php-fpm worker,内存被一些异常请求拖得很紧张,需要把旧 worker 全部结束掉,再让主进程重新拉起。用 ps 一条条找 PID 根本不现实,后来发现 pkill 加上合适的匹配方式,几秒就能完成旧进程清理。这篇文章把我这些年用 pkill 的实操经验整理出来,覆盖常规用法、进阶技巧、信号选择逻辑,以及手册里不会写的一些坑。
1. 先搞清楚 pkill 到底是什么
1.1 从 procps 工具集说起
pkill 不是一个独立内核工具,它和 pgrep、kill、ps、top 一样,属于 procps-ng 工具集。绝大多数 Linux 发行版默认都会安装这个工具集,所以你不需要额外编译安装什么东西,直接敲pkill就能看到它的参数和用法。要确认版本,可以执行pkill -V,或者在系统里查看man 1 pkill,这样能看到当前发行版对应的实现细节。
procps-ng 里几个命令的分工很有意思:pgrep 负责“按条件查找进程”,pkill 负责“按条件查找进程并发送信号”,kill 则只负责“给指定 PID 发送信号”。它们底层都依赖/proc文件系统去读取进程信息,读取的内容主要包括进程名(comm)、完整命令行(cmdline)、进程所属用户、父进程信息等。了解这个背景很重要,因为后面很多匹配问题和comm、cmdline这两个字段的差异直接相关。
1.2 pkill 本身不是“杀”进程,而是发信号
很多人以为 pkill 就是“杀进程”,这是理解上的偏差。pkill 本质上只是按条件找到一批进程,然后向它们发送一个信号。信号默认是 SIGTERM(值 15),含义是“请你正常退出”。进程收到 SIGTERM 后,可以做清理工作、保存状态、释放资源,然后退出。如果进程已经卡死,或者没有注册 SIGTERM 处理函数,那它可能不会退出,这时候你可能需要继续发送 SIGKILL(值 9)。
这个过程可以用一个生活类比理解:你给同事发一条“请把手里的事收尾后下班”的微信,这是 SIGTERM;你直接拔掉对方电脑电源,这是 SIGKILL。pkill 只是那个负责群发消息的人,至于收到消息的人怎么反应,取决于它自己的信号处理逻辑。所以遇到杀不掉的进程,别觉得 pkill 没用,问题大概率出在信号选择或者进程自身状态上。
2. pkill 的语法与核心选项拆解
2.1 基础语法和几个关键约定
pkill 的语法非常短:
pkill [选项] pattern这里的 pattern 默认被当作“扩展正则表达式”(ERE)处理,而不是简单的字符串精确匹配。它匹配的目标默认是进程的 comm,也就是进程名。注意,comm 是内核里记录的可执行文件短名称,不是你在终端敲的命令行,也不是完整路径。
举个例子,如果一个人执行了/usr/bin/python3 /home/user/reset_token.py,那么这台机器上的进程 comm 大概率是python3,cmdline 则是完整的/usr/bin/python3 /home/user/reset_token.py。默认情况下,pkill python3会匹配 comm 为python3的进程,而pkill -f reset_token.py才会匹配 cmdline 里包含reset_token.py的进程。这个差异直接决定了你匹配的精确度。
2.2 常用选项速查与选型逻辑
我用下来的高频选项主要有这几个,整理成一个表格,方便对照选择:
| 选项 | 作用 | 典型场景 |
|---|---|---|
-x | 精确匹配整个进程名,而不是子串 | 避免pkill ssh误杀 sshd |
-f | 匹配完整命令行,而不是只匹配进程名 | 杀掉 Python 脚本、Java 启动参数等 |
-u uid/用户名 | 匹配指定用户运行的进程 | 清理某个用户遗留的任务 |
-signal | 指定要发送的信号 | pkill -HUP nginx、pkill -9 firefox |
-n | 只匹配最近启动的进程 | 保留新的 worker,清理老的 |
-o | 只匹配最早启动的进程 | 定位最老的那个残留进程 |
-c | 只输出匹配数量,不发送信号 | 预检有多少进程会被命中 |
-v | 反向匹配,排除符合 pattern 的进程 | 杀掉除了某个服务外的其它相关进程 |
-P 父PID | 匹配指定父进程下的子进程 | 按父进程批量清理子任务 |
-t 终端 | 匹配在某个终端下运行的进程 | 结束某个登录会话的残留任务 |
单看选项容易记混,我总结一个选型思路:如果只关心程序名本身,用默认匹配加-x;如果程序名无法区分多个实例,就用-f去匹配命令行中的路径、脚本名、启动参数;如果要按归属清理,再用-u或者-P去缩小范围。这几个维度可以组合使用,比如pkill -u www-data -f 'php artisan queue:work',意思是只对 www-data 用户下面命令行里包含php artisan queue:work的进程发信号。
2.3 匹配优先级:先理解 comm 和 cmdline 的区别
这是 pkill 使用中最容易出问题的地方,我拿两个实际场景说明。
场景一:Redis 服务。系统里有一个 redis-server 进程,comm 是redis-server。如果你执行pkill redis,注意,因为 pattern 默认是正则表达式,它并不要求完全相等,只要 comm 包含redis这个子串就会命中。redis-server显然包含redis,所以这个命令会把 redis-server 杀掉。这还算符合直觉,但如果再执行pkill red,同样会命中 redis-server。所以“看起来像子串匹配”的行为,有时候会带来很大风险。
场景二:SSH 服务。你本意是想结束本地 ssh 客户端连接,执行pkill ssh。但系统里的 sshd 守护进程 comm 也是包含ssh的,于是 sshd 也被信号命中。如果你的机器是远程服务器,这就会直接把当前 SSH 连接干掉,严重一点还会影响后续运维。正确的精确匹配是pkill -x ssh,这样只匹配 comm 完全等于ssh的进程。
之前说过,comm 还有 15 字节长度限制。Linux 内核的 task comm 字段最大长度是 15 个字符,超过长度的部分会被截断。比如一些 Java 服务的进程名可能只有java,但你根本看不出它到底在跑哪个 jar。这时候用默认方式匹配是分不清多实例的,只能靠-f去匹配 cmdline。
3. 实操入门:从一次服务批量重启开始
3.1 先用 pgrep 做预检,确认匹配范围
任何一次 pkill 操作之前,我都建议先跑一遍 pgrep,看看到底会命中哪些进程。pgrep 和 pkill 的匹配规则、选项基本一致,但 pgrep 默认只输出 PID 和进程名,不会发信号。你可以把它理解成“模拟演练”,把即将被干掉的目标全部列出来。
以清理 php-fpm worker 为例,我先执行:
pgrep -a php-fpm输出类似:
1234 php-fpm: master process /usr/sbin/php-fpm 1235 php-fpm: pool www 1236 php-fpm: pool www看到目标之后,再决定是全部结束,还是只结束一部分。这里有个容易踩的坑:pgrep php-fpm会把 master 和 worker 全部列出来,但很多时候你只想清理 worker,让 master 重新 fork。这时候如果直接pkill php-fpm,master 也会收到 SIGTERM,整个服务可能直接停掉。正确做法是只清理 worker 进程,或者用pkill -f 'php-fpm: pool'去匹配 worker 的 cmdline。
3.2 一个典型流程:先监探再动手
继续讲这个场景。假设现在 php-fpm 有大量异常 worker,需要把 worker 全部结束,但保留 master 进程。我的操作习惯是这样的:
# 第一步:确认当前进程状态 pgrep -a -f 'php-fpm: pool' # 第二步:统计将有多少进程被命中 pkill -c -f 'php-fpm: pool' # 第三步:发送 SIGTERM,让 worker 优雅退出 pkill -TERM -f 'php-fpm: pool' # 第四步:等待几秒后复查 sleep 3 pgrep -f 'php-fpm: pool'第三步用-c统计命中数量,虽然它不会发送信号,但能帮你判断匹配规则是否过宽。如果计数与预期不符,就说明 pattern 可能写错了,这时候立刻检查pgrep -a的输出,不要继续往下执行。四步操作加在一起不到十秒钟,但能避免很多误杀事故。
3.3 从“杀不掉”到“精准清掉”:脚本进程的匹配方式
再举一个用 Python 跑脚本的例子。假设后台有一个脚本进程,启动命令是:
python3 /home/user/reset_token.py如果直接pkill python3,确实能杀掉这个脚本,但也会杀掉这台机器上所有其它 Python 进程。如果你只想结束 reset_token.py,正确匹配是:
pkill -f reset_token.py因为-f会匹配完整命令行,所以只有 cmdline 里包含reset_token.py的进程才会命中。
但-f也有副作用:它匹配的是整个命令行,所以如果有一个 bash 进程是这样启动的:bash /home/user/reset_token.py,那这个 bash 也会被信号命中。这不一定是你想要的结果。更稳妥的做法是,把脚本启动方式统一成python3 /home/user/reset_token.py,然后用pgrep -a -f reset_token.py先确认命令行列出的进程都是什么,再执行 pkill。
3.4 配置重载与“温和”信号的价值
前面提到,pkill 不只是用来杀进程,它也可以发送各种信号。最常见的用法是发送 HUP 信号让服务重新加载配置。比如常见 Web 服务一般会处理 HUP 信号来重新加载配置,但不退出进程:
pkill -HUP -x nginx不过我要特意提醒一句,这种用法在生产环境要非常谨慎。-HUP会发送给所有匹配到的 nginx 进程,包括 master 和 worker。如果 nginx 主进程收到了 HUP 信号,通常会重新读取配置并重新打开日志文件,这没问题;但如果你只是想给 master 发信号,建议先通过 pid 文件精确定位:
kill -HUP $(cat /var/run/nginx.pid)或者:
pgrep -a -x nginx pkill -HUP <具体的master PID>我见过一些运维脚本直接pkill -HUP 进程名,结果多个 worker 同时重新加载配置,瞬时负载飙高。所以用信号时第一条原则是:先确认“发送给谁”,再确认“发什么信号”。
4. 进阶技巧:组合条件、用户清理和正则匹配
4.1 扩展正则表达式的实际应用
pkill 的 pattern 使用扩展正则表达式,这意味着它支持|、*、.、[]、^这些符号。合理利用正则,可以做到非常精细的匹配。
举个例子,假设服务器上有几种 worker 进程,分别叫 worker_http、worker_grpc、worker_cron,你想把 http 和 grpc 的 worker 都结束掉,但保留 cron。可以这么写:
pkill -f 'worker_(http|grpc)'注意,因为 pattern 是正则,所以|在这里是“或者”的意思,不需要像 grep 一样加\转义。同理,如果你想匹配所有运行在/opt/app/目录下的 Python 进程,可以这么写:
pkill -f '^python3 /opt/app/'^表示从命令行开头匹配,这样能避免命中那些路径相似但不在/opt/app/下的进程。但一定要自己去权衡:这种写法虽然精确,但如果以后有人改了启动目录,这条命令就失效了。所以脚本里的匹配规则,最好写成配置项,方便后续调整,而不是埋在代码深处。
4.2 按用户清理:小心误伤整个会话
按用户匹配也是高频需求,尤其是清理开发机里某个测试账号残留的进程。比如:
pkill -u devuser这个命令会结束 devuser 用户所拥有的所有进程。看起来很方便,但风险很大:如果这个用户正在运行一个 nginx worker,或者正在终端里执行某个交互任务,也会被一并结束。更危险的是,如果用户是通过 SSH 登录开发的,登录会话的 shell 通常也由该用户持有,执行后远程终端会直接断掉。
所以我通常建议,按用户清理时一定要叠加进程匹配条件,缩小范围:
pkill -u devuser -f 'php artisan queue:work'这样只影响 devuser 下面命令行里包含php artisan queue:work的进程,其它任务不受牵连。把这个理解成“先选人,再选动作”,而不是直接对整个用户“一锅端”。
4.3 配合父进程 PID 做批量下线
还有一种场景非常适合-P:某个父进程崩溃后留下一堆孤儿子进程,你想把这些子进程全部结束,之后再由系统或者 supervisor 重新拉起。可以先查出父进程 PID:
pgrep -a -f 'parent_service.py'假设父进程 PID 是 5678,那么执行:
pkill -P 5678这条命令会结束所有父进程 PID 为 5678 的子进程。注意,它不会碰父进程本身。如果你的目的是“把整个进程树全部结束”,需要再单独对父进程发送信号:
pkill -TERM -f 'parent_service.py'先后顺序建议是先结束子进程,再发父进程,避免父进程收到信号后重新拉起子进程造成“野孩子复活”的现象。这个顺序在写自动化脚本时特别重要,否则很可能出现进程越杀越多的错觉。
4.4 不能再谨慎的预检流程
我慢慢养成了一套预检流程。在执行任何包含敏感条件的 pkill 之前,我会把它拆成两步:
pgrep -a -u devuser -f 'php artisan queue:work'然后看返回的 PID 列表是否符合预期,确认无误后再执行:
pkill -TERM -u devuser -f 'php artisan queue:work' sleep 5 pgrep -a -u devuser -f 'php artisan queue:work' && echo "仍有残留" || echo "已清理干净"很多老手会嫌这个流程啰嗦,但实际生产环境里,多出来的这几秒时间,远比一次误杀后的恢复时间要短。尤其是有多个项目部署在同一台机器上的时候,匹配规则稍微宽一点,就可能带走无关服务。
5. 跑 pkill 时最容易踩的坑和排查思路
5.1 pkill 把自己的脚本也杀了:别让正则匹配到自身
这是老手也会犯的经典错误。假设你写了一个部署脚本deploy.sh,里面执行:
pkill -f deploy.sh你本意是结束正在运行的旧部署进程,但问题是:当前这个 deploy.sh 脚本本身的命令行里,就包含deploy.sh这个字符串。于是 pkill 匹配时,会把自己也列入目标,然后给自己发送信号,脚本还没跑到下一步就中断了。
解决方式很巧妙,用正则的字符集技巧:
pkill -f '[d]eploy.sh'这条命令中的 pattern 是[d]eploy.sh,它能匹配真实的deploy.sh字符串,但当前正在执行的命令行里写的是[d]eploy.sh本身,这个字符串不会被正则[d]eploy.sh命中,因为正则要求开头是字母d,而自身命令行里开头是方括号[。所以能杀掉旧的 deploy.sh 进程,但不会杀到自己。
这个技巧不仅适用于脚本自身,也适用于所有“在命令行里包含了待匹配关键词”的场景。每次写 pkill 之前,先想想当前这条命令会不会被自己的 pattern 匹配到,能过滤掉很多奇怪的问题。
5.2 误杀同前缀进程:ssh 和 sshd 的教训
前面提到过,pkill ssh会命中 sshd,这里展开说下排查思路。有一次我帮同事排查,发现某台跳板机的 sshd 服务突然退出,日志里只有一条被 SIGTERM 结束的记录。查了半天,最后发现是有人在机器上执行过pkill ssh,本意是想断开自己的 ssh 客户端,结果把系统 sshd 也带走了。
这类“同前缀”误杀在服务端软件里非常普遍。再比如pkill java会把机器上所有 Java 进程都干掉,pkill php也会命中 php-fpm。要避免误杀,最简单的方法是使用-x精确匹配,或者加pgrep -a预检。如果发现匹配结果里混入了不想杀的进程,就用-v排除,比如:
pkill -f 'java.*gateway' -v 'java.*admin'这行的意思是,结束命令行匹配java.*gateway的进程,但排除命令行里包含java.*admin的进程。注意,-v与-f组合时,排除逻辑也是基于完整命令行的,理解这一点才能高效使用。
5.3 没有权限和僵尸进程:不是 pkill 不生效
如果你在普通用户下执行 pkill,只能向自己拥有的进程发送信号。遇到“Operation not permitted”的报错,一般有两个原因:一是目标进程属于 root 或其它用户,你权限不够;二是目标进程属于另一个更高级别的安全域,比如某些容器内的进程。
还有一种情况是目标进程已经变成僵尸进程(Zombie)。僵尸进程本身已经结束,它的 PID 和退出信息都保留在进程表里,等待父进程来回收,这时候任何 SIGKILL 都无法清除它。pkill 就算匹配到了僵尸进程,信号也不会让它“消失”,你必须处理它的父进程,让父进程调用 wait 回收子进程,或者直接结束父进程,让 init 进程代为回收。遇到僵尸进程时,可以先看/proc下的进程状态,筛选出 Z 状态进程,再去定位父进程。
5.4 D 状态进程和循环重启的假象
比僵尸进程更棘手的是 D 状态(不可中断睡眠)的进程。这种状态通常出现在进程等待内核 IO 操作完成时,比如磁盘卡住、NFS 挂载无响应。处于 D 状态的进程不响应 SIGTERM,也不响应 SIGKILL,因为内核认为此时强制终止可能会导致数据不一致。pkill 发信号后,目标进程不会退出,于是你看到的现象就是“怎么杀都杀不掉”。
排查这种问题时,先用ps -eo pid,stat,comm,cmd | grep 进程名看看状态。如果确实处于 D 状态,只能等待底层 IO 恢复,或者重启系统来恢复稳定。D 状态不是 pkill 能力不足,而是内核层面不允许打断不可中断睡眠。
6. 几年跑下来,我总结的 pkill 使用习惯
说了这么多,最后分享几个我在日常操作中形成的习惯。第一个习惯是给 pkill 设置“准入门槛”:任何非紧急操作,都先用 pgrep 预检,确认匹配数量与目标完全一致后再动手。第二个习惯是默认优先发送 SIGTERM,而不是 SIGKILL,给服务留出优雅退出的时间,必要时再升级信号。第三个习惯是在脚本里绝不用模糊的短名字去匹配,比如不带-x的pkill php,这种写法在高压环境里迟早会出事。
还有一个细节值得一提:如果服务是通过 systemd 管理的,尽量不要用 pkill 去杀它的主进程,正确方式是用systemctl restart 服务名,让守护进程按编排好的生命周期去停止和启动程序。pkill 适合处理的是那些临时任务、进程树、多实例 worker,而不是替代系统服务管理器。
我在实际使用中还会习惯性地把“预检、清理、复查”三个动作绑定成一条流水线,即使是在非常紧急的情况下,也会先快速敲一遍pgrep -a瞥一眼结果再继续。这套动作练熟了之后,pkill 用起来反而比手动 kill PID 更稳、更快。毕竟批量清进程这件事,真正的重点从来不是把命令敲多快,而是每一次敲出去,都能确定它会带走哪些进程,留下哪些进程。