kill 命令详解与参数实测(Ubuntu 22.04)

kill 是 Linux 进程管理的基石:本文在 Ubuntu 22.04 逐项实测 bash 内置与 procps 双形态,覆盖 kill -l 信号全表、默认 TERM 与 kill -0 探测、-9/-SIGKILL/-n/-s 四种等效写法、TERM 与 KILL 的可捕获性与清理差异 trap 实证、HUP 热重载、作业号与 kill 0 整组投递、权限与退出码边界,并给出按模式批量终止、TERM 超时 KILL 兜底等实战组合。

Linux命令Linux系统UbuntuUbuntu 22.04命令行进程管理2026/10/15

kill 命令详解与参数实测(Ubuntu 22.04)

目标:全面掌握 kill 的每一个常用参数与信号机制——信号全表查询(kill -l)、默认 SIGTERM 与 kill -0 存活探测、-9/-SIGKILL/-n 9/-s KILL 四种等效写法、SIGTERM 与 SIGKILL 的可捕获性与清理行为差异(trap 实证)、SIGHUP 热重载、作业号 %1、kill 0 整组投递、错误与权限边界(rc=1/137、Operation not permitted),全部命令与回显已在 Ubuntu 22.04(bash 5.1 内置 kill + procps-ng 3.3.17)实测通过。

一、环境信息

本节记录实测环境与演示数据,便于读者对照复现。kill 是 Linux 进程管理的核心命令,既是 bash 内置命令,也存在 procps 软件包提供的外部版本,两种形态在本次实测中都会覆盖。

  • 操作系统:Ubuntu 22.04.5 LTS(x86_64)
  • Shell 与内置 kill:GNU bash 5.1.16(1)-release(见 2.3 实测)
  • 外部 kill:procps-ng 3.3.17(/usr/bin/kill 与 /bin/kill 同属该包,见 2.3 实测)
  • 演示目录:/root/demo/kill/
  • 测试用户:普通用户 tester(uid=1012,gid=1016,用于权限边界实验,创建命令见 2.1)
  • 演示进程:sleep 300、sleep 120(长睡眠进程,作为"被管理的目标进程"),以及三个演示用 trap 工作脚本 trap-term.sh、trap-worker.sh、cleanup-worker.sh、reload-worker.sh、stubborn-worker.sh

二、命令概览

2.1 语法与测试数据准备

kill 的职责只有一句话:向指定进程发送指定信号。帮助原文给出的语法是:

kill [-s sigspec | -n signum | -sigspec] pid | jobspec ... or kill -l [sigspec]

即「指定信号的方式(-s 信号名 / -n 信号编号 / -信号编号 三选一)+ 一个或多个 PID 或作业号」。不指定信号时默认发 SIGTERM(15)。它同时支持 kill -l 查询信号表。

kill 在系统里有两种形态:bash 内置命令和 procps 包的外部命令。内置版支持作业号(kill %1)且在进程数达到上限时仍可用,外部版则提供更规整的 --help/--version 与 -L 表格输出,日常交互 Shell 中默认命中内置版。

先准备测试环境:创建普通用户 tester(权限实验用)、演示目录,并启动演示进程。以下演示均在 root 登录 Shell 中执行:

id tester >/dev/null 2>&1 || useradd -m -s /bin/bash tester
mkdir -p /root/demo/kill && cd /root/demo/kill

useradd 静默完成,用 id tester 确认用户已创建;随后启动一个演示进程并用 kill -0 验证它已就绪(探测命令详解见 3.3):

root@ubuntu2204:~/demo/kill# id tester
uid=1012(tester) gid=1016(tester) groups=1016(tester)
root@ubuntu2204:~/demo/kill# sleep 300 & SPID=$!; echo demo_pid=$SPID; kill -0 $SPID; echo probe_rc=$?
demo_pid=247
probe_rc=0

💡 说明:后文各节为了演示会反复启动与终止 sleep 300 等进程,PID 每次都不同,属正常现象;每节内的 PID 与回显来自同一次连续实测,前后一致。

2.2 --help 全量输出(逐行照抄)

bash 内置 kill 没有 --help 长选项,用 help kill 查看内置帮助,输出逐行照抄如下:

