521

I start a background process from my shell script, and I would like to kill this process when my script finishes.

How to get the PID of this process from my shell script? As far as I can see variable $! contains the PID of the current script, not the background process.

Chris Stryczynski
  • 30,145
  • 48
  • 175
  • 286
Volodymyr Bezuglyy
  • 16,295
  • 33
  • 103
  • 133

9 Answers9

785

You need to save the PID of the background process at the time you start it:

foo &
FOO_PID=$!
# do other stuff
kill $FOO_PID

You cannot use job control, since that is an interactive feature and tied to a controlling terminal. A script will not necessarily have a terminal attached at all so job control will not necessarily be available.

camh
  • 40,988
  • 13
  • 62
  • 70
  • 39
    Since $! returns the last background process's pid. Is it possible that something starts between `foo` and `$!`, and we get that something's pid instead of `foo`'s? – WiSaGaN Oct 16 '13 at 09:06
  • 79
    @WiSaGaN: No. There is nothing between those lines. Any other activity on the system will not affect this. $! will expand to the PID of the last background process _in that shell_. – camh Oct 17 '13 at 09:36
  • 12
    ... which hoses you if `foo` happens to be multiple piped commands (eg. `tail -f somefile.txt | grep sometext`). In such cases, you will get the PID of the grep command from `$!` rather than the tail command if that's what you were looking for. You will need to use `jobs` or `ps` or the likes in this instance. – John Rix Oct 14 '14 at 13:52
  • 3
    @JohnRix: Not necessarily. You will get the pid of `grep`, but if you kill that, tail will get a SIGPIPE when it tries to write to the pipe. But as soon as you try to get into any tricky process management/control, bash/shell becomes quite painful. – camh Oct 15 '14 at 04:20
  • 1
    @camh: My particular scenario required killing the first process in the chain only, so that the second received an EOF and performed normal exit processing (it was an awk script with an END block that I needed executing to dump out some statistics). Killing the tail command (after getting it's PID using `jobs -p`) has the desired result. – John Rix Oct 16 '14 at 21:40
  • 9
    Another worthy solution is suggested in (a comment to an answer to) [How to get pid of just started process](http://serverfault.com/a/205563/68972): oh, and the "oneliner": `/bin/sh -c 'echo $$>/tmp/my.pid && exec program args' &` – sysfault Nov 24 '10 at 14:28 – imz -- Ivan Zakharyaschev Jun 02 '15 at 14:40
  • Regarding piped commands, they get a process group ID (PGID), which is the same as the PID of the process that created them. So `tail -f somefile.txt | grep sometext; FOO_PID=$!; kill $FOO_PID;` should kill the whole pipe. And if you want the PID of a distinct part of a pipe, you can work around it when you create the pipe. Unless you didn't create it, in which case you'd need to do a `pgrep` or `pkill` to get/kill all processes by name and/or arguments, vs. PID. – Beejor Feb 17 '19 at 22:54
  • What if your `foo &` process never ends and you want to suppress what it sends to stdout? – James T. Apr 27 '21 at 19:13
  • @JamesT. You should post your question as a question, not a comment. It is not related to this answer. – camh Apr 27 '21 at 23:08
  • As John says, if `foo` happens to be more than a single command with a single PID (e.g. it's a pipe line, another shell script, or a shell function), then `kill $!` will only kill one of the several processes and that may not do what you want. Killing the last command in a pipeline will only cause the pipeline to exit (with SIGPIPE) when the command before it generates output. If that preceding command is taking hours of CPU before it produces any output, you probably want it killed right away, not after hours of wasted CPU. You can't do that with `kill $!` – Ian D. Allen May 02 '22 at 21:29
191

You can use the jobs -l command to get to a particular jobL

^Z
[1]+  Stopped                 guard

my_mac:workspace r$ jobs -l
[1]+ 46841 Suspended: 18           guard

In this case, 46841 is the PID.

From help jobs:

-l Report the process group ID and working directory of the jobs.

jobs -p is another option which shows just the PIDs.

Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
jldupont
  • 93,734
  • 56
  • 203
  • 318
  • 2
    To use this in a shell script, you'd have to process the output. – Phil Oct 15 '13 at 18:22
  • 12
    @Phil To list pids only : jobs -p. To list pid of a certain job: jobs -p %3. No need to process output. – Erik Aronesty Aug 27 '14 at 14:56
  • 1
    @Erik with different commands / arguments you change the context of my comment. Without the additional argument suggested the output requires processing. Suggest an improvement to the answer! – Phil Aug 28 '14 at 17:45
  • 2
    Saving the PID from `$!` just after you started it is more portable and more straightforward in most situations. That's what the currently accepted answer does. – tripleee Sep 18 '17 at 08:36
  • `jobs -p` returned the same as `jobs -l` on Lubuntu 16.4 – Timo Dec 02 '17 at 08:01
  • @tripleee you're also less likely to have saved `$!` when you realize a bit later that you need to kill this job in an interactive shell. – Calimo Feb 10 '19 at 13:12
  • 2
    @Calimo I agree that `jobs` is the way to go in an interactive session, but this is posted as an answer to a question specifically about how to do this programmatically from a script. – tripleee Feb 10 '19 at 13:24
69
  • $$ is the current script's pid
  • $! is the pid of the last background process

Here's a sample transcript from a bash session (%1 refers to the ordinal number of background process as seen from jobs):

$ echo $$
3748

$ sleep 100 &
[1] 192

$ echo $!
192

$ kill %1

[1]+  Terminated              sleep 100
oHo
  • 51,447
  • 27
  • 165
  • 200
catwalk
  • 6,340
  • 25
  • 16
  • echo `%1` does not return the background process on my Ubuntu whereas `echo $!` does – Timo Dec 02 '17 at 08:03
  • Note that `$$` is not always the current PID. For example, if you define a new function in `bash` and run the function in the background, the `$$` within that function contains the PID of the process that started the function in the background. If you need PID of the actual process running any given code, you have to use `$BASHPID`, instead. – Mikko Rantalainen Sep 22 '20 at 12:07
35

An even simpler way to kill all child process of a bash script:

pkill -P $$

The -P flag works the same way with pkill and pgrep - it gets child processes, only with pkill the child processes get killed and with pgrep child PIDs are printed to stdout.

Alexey Polonsky
  • 1,141
  • 11
  • 12
  • 1
    Very convenient! This is the best way to ensure you don't leave opened processes on the background. – lepe Oct 08 '15 at 06:40
  • @lepe: Not quite. If you are a grandparent, this won't work: after `bash -c 'bash -c "sleep 300 &"' &` running `pgrep -P $$` shows nothing, because the sleep will not be a direct child of your shell. – petre Feb 15 '16 at 08:45
  • 2
    @AlexeyPolonsky: it should be: kill all child procs of a shell, not a script. Because `$$` refers to the current shell. – Timo Dec 02 '17 at 08:23
  • performing `bash -c 'bash -c "sleep 300 &"' & ; pgrep -P $$` , I get on stdout `[1] . So at least it shows something, but this is probably not the output of `pgrep` – Timo Dec 02 '17 at 08:29
  • Even processes can deattach them from parent process. Simple trick is to call fork (create a grandchild) and then just let the child process exit while grandchild continues doing the work. (daemonization is the keyword) But even if child keeps running too pkill -P isn't enough to propagate signal to grandchilds. Tool like pstree is required to follow whole dependant process tree. But this won't catch daemons started from the process as their parent is process 1. eg: `bash -c 'bash -c "sleep 10 & wait $!"' & sleep 0.1; pstree -p $$` – Pauli Nieminen Nov 07 '19 at 04:20
6

this is what I have done. Check it out, hope it can help.

#!/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

Then just run it as: ./bgkill.sh with proper permissions of course

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
APerson
  • 8,140
  • 8
  • 35
  • 49
Luis Ramirez
  • 69
  • 1
  • 1
4

You might also be able to use pstree:

pstree -p user

This typically gives a text representation of all the processes for the "user" and the -p option gives the process-id. It does not depend, as far as I understand, on having the processes be owned by the current shell. It also shows forks.

villaa
  • 1,043
  • 3
  • 14
  • 32
4

pgrep can get you all of the child PIDs of a parent process. As mentioned earlier $$ is the current scripts PID. So, if you want a script that cleans up after itself, this should do the trick:

trap 'kill $( pgrep -P $$ | tr "\n" " " )' SIGINT SIGTERM EXIT
curious_prism
  • 349
  • 1
  • 5
  • Wouldn't this kill the lot? – Phil Oct 15 '13 at 18:24
  • Yes it would, the question never mentioned keeping _some_ background children alive upon exit. – curious_prism Oct 24 '13 at 04:44
  • `trap 'pkill -P $$' SIGING SIGTERM EXIT` looks simpler, but I did not test it. – petre Feb 15 '16 at 08:36
  • For compatibility, don't use the `SIG` prefix. It is allowed by POSIX but just as an extension that implementations *may* support: http://pubs.opengroup.org/onlinepubs/007904975/utilities/trap.html The dash shell for example does not. – josch Dec 04 '18 at 09:46
  • Assuming if you have `pgrep` you may also have `pkill` the shorter version is more concise. – dragon788 Nov 01 '20 at 19:10
  • @petre you'd need to use double quotes or else `$$` will not expand – Pat Jul 15 '22 at 13:00
0

I have run into this problem many times provisioning various infrastructure objects. Many times you need a temp proxy using kubectl or a temp port forward. I have found the timeout command to be a good solution for these, since it allows my script to be self contained and I can be assured that the process will end. I try to set small timeouts and rerun the script if I need still need it.

matttrach
  • 125
  • 1
  • 6
0

If you're not able to get the PID directly after starting the job, you could also try this and get the PID later:

foo &

# do some stuff and then

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

Explanation:

  • ps get information for all processes incl. command
  • grep is filtering for your command
  • tr is removing duplicate spaces for cut
  • cut is getting you the column with the PID (2 in this case)
ronnyworm
  • 21
  • 3