Since, according to zshall(1), $ZDOTDIR/.zshenv gets sourced whenever a new instance of zsh starts
If you focus on the word "starts" here you'll have a better time of things. The effect of fork()
is to create another process that begins from exactly where the current process already is. It's cloning an existing process, with the only difference being the return value of fork
. The documentation is using "starts" to mean entering the program from the beginning.
Your example #3 runs $SHELL -c 'date; printenv; echo $$'
, starting an entirely new process from the beginning. It will go through the ordinary startup behaviour. You can illustrate that by, for example, swapping in another shell: run bash -c ' ... '
instead of zsh -c ' ... '
. There's nothing special about using $SHELL
here.
Examples #1 and #2 run subshells. The shell fork
s itself and executes your commands inside that child process, then carries on with its own execution when the child is done.
The answer to your question #1 is the above: example 3 runs an entirely new shell from the start, while the other two run subshells. The startup behaviour includes loading .zshenv
.
The reason they call this behaviour out specifically, which is probably what leads to your confusion, is that this file (unlike some others) loads in both interactive and non-interactive shells.
To your question #2:
if the shells that get created in #1 and #2 are called "subshells", what are those like the one generated by #3 called?
If you want a name you could call it a "child shell", but really it's nothing. It's no different than any other process you start from the shell, be it the same shell, a different shell, or cat
.
To your question #3:
is it possible to rationalize (and maybe generalize) the empirical/anecdotal findings described above in terms of the "theory" (for lack of a better word) of Unix processes?
fork
makes a new process, with a new PID, that starts running in parallel from exactly where this one left off. exec
replaces the currently-executing code with a new program loaded from somewhere, running from the beginning. When you spawn a new program, you first fork
yourself and then exec
that program in the child. That is the fundamental theory of processes that applies everywhere, inside and outside of shells.
Subshells are fork
s, and every non-builtin command you run leads to both a fork
and an exec
.
Note that $$
expands to the PID of the parent shell in any POSIX-compatible shell, so you may not be getting the output you expect regardless. Note also that zsh aggressively optimises subshell execution anyway, and commonly exec
s the last command, or doesn't spawn the subshell at all if all the commands are safe without it.
One useful command for testing your intuitions is:
strace -e trace=process -f $SHELL -c ' ... '
That will print to standard error all process-related events (and no others) for the command ...
you run in a new shell. You can see what does and does not run in a new process, and where exec
s occur.
Another possibly-useful command is pstree -h
, which will print out and highlight the tree of parent processes of the current process. You can see how many layers deep you are in the output.
When you run, for example:
bash -c "echo $BASHPID ; exec sleep 10"
or
echo $(echo $BASHPID ; exec sleep 10)
your current shell is interpolating the $BASHPID
variable before the second bash (or subshell) sees it. The solution is to prevent the expansion of those variables by the current shell:
bash -c 'echo $BASHPID ; exec sleep 10'
Best Answer
The
{}
just groups commands together in the current shell, while()
starts a new subshell. However, what you're doing is putting the grouped commands into the background, which is indeed a new process; if it was in the current process, it could not possibly be backgrounded. It's easier, IMHO, to see this kind of thing with strace:Note that the bash command starts, then it creates a new child with
clone()
. Using the -f option to strace means it also follows child processes, showing yet another fork (well, "clone") when it runs sleep. If you leave the -f off, you see just the one clone call when it creates the backgrounded process:If you really just want to know how often you're creating new processes, you can simplify that even further by only watching for fork and clone calls: