Does the Linux shell do a fork/exec and then waitpid() to get the return code to populate the $? variable, each time it executes something?
Yes, that's exactly what it does.
You can see this for yourself, if you are interested, by running the shell under strace
(a tool that intercepts and prints all system calls a program makes).
strace bash -c '/usr/bin/ls > /dev/null; echo $?'
This gives the following output, much trimmed:
execve("/usr/bin/bash", ["bash", "-c", "/usr/bin/ls > /dev/null; echo $?"], [/* 58 vars */]) = 0
[....]
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7ff16c3d69d0) = 1189
[....]
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 1189
[....]
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1189, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
wait4(-1, 0x7ffd3096b290, WNOHANG, NULL) = -1 ECHILD (No child processes)
[....]
exit_group(0) = ?
+++ exited with 0 +++
I used a bash script with two commands because otherwise bash doesn't fork but just execs the command, replacing itself (a useful optimization I just discovered it does!)
In the output, clone
is the system call behind the fork
function, and likewise wait4
is called by waitpid
. You don't see the exec
because that happens in the child process, which we didn't ask strace
to trace. If you add -f
then it will do so, but will also trace the contents of the ls
process.