When you run a job in the background, bash prints the process ID of its subprocess, the one that runs the command in that job. If that job happens to create more subprocesses, that's none of the parent shell's business.
When the background job is a pipeline (i.e. the command is of the form something1 | something2 &
, and not e.g. { something1 | something2; } &
), there's an optimization which is strongly suggested by POSIX and performed by most shells including bash: each of the elements of the pipeline are executed directly as subprocesses of the original shell. What POSIX mandates is that the variable $!
is set to the last command in the pipeline in this case. In most shells, that last command is a subprocess of the original process, and so are the other commands in the pipeline.
When you run ls -lsa | grep feb
, there are three processes involved: the one that runs the left-hand side of the pipe (a subshell that finishes setting up the pipe then executes ls
), the one that runs the right-hand side of the pipe (a subshell that finishes setting up the pipe then executes grep
), and the original process that waits for the pipe to finish.
You can watch what happens by tracing the processes:
$ strace -f -e clone,wait4,pipe,execve,setpgid bash --norc
execve("/usr/local/bin/bash", ["bash", "--norc"], [/* 82 vars */]) = 0
setpgid(0, 24084) = 0
bash-4.3$ sleep 10 | sleep 20 &
…
Note how the second sleep
is reported and stored as $!
, but the process group ID is the first sleep
. Dash has the same oddity, ksh and mksh don't.