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?


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 '18 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. – Hauke Laging Mar 28 '18 at 21:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.