root@ubuntu2204:~/demo/kill# help kill
kill: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ... or kill -l [sigspec]
    Send a signal to a job.

    Send the processes identified by PID or JOBSPEC the signal named by
    SIGSPEC or SIGNUM.  If neither SIGSPEC nor SIGNUM is present, then
    SIGTERM is assumed.

    Options:
      -s sig	SIG is a signal name
      -n sig	SIG is a signal number
      -l	list the signal names; if arguments follow `-l' they are
    		assumed to be signal numbers for which names should be listed
      -L	synonym for -l

    Kill is a shell builtin for two reasons: it allows job IDs to be used
    instead of process IDs, and allows processes to be killed if the limit
    on processes that you can create is reached.

    Exit Status:
    Returns success unless an invalid option is given or an error occurs.

帮助原文直接点明了内置 kill 存在的两个理由:支持作业号、进程数达上限时仍可执行。外部版 /bin/kill --help 输出如下(逐行照抄):

root@ubuntu2204:~/demo/kill# /bin/kill --help

Usage:
 kill [options] <pid> [...]

Options:
 <pid> [...]            send signal to every <pid> listed
 -<signal>, -s, --signal <signal>
                        specify the <signal> to be sent
 -q, --queue <value>    integer value to be sent with the signal
 -l, --list=[<signal>]  list all signal names, or convert one to a name
 -L, --table            list all signal names in a nice table

 -h, --help     display this help and exit
 -V, --version  output version information and exit

对比两份帮助可见关键差异:外部版多了 -q(随信号携带一个整数值,即 sigqueue 语义)与表格化的 -L;内置版独有作业号支持。二者发信号的核心能力完全一致。

2.3 版本信息

用 type -a 一次看清两种形态,再分别确认版本:

root@ubuntu2204:~/demo/kill# type -a kill
kill is a shell builtin
kill is /usr/bin/kill
kill is /bin/kill
root@ubuntu2204:~/demo/kill# /bin/kill --version | head -2
kill from procps-ng 3.3.17
root@ubuntu2204:~/demo/kill# bash --version | head -1
GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)

/usr/bin/kill 与 /bin/kill 是同一程序(Ubuntu 22.04 中 /bin 是指向 /usr/bin 的符号链接),来自 procps-ng 3.3.17。

三、参数逐项实测

3.1 信号全表:kill -l 与 /bin/kill -L

kill -l(list)不带参数时列出当前系统支持的全部信号。bash 内置版按 5 列网格输出「编号)信号名」,实测全表照抄:

root@ubuntu2204:~/demo/kill# kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

注意编号从 31 直接跳到 34:32/33 被用户态线程库(NPTL)内部占用,不对外作为标准信号使用。外部版 /bin/kill -L(table)以 7 列紧凑格式列出 31 个标准信号(不带 SIG 前缀、不含实时信号):

root@ubuntu2204:~/demo/kill# /bin/kill -L
 1 HUP      2 INT      3 QUIT     4 ILL      5 TRAP     6 ABRT     7 BUS
 8 FPE      9 KILL    10 USR1    11 SEGV    12 USR2    13 PIPE    14 ALRM
15 TERM    16 STKFLT  17 CHLD    18 CONT    19 STOP    20 TSTP    21 TTIN
22 TTOU    23 URG     24 XCPU    25 XFSZ    26 VTALRM  27 PROF    28 WINCH
29 POLL    30 PWR     31 SYS

两份表的解读:日常运维真正高频使用的只有 1(HUP)、2(INT)、9(KILL)、15(TERM)、18(CONT)、19(STOP)这几个;SIGRTMIN~SIGRTMAX 是 34 个实时信号(RT),多用于应用程序间自定义通信。

💡 技术拓展:进程收到信号后如何处置,由「信号处置模型」决定,常见处置方式对比如下——

处置方式原理/特点典型信号/适用场景
默认终止 Term内核直接按默认动作终止进程TERM、KILL、HUP、INT
默认终止+核心转储 Core终止并生成 core 文件便于事后分析QUIT、SEGV、ABRT
默认忽略 Ign收到后什么都不做CHLD、URG、WINCH
默认停止 Stop暂停进程执行,等待 CONT 恢复STOP、TSTP、TTIN、TTOU
程序捕获 Catch程序注册 handler 自定义处理(可优雅退出/热重载)TERM、HUP、USR1/USR2
不可捕获/不可忽略内核强制执行默认动作,程序无法拦截KILL(9)、STOP(19)

本文重点演示的 TERM 属「可捕获的默认终止」、KILL 属「不可捕获的内核直杀」,二者的行为差异在 3.6~3.8 用 trap 实证。

3.2 信号名与编号互查

kill -l 带参数即进入「互查」模式:给名字查编号,给编号查名字,支持一次查多个:

root@ubuntu2204:~/demo/kill# kill -l SIGTERM
15
root@ubuntu2204:~/demo/kill# kill -l 15
TERM
root@ubuntu2204:~/demo/kill# kill -l 9
KILL

一次查多个时逐行输出对应结果:

root@ubuntu2204:~/demo/kill# kill -l HUP INT
1
2
root@ubuntu2204:~/demo/kill# kill -l TERM KILL
15
9
root@ubuntu2204:~/demo/kill# kill -l 18 19
CONT
STOP

💡 要点:kill -l 18 19 的输出印证了常用信号速查表中最容易记混的一对——18 是 SIGCONT(恢复执行),19 是 SIGSTOP(暂停执行),编号顺序与字母表顺序一致(C 在 S 前)。

3.3 默认信号 SIGTERM 与 kill -0 探测

不带任何信号参数的 kill PID 等价于 kill -TERM PID。同时演示 kill -0:信号编号 0 是一个特殊值——不向进程投递任何信号,只做权限与存在性探测,常用于脚本中判断进程是否存活:

root@ubuntu2204:~/demo/kill# sleep 300 & SPID=$!; echo started_pid=$SPID; kill -0 $SPID; echo probe_rc=$?
started_pid=18048
probe_rc=0

kill -0 返回 0 表示进程存在且当前用户有权向它发信号。正常 TERM 终止后再次探测则返回 1:

root@ubuntu2204:~/demo/kill# sleep 300 & SPID=$!; kill $SPID; echo kill_rc=$?; sleep 0.3; kill -0 $SPID 2>/dev/null; echo probe_after_rc=$?
kill_rc=0
probe_after_rc=1

💡 要点:kill PID 与 kill -0 PID 是两件事——前者"发 TERM 杀进程",后者"探测进程是否可投递信号"。kill -0 不产生任何副作用,是 Shell 脚本里最轻量的存活检测手段。

3.4 四种信号指定写法等效:-9 / -SIGKILL / -n 9 / -s KILL

kill 支持三种显式指定信号的方式(信号名、编号、-编号 简写),加上默认 TERM 共四种日常写法。实测证明对同一目标进程四种写法完全等效(每条都先杀再 wait 回收,wait 返回 137 即 128+9,确认死因是 9 号信号):

root@ubuntu2204:~/demo/kill# sleep 300 & P1=$!; kill -9 $P1; echo kill_rc=$?; wait $P1 2>/dev/null; echo wait_rc=$?
kill_rc=0
wait_rc=137
root@ubuntu2204:~/demo/kill# sleep 300 & P2=$!; kill -SIGKILL $P2; echo kill_rc=$?; wait $P2 2>/dev/null; echo wait_rc=$?
kill_rc=0
wait_rc=137
root@ubuntu2204:~/demo/kill# sleep 300 & P3=$!; kill -n 9 $P3; echo kill_rc=$?; wait $P3 2>/dev/null; echo wait_rc=$?
kill_rc=0
wait_rc=137
root@ubuntu2204:~/demo/kill# sleep 300 & P4=$!; kill -s KILL $P4; echo kill_rc=$?; wait $P4 2>/dev/null; echo wait_rc=$?
kill_rc=0
wait_rc=137

四种写法的对应关系:-9(-编号 简写)、-SIGKILL(全名)、-n 9(编号参数)、-s KILL(-s + 名字)。-s 与 -n 是 POSIX 风格,跨 Shell 兼容性最好;-9 最简短、使用最广。

3.5 常用信号速查表

结合 3.1 全表与后续实测,把日常真正会用到的信号整理成速查表:

编号信号名默认动作能否被捕获典型用途
1SIGHUP终止可传统上"终端挂断";守护进程惯用它做配置热重载(见 3.9)
2SIGINT终止可键盘 Ctrl+C 中断前台进程
9SIGKILL终止不可内核强制立即结束进程,最后手段(见 3.7)
15SIGTERM终止可kill 默认信号,请求进程优雅退出(见 3.6/3.8)
18SIGCONT继续可(应用一般不拦)让被暂停的进程恢复执行
19SIGSTOP暂停不可无条件暂停进程(调试、限流场景)

表后解读:日常停止进程的黄金顺序是 先 15 后 9——TERM 给进程自我清理的机会,等不到退出再用 KILL。Ctrl+C(INT)只作用于前台进程组,后台进程需用 kill 显式发信号。

💡 技术拓展:同样是"让进程退出",TERM/INT/KILL/HUP 的场景分工如下——

信号语义适用场景
TERM(15)"请你退出"日常 stop,进程可清理临时文件、释放锁
INT(2)"用户中断"等价 Ctrl+C,交互场景
KILL(9)"立即消失"进程僵死、TERM 无效时的兜底
HUP(1)"终端断开/请重载"常驻守护进程的 reload 约定(nginx -s reload 底层即 HUP)

本文实测以 TERM 与 KILL 的对比为主线,HUP 在 3.9 实证。

3.6 SIGTERM 可被进程捕获(trap 实证)

写一个 trap 工作脚本:注册 TERM 处理器,收到信号后打印提示再退出。注意 sleep 300 & + wait 的结构——trap 处理函数要等当前前台命令结束才执行,若把 sleep 300 放前台,信号会被拖延到 300 秒后才处理(这是本组实测踩过的坑,详见 5. Q7);后台 + wait 让 bash 在信号到达时立即中断 wait 执行 trap:

cat > /root/demo/kill/trap-term.sh <<'EOF'
#!/bin/bash
trap 'echo "caught SIGTERM, doing cleanup then exit"' TERM
echo "worker started, pid=$$"
sleep 300 &
wait
EOF
chmod +x /root/demo/kill/trap-term.sh

后台启动它,用不带信号参数的裸 kill(默认 TERM)终止,观察 worker 的反应:

root@ubuntu2204:~/demo/kill# ./trap-term.sh & TPID=$!; sleep 0.5; kill $TPID; wait $TPID; echo worker_rc=$?
worker started, pid=18122
caught SIGTERM, doing cleanup then exit
worker_rc=0

worker 捕获了 TERM 并打印清理信息后主动退出,退出码 0。这证明:裸 kill PID 发出的默认信号就是 SIGTERM,且 TERM 可以被进程捕获。

3.7 SIGKILL 不可捕获(trap 实证 + rc=137)

先看一个有趣的现象:bash 并不拒绝为 KILL 注册 trap,只是这条 trap 永远不会触发:

root@ubuntu2204:~/demo/kill# bash -c 'trap "echo never-seen" KILL; echo trap_set_rc=$?'
trap_set_rc=0

用同一类 trap 工作脚本(注册了 TERM 处理器)验证 kill -9——进程被直接杀死,任何 trap 输出都没有出现,wait 返回 137(128+9,"死于 9 号信号"的标准退出码):

root@ubuntu2204:~/demo/kill# ./trap-worker.sh & WPID=$!; sleep 0.5; kill -9 $WPID; wait $WPID; echo wait_rc=$?
worker started, pid=218
bash: line 1:   218 Killed                  ./trap-worker.sh
wait_rc=137

(218 Killed 一行是 Shell 对被杀后台作业的终止通知,属正常回显。)

💡 要点:SIGKILL 由内核直接执行,进程完全没有运行代码的机会,因此无法捕获、无法忽略、无法延迟。这也是它只能做"最后手段"的原因——被 KILL 的进程没有任何机会落盘、解锁、删除临时文件。

3.8 TERM 与 KILL 的清理行为差异(清理标记实证)

用同一个 worker 对比两种信号的清理能力:worker 的 trap 会向标记文件写入一行"清理完成"。先发 TERM:

cat > /root/demo/kill/cleanup-worker.sh <<'EOF'
#!/bin/bash
cleanup() { echo "cleanup done, file closed, tmp removed" >> /root/demo/kill/cleanup.log; exit 0; }
trap cleanup TERM
echo "cleanup worker pid=$$"
sleep 300 &
wait
EOF
chmod +x /root/demo/kill/cleanup-worker.sh
rm -f /root/demo/kill/cleanup.log
root@ubuntu2204:~/demo/kill# ./cleanup-worker.sh & CPID=$!; sleep 0.5; kill -TERM $CPID; sleep 0.5; echo log_after_TERM=$(cat /root/demo/kill/cleanup.log 2>/dev/null)
cleanup worker pid=231
log_after_TERM=cleanup done, file closed, tmp removed

TERM 送达后 trap 执行,标记文件里留下了清理记录。清空标记重来一次,改用 kill -9:

root@ubuntu2204:~/demo/kill# rm -f /root/demo/kill/cleanup.log
root@ubuntu2204:~/demo/kill# ./cleanup-worker.sh & CPID=$!; sleep 0.5; kill -9 $CPID; sleep 0.5; echo log_after_KILL=$(cat /root/demo/kill/cleanup.log 2>/dev/null); if [ -f /root/demo/kill/cleanup.log ]; then echo log_lines=$(wc -l < /root/demo/kill/cleanup.log); else echo log_lines=0; fi
cleanup worker pid=223
bash: line 1:   223 Killed                  ./cleanup-worker.sh
log_after_KILL=
log_lines=0

标记文件不存在(log_lines=0):进程被内核直接移除,清理逻辑一次都没运行。结论:TERM=进程可自行清理;KILL=内核直杀、零清理。对持有锁文件、写队列、连接池的服务,这个差异直接决定数据一致性。

3.9 SIGHUP 热重载:给常驻进程发 reload 信号

SIGHUP 的经典用途是让守护进程"不重启就重载配置"。写一个可重载 worker:HUP 触发计数重载、TERM 触发保存退出,然后连续发两次 HUP、一次 TERM:

cat > /root/demo/kill/reload-worker.sh <<'EOF'
#!/bin/bash
count=0
trap 'count=$((count+1)); echo "SIGHUP received, reload config #$count"' HUP
trap 'echo "SIGTERM received, save state and exit"; exit 0' TERM
echo "reload-worker pid=$$"
sleep 300 &
while :; do wait; done
EOF
chmod +x /root/demo/kill/reload-worker.sh
root@ubuntu2204:~/demo/kill# ./reload-worker.sh & RP=$!; sleep 0.5; kill -HUP $RP; sleep 0.3; kill -HUP $RP; sleep 0.3; kill $RP; wait $RP; echo worker_rc=$?
reload-worker pid=314
SIGHUP received, reload config #1
SIGHUP received, reload config #2
SIGTERM received, save state and exit
worker_rc=0

worker 全程 PID 不变、两次 HUP 都被处理、最后 TERM 优雅退出。确认无残留:

root@ubuntu2204:~/demo/kill# pgrep -f reload-worker || echo no-worker-left
no-worker-left

💡 要点:while :; do wait; done 是 Shell 守护进程的标准写法——wait 被信号中断返回后循环重新挂起,保证 worker 不会因收到一次信号就意外退出。

3.10 作业号 %1:终止后台作业(需 job control)

bash 内置 kill 独有能力:可用作业号(%1、%2)代替 PID。作业号仅在交互式 Shell 或开启 job control(set -m)时可用,演示在子 Shell 中开启 monitor 模式后用 jobs -l 配合作业号逐个终止:

root@ubuntu2204:~/demo/kill# bash -c 'set -m; sleep 300 & sleep 300 & jobs -l; kill %1; sleep 0.3; jobs -l; kill %2 2>/dev/null; sleep 0.3; jobs -l'
[1]- 17912 Running                 sleep 300 &
[2]+ 17913 Running                 sleep 300 &
[1]-  Terminated              sleep 300
[2]+ 17913 Running                 sleep 300 &
[2]+  Terminated              sleep 300

jobs -l 输出作业号、PID 与状态;kill %1 终止 1 号作业后状态变为 Terminated,再 kill %2 清掉最后一个。

💡 要点:作业号只在当前 Shell 会话内有效,不能跨会话使用;kill %1 报错 kill: %1: no such job 通常就是换了会话或 job control 未开启。

3.11 kill 0:向整个进程组发信号

kill 0 的目标不是"PID 为 0 的进程",而是当前进程组的全部成员(包括发出命令的 Shell 自身)。用 setsid 把演示 Shell 放进独立进程组,设 trap 后执行 kill 0,再用 kill -0 逐个验证组内进程生死:

root@ubuntu2204:~/demo/kill# setsid bash -c 'trap "echo group-shell: got TERM from kill 0, still alive" TERM; sleep 300 & sleep 300 & SP1=$!; SP2=$!; sleep 0.3; kill 0; sleep 0.5; kill -0 $SP1 2>/dev/null; echo sp1_alive_rc=$?; kill -0 $SP2 2>/dev/null; echo sp2_alive_rc=$?; kill -0 $$ 2>/dev/null; echo shell_alive_rc=$?; exit 0' < /dev/null
group-shell: got TERM from kill 0, still alive
sp1_alive_rc=1
sp2_alive_rc=1
shell_alive_rc=0

解读回显:两个后台 sleep 收到 TERM 直接死亡(sp1_alive_rc=1、sp2_alive_rc=1);演示 Shell 自己也收到了 TERM,但因为注册了 trap 而存活(shell_alive_rc=0)并打印了那行提示。

⚠️ 注意:kill 0 是组播信号,会把命令所在的整个进程组一起轰掉。在没有 trap 的交互 Shell 里执行 kill 0 会直接把当前登录会话杀断线,生产环境慎用;它适合的场合是"清理自己启动的一组子进程"。

3.12 错误与权限边界:rc=1 与 Operation not permitted

目标不存在(PID 取 pid_max 上限 4194303 保证不存在),报 No such process,退出码 1:

root@ubuntu2204:~/demo/kill# kill 4194303; echo rc=$?
bash: line 1: kill: (4194303) - No such process
rc=1
root@ubuntu2204:~/demo/kill# kill -0 4194303 2>&1; echo rc=$?
bash: line 1: kill: (4194303) - No such process
rc=1

参数不是 PID/作业号,直接拒绝执行:

root@ubuntu2204:~/demo/kill# kill abc 2>&1; echo rc=$?
bash: line 1: kill: abc: arguments must be process or job IDs
rc=1

权限边界:普通用户 tester 试图 kill root 启动的 sleep 进程,被内核拒绝(信号投递要求发送者 root,或与目标同 UID):

root@ubuntu2204:~/demo/kill# id tester
uid=1012(tester) gid=1016(tester) groups=1016(tester)
root@ubuntu2204:~/demo/kill# sleep 300 & RP=$!; echo root_sleep_pid=$RP; su tester -c "kill $RP"; echo su_kill_rc=$?
root_sleep_pid=17917
bash: line 1: kill: (17917) - Operation not permitted
su_kill_rc=1

对照:root 杀自己的进程畅通无阻:

root@ubuntu2204:~/demo/kill# sleep 300 & RP=$!; kill $RP; echo root_kill_own_rc=$?
root_kill_own_rc=0

四、实战示例

4.1 按模式批量终止:kill $(pgrep -f ...)

kill 只认 PID,"按名字/模式杀一批"的惯用组合是 kill $(pgrep -f 模式)——由 pgrep 先解析出 PID 列表再交给 kill。启动 2 个 sleep 300 与 1 个 sleep 120:

root@ubuntu2204:~/demo/kill# sleep 300 & sleep 300 & sleep 120 & echo started
started
root@ubuntu2204:~/demo/kill# ps -eo pid,args | grep -E '[s]leep (300|120)'
     86 sleep 300
     87 sleep 300
     88 sleep 120

用锚定模式 ^sleep 300$ 一次终止两个 sleep 300(不误伤 sleep 120),再用 pgrep -c 验证只剩 1 个进程:

root@ubuntu2204:~/demo/kill# kill $(pgrep -f '^sleep 300$') 2>/dev/null; echo rc=$?
rc=0
root@ubuntu2204:~/demo/kill# sleep 0.3; pgrep -c -f '^sleep (300|120)$'; echo rc=$?
1
rc=0

清掉最后一个并确认全净:

root@ubuntu2204:~/demo/kill# kill $(pgrep -f '^sleep 120$') 2>/dev/null; sleep 0.3; pgrep -c -f '^sleep (300|120)$' || echo all-cleaned
0
all-cleaned

💡 要点:$(...) 命令替换会丢弃尾部换行,pgrep 输出的多行 PID 被展开成 kill PID1 PID2 ...;模式写 ^...$ 锚定可避免 sleep 30 误匹配 sleep 300 这类前缀陷阱。pgrep 与 pkill 的完整参数见《pgrep 命令详解与参数实测(Ubuntu 22.04)》《pkill 命令详解与参数实测(Ubuntu 22.04)》。

4.2 优雅停止兜底:TERM 无效后升级 KILL

对"卡住不理 TERM"的进程(常见于 IO 挂起、死锁),标准处置是 TERM → 探测 → 仍活着就 KILL。先构造一个故意忽略 TERM 的 worker:

cat > /root/demo/kill/stubborn-worker.sh <<'EOF'
#!/bin/bash
trap 'echo "ignoring TERM for a while (stuck in io)"' TERM
echo "stubborn worker pid=$$"
sleep 300 &
while :; do wait; done
EOF
chmod +x /root/demo/kill/stubborn-worker.sh

第一步 TERM,并用 kill -0 探测——worker 捕获 TERM 后选择不退,仍存活:

root@ubuntu2204:~/demo/kill# ./stubborn-worker.sh & KP=$!; sleep 0.5; kill $KP; sleep 0.3; kill -0 $KP 2>/dev/null && echo still-alive-after-TERM || echo gone-after-TERM
stubborn worker pid=241
ignoring TERM for a while (stuck in io)
still-alive-after-TERM

第二步升级 KILL,进程立即消失,wait 返回 137:

root@ubuntu2204:~/demo/kill# kill -9 $KP 2>/dev/null; sleep 0.3; kill -0 $KP 2>/dev/null && echo still-alive || echo gone-after-KILL
bash: line 1:   241 Killed                  ./stubborn-worker.sh
gone-after-KILL
root@ubuntu2204:~/demo/kill# wait $KP 2>/dev/null; echo final_wait_rc=$?
final_wait_rc=137

⚠️ 注意:升级 KILL 前务必给 TERM 留出合理等待时间(数据库类服务通常给 30~90 秒做 checkpoint),直接 -9 可能丢数据。

4.3 kill 0 一步清理整组子进程

批量清理"自己启动的一组子进程"时,kill 0 可以一条命令发完整组。把启动逻辑放进独立进程组(setsid),脚本退出前对全组发 TERM,配合 3.11 的实测结论即可安全落地;更保守的等价写法是用 4.1 的 kill $(pgrep -P $父PID) 按父进程枚举后逐 PID 发信号。两种方式的取舍:kill 0 少一次查询、原子性更好,但要求调用者对"自己所在组"有清晰认知;按父进程枚举更精确,适合脚本嵌套场景。

五、注意事项与常见问题

Q1:kill 之后进程为什么还活着? A:默认 TERM 是"可协商"信号,进程可以捕获、忽略它(3.6/4.2 的 worker 就是例子)。先确认是否真的忽略了(kill -0 探测 + 稍等),再升级 kill -9。

Q2:-9 和 -15 到底怎么选? A:默认选 15(TERM)。TERM 让进程有机会完成落盘、释放锁、通知下游;只有进程对 TERM 无响应(僵死、死锁)时才用 9。持有写状态的服务(数据库、消息队列)尤其要避免直接 -9。

Q3:退出码 137、143 是什么意思? A:Shell 中 128+N 表示"进程死于编号 N 的信号"。137=128+9(被 SIGKILL 杀死),143=128+15(被 SIGTERM 杀死)。脚本里可据此判断进程死因。

Q4:提示 Operation not permitted 怎么回事? A:见 3.12 实测——向进程发信号要求发送者是 root,或与目标进程同 UID。普通用户杀不了 root 的进程,root 一般只受"目标是否还存在"限制。

Q5:bash 内置 kill 和 /bin/kill 有什么差异? A:见 2.2——内置版支持作业号 %1、进程数达上限时仍可用;外部版多 -q(sigqueue 语义)和表格化 -L。日常交互 Shell 敲 kill 命中的是内置版,两者发信号行为一致。

Q6:kill 0 为什么危险? A:它的语义是"当前进程组的所有成员",包括发出命令的 Shell 自身(3.11 实证)。交互终端里误敲会直接断线;脚本中若不慎在父进程组执行,会把父进程一起带走。

Q7:为什么 trap 的处理函数"迟迟不执行"? A:bash 收到信号后,要等当前正在执行的前台命令结束才运行 trap 处理函数。本组实测中 trap ... TERM + 前台 sleep 300 的组合让信号被拖了 300 秒。正确姿势是 sleep 300 & + wait(wait 可被信号中断),详见 3.6 的脚本结构。

六、参考链接