如何获取后台进程的进程ID?

532

我从我的Shell脚本中启动了一个后台进程,当我的脚本结束时,我想要杀死这个进程。

如何从我的Shell脚本中获取该进程的PID?据我所知,变量$!包含当前脚本的PID,而不是后台进程的PID。


12
$! 是正确的。你确定要在后台启动脚本吗?可以请提供一个示例吗? - pixelbeat
7
是的,$! 是正确的。 我错了。 - Volodymyr Bezuglyy
12
$$ 包含当前脚本的进程 ID。 - HUB
2
请注意,在bash中,$$可能是父进程ID:testfun() { echo "\$\$=$$ \$BASHPID=$BASHPID"; }; echo "my pid is $$"; testfun & wait - Mikko Rantalainen
9个回答

795
你需要在启动后台进程时保存该进程的PID:
foo &
FOO_PID=$!
# do other stuff
kill $FOO_PID

您不能使用作业控制,因为它是交互式功能,与控制终端相关联。脚本可能根本没有连接终端,因此作业控制可能不可用。


39
由于$!返回最后一个后台进程的进程ID,那么在foo$!之间会不会有其他进程启动并获取该进程的PID而不是foo的PID呢? - WiSaGaN
79
不。这些句子之间没有任何关联。系统上的其他任何活动都不会影响它。"$!"将展开为在该shell中的最后一个后台进程的PID。 - camh
12
如果foo恰好是多个管道命令(例如 tail -f somefile.txt | grep sometext),那么这会给你带来麻烦。在这种情况下,如果你要查找的是tail命令的PID,那么你将从$!中得到grep命令的PID。在这种情况下,你需要使用jobsps等命令。 - John Rix
3
不一定。你会得到grep的pid,但如果你杀死它,当tail尝试写入管道时,会产生一个SIGPIPE信号。但一旦你尝试进入任何复杂的进程管理/控制,bash/shell就变得相当麻烦。 - camh
9
在回答“如何获取刚启动进程的pid”的评论中,提出了另一个值得一试的解决方案:哦,还有这个“一行命令”:/bin/sh -c 'echo $$>/tmp/my.pid && exec program args' & - sysfault 于2010年11月24日14:28。 - imz -- Ivan Zakharyaschev
显示剩余5条评论

194

您可以使用jobs -l 命令来获取特定的作业。

^Z
[1]+  Stopped                 guard

my_mac:workspace r$ jobs -l
[1]+ 46841 Suspended: 18           guard
在这种情况下,46841是PID。
help jobs得知:
-l 显示作业的进程组ID和工作目录。 jobs -p是另一种选项,仅显示PID。

2
要在shell脚本中使用它,您必须处理输出。 - Phil
12
仅列出进程 ID,请使用命令 "jobs -p"。若要列出特定作业的进程 ID,请使用命令 "jobs -p %3"(其中 3 是作业号)。不需要处理输出。 - Erik Aronesty
2
在大多数情况下,从 $! 中保存 PID 是更具可移植性和更简单的方法,这就是当前被接受的答案所做的。 - tripleee
1
@tripleee,当你稍后需要在交互式 shell 中终止此作业时,你也不太可能保存 $! - Calimo
2
@Calimo,我同意在交互会话中使用jobs是正确的选择,但这篇帖子是作为回答一个关于如何从脚本中以编程方式实现此操作的问题。 - tripleee
显示剩余2条评论

71
  • $$ 是当前脚本的进程号
  • $! 是上一个后台进程的进程号

这里是一个 bash 会话的示例记录(%1 表示从 jobs 看到的后台进程的序号):

$ echo $$
3748

$ sleep 100 &
[1] 192

$ echo $!
192

$ kill %1

[1]+  Terminated              sleep 100

在我的 Ubuntu 上,echo%1不能返回后台进程,而echo $!可以。 - Timo
请注意,$$ 并不总是当前的 PID。例如,如果您在 bash 中定义一个新函数并在后台运行该函数,则该函数内部的 $$ 包含启动该函数的进程的 PID。如果您需要任何给定代码实际运行的进程的 PID,则必须使用 $BASHPID - Mikko Rantalainen

37
一个更简单的方式来终止bash脚本的所有子进程:
pkill -P $$

-P 标志在 pkillpgrep 中的工作方式相同 - 它获取子进程,只是使用 pkill 杀死子进程,而使用 pgrep 将子 PID 打印到 stdout。


