In a bash script, `echo $$` inside a `( ... )` subshell prints the same number as `echo $$` outside it. Why doesn't `$$` change, and which variable reports the PID of the process actually running that code?
answer
- one of them is frozen on purpose
- POSIX pins it to the invoking shell
- forks do not refresh it
- a bash 4.0 variable holds the real one
- BASHPID is per-process, $$ is per-script
basics
~20 sBash keeps $$ pinned to the process ID of the shell that was invoked, even inside a subshell, because POSIX defines it that way. Use $BASHPID, added in bash 4.0, to get the PID of the process currently executing.
solid answer
~40 s`$$` is specified as the process ID of the *invoking* shell, and bash deliberately does not update it when it forks a subshell — so inside `( ... )`, inside a pipeline stage, and inside a background job, `$$` still reports the script's original PID. Bash 4.0 added `$BASHPID`, which is re-evaluated per process and therefore gives the real PID of whichever process expands it. The distinction bites in two places. First, PID-derived names: `tmp.$$` produces the *same* name in several parallel subshells, so they collide instead of getting one file each. Second, `kill $$` inside a subshell signals the whole script rather than that child. If you need per-process identity, use `$BASHPID`; if you need a unique temp file, let `mktemp` generate the name rather than deriving it from any PID at all.
code
bash · 4 lines#!/usr/bin/env bash
echo "main : $$ $BASHPID"
( echo "subshell : $$ $BASHPID" )
echo x | while read -r _; do echo "pipeline : $$ $BASHPID"; donego deeper
Remember that $$ is the script's own process ID and does not change inside parentheses, and that $BASHPID is the one that tracks whichever process is running the code.
Explain that POSIX pins $$ to the invoking shell so bash deliberately never refreshes it, that bash 4.0 added $BASHPID for the real value, and that pipeline stages and background jobs see the same frozen $$.
Bring the consequence you have actually hit: PID-derived scratch files colliding across parallel subshells, or kill $$ inside a worker taking down the whole script and firing its cleanup trap early.
Take the position that PIDs are weak identity at all — reused by the kernel, repeated across containers, guessable by other users — and state what your standard requires instead for lock files, scratch paths and run correlation.
## The two variables - **`$$`** — the process ID of the shell that was invoked to run this script. POSIX requires it to stay constant for the life of that shell, and bash honours that by *not* refreshing it in forked children. - **`$BASHPID`** — a bash-specific variable, introduced in **bash 4.0**, that always expands to the PID of the bash process doing the expansion. In the top-level shell the two are equal; anywhere bash has forked, they differ. ```bash #!/usr/bin/env bash echo "main : $$ $BASHPID" # e.g. 4711 4711 ( echo "paren : $$ $BASHPID" ) # e.g. 4711 4712 ``` Because `$BASHPID` is bash 4.0+, it is unavailable on the bash 3.2 that ships as `/bin/bash` on macOS, and it does not exist in POSIX `sh`. ## Why bash was designed this way `$$` predates the idea of asking "which process am I?" — it exists so a script can identify *itself*, mostly for naming things and for signalling itself. A shell forks constantly and mostly invisibly: every pipeline stage, every command substitution, every background job, every `( ... )`. If `$$` tracked the current process, then a value captured early and used later would be unstable in a way scripts could not reason about, and it would break the POSIX contract. So bash froze `$$` at the invoking shell and, when the need for the true PID became clear, exposed it as a separate variable rather than changing the old one. Note what this means for children that are not bash: an external program's `getpid()` is of course its own PID; `$$` is a *shell expansion*, evaluated by bash before the program is launched, so `some-tool $$` passes the script's PID as an argument, not the tool's. ## Where it actually causes bugs ### PID-derived file names A long-standing habit is to build a scratch file name from the PID: ```bash tmp="/tmp/build.$$" ``` Inside a single script that is at least stable. But run several blocks in parallel subshells and every one of them computes the same `$$`, so they all write `/tmp/build.4711` — they clobber each other, and the failure is timing-dependent and rare enough to survive testing. Substituting `$BASHPID` makes the names distinct, but PIDs make poor unique identifiers in general: the kernel reuses them, a stale file from a previous run can collide with a recycled number, and inside a container the numbers restart from a small range. The robust answer is to let the system allocate the name — `mktemp` — instead of deriving it. ### Signalling the wrong process ```bash ( slow-thing || kill $$ ) # intends: give up on this block ``` Because `$$` is the script's PID, this kills the entire script — often the opposite of what the author wanted, and dramatic when a parent has a cleanup trap that then also runs. To end just the subshell, `exit` from it. To signal the current process explicitly, `kill "$BASHPID"`. ### Log lines that all look alike A script that fans work out into subshells and stamps each log line with `$$` produces output where every worker claims the same identity, which makes interleaved logs unreadable. `$BASHPID` distinguishes them. ## Related identity variables - **`$PPID`** — the PID of this shell's parent. Like `$$`, it is not refreshed in a subshell, so a subshell's `$PPID` is the *script's* parent, not the script itself. - **`$SHLVL`** — counts nested shell *invocations*, incremented when a new bash starts. A `( ... )` subshell is a fork of the running shell, not a new invocation, so it does not bump `SHLVL`; running `bash` explicitly does. ## The rule of thumb Ask what you want the number *for*. Identifying the script as a whole — a top-level lock, a message that says "run 4711 started" — is `$$`, and it is correct precisely because it is stable. Identifying the process executing right now — per-worker logging, signalling yourself — is `$BASHPID`. And generating something unique is neither: use a tool built for it.
- Does `$$` also stay fixed inside a background job and a pipeline stage, or only inside `( ... )`?It stays fixed in all of them. Every one of those constructs is a fork of the running shell rather than a new shell invocation, and bash refreshes `$BASHPID` but never `$$`. That uniformity is the point: `$$` names the script for its whole lifetime regardless of how many times it forks internally.
- What should a script use to name a temporary file, if not `$$`?`mktemp`, which asks the system for a name that does not already exist and creates it atomically with restrictive permissions. Any name derived from a PID is guessable and collision-prone: PIDs are reused, they repeat across containers, and parallel subshells share `$$`. Capture `mktemp`'s output into a variable and clean it up on exit.
- On bash 3.2, where `$BASHPID` does not exist, how would you get the current process's PID?Read it from the kernel: `read -r mypid < /proc/self/stat` and take the first field on Linux, or use `sh -c 'echo $PPID'` from within the process whose PID you want. Both are clumsier and less portable than `$BASHPID`, which is a reason to require bash 4+ in the shebang and check the version early if the script depends on it.
saying these in an interview costs you the question
- Claims $$ shows the subshell's own PID
- Says $BASHPID exists in POSIX sh
- Uses tmp.$$ for uniqueness across parallel subshells
- Expects kill $$ inside a subshell to kill only that subshell
- Assumes $PPID is refreshed inside a subshell