What a process is, and how the shell runs a command
◈ 9 cardsA process is a program in execution. The shell runs an external command with fork, exec and wait — and a builtin with none of them.
A process is a program in execution
A program is a file on disk. A process is that program running: an image in memory with its own address space, its own open files, its own working directory, and a numeric identity — the PID — that the kernel hands out when the process is created and never reuses while the process lives.
UNIX has exactly one way to run an external command, and it is process creation followed by process termination. There is no other mechanism. Every time you type wc, grep, gcc or firefox, a new process comes into existence, does the work, and is destroyed. That single fact explains a surprising amount of shell behaviour, including several things that look like bugs the first time you meet them.
The three calls: fork, exec, wait
The kernel gives the shell three system calls and the shell composes them:
fork()creates a new process that is a copy of the caller. Both the original (the parent) and the copy (the child) return from the samefork()call and carry on at the next statement. They differ in exactly one visible way:fork()returns the child's PID in the parent and0in the child, so each can tell which one it is.exec()does not create a process. It replaces the image of the process that calls it with a different program. Same PID, same open files, entirely new code and data. Ifexec()succeeds it never returns, because the code that would have received the return value has been overwritten.wait()suspends the caller until one of its children terminates, and collects the child's exit status.
Worked example — tracing ls -l
You type ls -l and press Enter. Step by step:
- The shell reads the line, splits it into words, and expands anything that needs expanding. It looks up
ls: not a builtin, so it searchesPATHand finds/usr/bin/ls. - The shell calls
fork(). There are now two shells. They are identical — same code, same variables, same terminal — but one is the parent and one is the child. - The child calls
exec("/usr/bin/ls", …). Its shell image is thrown away and replaced byls. The PID does not change; the process that was a shell a microsecond ago is nowls, and it inherits the shell's standard output, which is why the listing lands on your terminal. - The parent calls
wait()and blocks. This is why the prompt does not come back whilelsis running — the shell is asleep, not busy. lsfinishes and callsexit(0). The kernel wakes the parent,wait()returns the child's status, and the shell prints the prompt again.
Now put & on the end — ls -l & — and step 4 disappears. The shell skips the wait() and prints the prompt immediately. Nothing else changes. Backgrounding is not a different mechanism; it is the same mechanism with the wait omitted.
A script is the same mechanism, one level deeper
Run a shell script and the shell forks a child as usual, but the child execs another shell, which opens the script file and reads it as its input. That child shell then forks and execs once per external command inside the script, waiting for each in turn. At end of file the child shell exits, and your original shell — which has been sitting in wait() the whole time — prints the prompt.
So a three-command script costs you four processes, not three: one child shell plus one per external command. That is also why a variable a script sets is gone when the script ends. The assignment happened in the child shell's memory, and the child shell no longer exists.
Builtins make no process at all
Some commands live inside the shell's own executable rather than in a file: cd, pwd, echo, export, read, set, shift, umask, wait, jobs, fg, bg, exit, trap. Running one costs zero new processes — the shell just calls its own internal function.
Some of these have to be builtins. cd is the clearest case. If cd were an external program the shell would fork, the child would change its own working directory, and the child would then exit — leaving the parent exactly where it started. A child cannot reach into its parent. cd can only work by being code the shell runs in its own process.
type -a name tells you which kind you have, and -a shows every match in order. On many systems type -a echo reports both a shell builtin and /usr/bin/echo; the builtin wins, and the binary exists only so that programs like find -exec have something to execute.
Why cd inside ( … ) does not move you
Parentheses are command grouping: the shell forks a subshell and runs the whole group inside it. So ( cd /usr; pwd ); pwd prints /usr and then your original directory. The cd really did happen — in the subshell, which then exited and took its working directory with it. This is the same parent/child independence that forces cd to be a builtin, viewed from the other side, and it is the cheapest way to visit a directory without having to remember to come back.