Crisp answer: The shell forks a child process, the child calls exec to replace itself with the program image, the kernel loads the binary, and the program runs with file descriptors inherited from the shell.
The full sequence:
1. Shell reads and parses the command
You type ls -la /tmp. The shell (bash/zsh) reads it from stdin (the
terminal), parses it: command = ls, args = ["-la", "/tmp"].
2. Shell resolves the path
The shell searches PATH for ls:
which ls # /usr/bin/ls
type ls # ls is /usr/bin/ls
If the command is a shell builtin (cd, echo, export), the shell handles
it directly without forking. If it's an alias, it expands the alias first.
3. fork()
The shell calls fork(). This creates a copy of the shell process (child).
The child is nearly identical to the parent initially — same memory, same
file descriptors, same environment variables. fork() returns the child's
PID to the parent, and 0 to the child.
4. Child handles redirects and pipes
Before exec, the child sets up I/O redirections and pipe connections:
ls -la > output.txt
# Child: close stdout (fd 1), open output.txt, dup2 to fd 1
# Now any writes to stdout go to the file
5. execve()
The child calls execve("/usr/bin/ls", ["-la", "/tmp"], environ). This:
- Replaces the child's entire memory space with the
lsprogram image - Loads the ELF binary: reads the ELF header, maps segments into memory
- Loads the dynamic linker (
ld.so) which resolves shared library symbols - Jumps to the program's entry point (
main())
After execve(), the child IS ls — there's no more shell code running.
6. ls runs
ls reads directory entries, formats output, writes to stdout (fd 1). If
stdout was redirected, writes go to the file. If it was a pipe, writes go
to the read end of the pipe.
7. exit() and wait()
When ls finishes, it calls exit(0) (success). The kernel sends SIGCHLD
to the parent (shell). The shell calls wait() or waitpid() to collect
the exit status. If the shell didn't call wait(), the child becomes a
zombie — still in the process table until collected.
File descriptors inherited:
| FD | Default |
|---|---|
| 0 | stdin (terminal) |
| 1 | stdout (terminal) |
| 2 | stderr (terminal) |
All three are inherited by the child. Redirects and pipes modify them before
exec. The CLOEXEC flag (O_CLOEXEC) on a file descriptor causes it to be
closed automatically on exec — used for private fds the child shouldn't inherit.
What to say in the interview:
"The shell parses the command and searches PATH. If it's a builtin, it runs directly. Otherwise the shell calls fork() to create a child process, sets up any redirections or pipes by manipulating file descriptors, then calls execve() which replaces the child's memory space with the program. The dynamic linker resolves shared libraries. The program runs with inherited file descriptors. When it exits, the parent's wait() collects the exit status. The key insight: fork creates a copy, exec replaces it. That's why environment variables set before a command work — they're inherited through fork before exec overwrites the memory."