I'm trying to use some functions in a bash script to simplify calling some child processes. I want to decide at the call site whether or not to run the process in the background as a job, or in the foreground, and later kill the child process when necessary. Unfortunately, as far as I can tell, the pid I get with $! doesn't belong to the long-running background process, but the function that called it. Killing the pid I get with $! doesn't kill the long running child process, and it seems to get orphaned.

function inner() {
  tail -f /dev/null

function outer() {

outer &
echo 'Before, $!: ' $!
echo 'Before, jobs -p: ' $(jobs -p)
echo 'Before, ps aux: ' $(ps aux | grep /dev/null | grep -v grep)

kill $!
echo 'After, $!: ' $!
echo 'After, jobs -p: ' $(jobs -p)
echo 'After, ps aux: ' $(ps aux | grep /dev/null | grep -v grep)

The output I get is:

Before, $!:  71644
Before, jobs -p:  71644
Before, ps aux:  jstaab 71646 0.0 0.0 4267744 688 s005 S+ 2:53PM 0:00.00 tail -f /dev/null
After, $!:  71644
After, jobs -p:  71644
./test.sh: line 17: 71644 Terminated: 15          outer
After, ps aux:  jstaab 71646 0.4 0.0 4267744 688 s005 S+ 2:53PM 0:00.00 tail -f /dev/null

You'll notice that ps aux gives me a different pid than either $! or jobs -p. This makes sense, but how can I kill tail -f /dev/null with a kill without grepping for the command?

1 Answer 1


outer & creates a subshell. $! gives the PID of this subshell.

tail -f /dev/null is a child of this subshell so it has a different PID. But you can do

exec tail -f /dev/null

instead. Then the kill hits the tail.

Another possibility is to use /bin/kill instead of the shell builtin. With a negative number as argument you can kill the whole process group:

/bin/kill -TERM -$!
  • Interesting, ok. I read up on exec and it looks like it replaces the shell. So in a subshell created with & it kills its parent and steals its pid? That negative pid behavior seems completely bizarre to me, but that's really useful behavior. I'll take it.
    – jstaab
    Mar 28, 2018 at 19:57
  • 1
    @jstaab The shell exec command executes an execve() syscall which replaces the running process with a new one. That is the way any process creates a child process: first fork, then the "copy process" does an execve() to the desired binary. The other point: Each background pipeline creates a new process group. Thus you can kill all process which belong to it (and only those) by signaling the whole process group. Mar 28, 2018 at 21:31

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .