Step 1 of 5 · Reading · ~3 min
Read
Execution & Pipelines
fork, exec and wait
After this lesson you can model, without leaving your program, the three system calls a shell uses to run a command: fork makes a copy of the shell, exec turns the copy into the new program, and wait collects the result. You will track a process table through exits, signals, zombies and orphans, and print the exit statuses exactly as a shell reports them in $?.
Three calls, one pattern
Unix has no call that means "run this program as a new process". It splits the job:
shell: fork --> child (a copy of the shell, new pid)
child: exec "ls" --> same pid, memory replaced by ls
child: ls runs, then exits with a status
shell: wait --> gets the child's pid and status
fork returns twice: in the parent it returns the child's pid, in the child it returns 0. That one difference is how each side knows which it is. exec never returns on success, because the old program no longer exists; the process keeps its pid and its parent. Splitting the work lets the child adjust its own file descriptors between the two calls, which is how redirection and pipes work (the next lessons).
The process table
For each process the kernel keeps a parent pid, the program name, and a state. In this model there are three states: running, zombie and gone.
A process that exits does not vanish. The kernel keeps a small record (its exit code or the signal that killed it) until the parent collects it with wait. That record is a zombie. After the parent waits, the process is gone. A zombie uses no CPU, but it holds a table slot, so a shell that forgets to wait leaks them.
Statuses and $?
A child either exits with a code from 0 to 255 or is killed by a signal. The shell stores one number for $?: the exit code itself, or 128 plus the signal number. That is why a command killed by SIGKILL (signal 9) shows $? as 137, and Ctrl-C (SIGINT, 2) as 130. An exit code outside 0 to 255 wraps, so exit 300 reports 44.
Waiting
wait looks only at the caller's own children, never grandchildren. If it has none it fails with ECHILD. If it has children but none has finished, a real call would block until one does; the model says blocked. If several are zombies it reaps the one with the lowest pid, so repeated waits give the children one at a time.
Orphans and init
When a process exits while it still has children, those children are not left parentless. The kernel hands them to the init process (pid 1). Init does nothing but wait, so any child of init that is a zombie is collected at once, and any later child of init that exits is collected on the spot. Here the shell has pid 100 and is itself a child of init; new pids start at 101 and only go up.
Signals in this model
KILL pid sig with signal 1, 2, 9 or 15 terminates a running process, which becomes a zombie that remembers the signal. Signal 0 only checks that the process exists. Any other signal does nothing. Killing a zombie succeeds and changes nothing; killing a process that is already gone fails.
Common mistakes
- Treating exit as removal. The process must stay a zombie until it is waited for.
- Letting the parent reap a grandchild.
- Forgetting to hand children to init, so their parent field still names a dead process.
- Reporting a signal death as the plain signal number instead of 128 plus it.
Your exercise
The starter reads commands and holds the process table. You write the command handler and the routine that terminates a process. The most likely mistake is skipping the reparenting when a process with children exits.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…