1
非常方便!这是确保您不会在后台留下打开的进程的最佳方法。 - lepe
@lepe:不完全正确。如果你是一个祖父母,这个方法行不通:在运行 bash -c 'bash -c "sleep 300 &"' & 后,执行 pgrep -P $$ 将什么也不会显示,因为 sleep 不会成为你的 shell 的直接子进程。 - petre
2
@AlexeyPolonsky:应该是杀死一个 shell 的所有子进程,而不是脚本。因为 $$ 指的是当前 shell。 - Timo
执行 bash -c 'bash -c "sleep 300 &"' & ; pgrep -P $$ 命令后,标准输出会显示 [1] <pid>。至少它显示了一些东西,但这可能不是 pgrep 的输出。 - Timo
即使进程可以从父进程中分离出来。简单的技巧是调用fork(创建一个孙子进程),然后让子进程退出,而孙子进程继续执行工作。(守护进程是关键词)但是,即使子进程仍在运行,pkill -P也不足以将信号传播到孙子进程。需要像pstree这样的工具来跟踪整个依赖进程树。但是,这不会捕获从该进程启动的守护程序,因为它们的父进程是进程1。例如:bash -c 'bash -c "sleep 10 & wait $!"' & sleep 0.1; pstree -p $$ - Pauli Nieminen

6

这是我所做的。请查看,希望能对您有所帮助。

#!/bin/bash
#
# So something to show.
echo "UNO" >  UNO.txt
echo "DOS" >  DOS.txt
#
# Initialize Pid List
dPidLst=""
#
# Generate background processes
tail -f UNO.txt&
dPidLst="$dPidLst $!"
tail -f DOS.txt&
dPidLst="$dPidLst $!"
#
# Report process IDs
echo PID=$$
echo dPidLst=$dPidLst
#
# Show process on current shell
ps -f
#
# Start killing background processes from list
for dPid in $dPidLst
do
        echo killing $dPid. Process is still there.
        ps | grep $dPid
        kill $dPid
        ps | grep $dPid
        echo Just ran "'"ps"'" command, $dPid must not show again.
done

然后只需以适当的权限运行它:./bgkill.sh
root@umsstd22 [P]:~# ./bgkill.sh
PID=23757
dPidLst= 23758 23759
UNO
DOS
UID        PID  PPID  C STIME TTY          TIME CMD
root      3937  3935  0 11:07 pts/5    00:00:00 -bash
root     23757  3937  0 11:55 pts/5    00:00:00 /bin/bash ./bgkill.sh
root     23758 23757  0 11:55 pts/5    00:00:00 tail -f UNO.txt
root     23759 23757  0 11:55 pts/5    00:00:00 tail -f DOS.txt
root     23760 23757  0 11:55 pts/5    00:00:00 ps -f
killing 23758. Process is still there.
23758 pts/5    00:00:00 tail
./bgkill.sh: line 24: 23758 Terminated              tail -f UNO.txt
Just ran 'ps' command, 23758 must not show again.
killing 23759. Process is still there.
23759 pts/5    00:00:00 tail
./bgkill.sh: line 24: 23759 Terminated              tail -f DOS.txt
Just ran 'ps' command, 23759 must not show again.
root@umsstd22 [P]:~# ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
root      3937  3935  0 11:07 pts/5    00:00:00 -bash
root     24200  3937  0 11:56 pts/5    00:00:00 ps -f

好的,做得很好!不知道是否是最佳方法,但运行良好。 - jonathask

4

pgrep可以获取父进程的所有子PID。如前所述,$$是当前脚本的PID。因此,如果您想要一个在执行完后自动清理的脚本,可以使用以下命令:

trap 'kill $( pgrep -P $$ | tr "\n" " " )' SIGINT SIGTERM EXIT

这不会把所有东西都毁掉吗? - Phil
是的,问题从未提到在退出时保留“一些”背景子进程。 - curious_prism
trap 'pkill -P $$' SIGING SIGTERM EXIT 看起来更简单,但我没有测试过。 - petre
为了兼容性,请不要使用 SIG 前缀。虽然 POSIX 允许这样做,但只是一种扩展,实现可能会支持:http://pubs.opengroup.org/onlinepubs/007904975/utilities/trap.html 例如,破折号 shell 就不支持。 - josch
假设你已经有了 pgrep,那么你也可能拥有 pkill,这个更短的版本更为简洁。 - dragon788
@petre,你需要使用双引号,否则$$将不会扩展。 - Pat

4
你也可以使用pstree:
pstree -p user

这通常会给出所有“用户”进程的文本表示,-p选项则给出了进程ID。据我所知,它不依赖于当前Shell拥有进程的情况。它还显示分支。


2
要在shell脚本中使用它,您需要对输出进行大量处理。 - Phil

0
我在配置不同基础设施对象时多次遇到此问题。许多情况下,您需要使用 kubectl 或临时端口转发来使用临时代理。我发现 timeout 命令是一个很好的解决方案,因为它使我的脚本自包含化,并确保进程将会结束。我尝试设置小的超时时间,如果还需要,则重新运行脚本。

0

如果您在启动作业后无法直接获取PID,则可以尝试以下方法稍后获取PID:

foo &

# do some stuff and then

pid=$(ps -aux | grep foo | tr -s ' ' | cut -d\  -f2)
kill $pid

解释:

  • ps 获取所有进程的信息,包括命令
  • grep 过滤您的命令
  • tr 去除重复空格以供 cut 使用
  • cut 获取 PID 列(在此情况下为第二列)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接