Frontend Labs
⋯

Chapter 3 — A program's whole life, from ./app to exit code

In one sentence

Running a program is a specific, well-defined sequence — the shell asks the kernel to create a new process, load your code into it, and start it running; the kernel tracks it until it finishes; and everyone who cares can find out how it ended.

Why this matters

"Why didn't my script's error get caught," "why is this process a zombie," and "how does Docker know my container exited" are all answered by understanding this one lifecycle.

How it actually works

   1. YOU TYPE: ./myapp arg1 arg2

   2. YOUR SHELL calls fork() (Chapter 6) -- the kernel clones the
      shell's own process into a near-identical CHILD process.

   3. THE CHILD calls exec() -- this REPLACES the child's code and
      memory with `myapp`'s code, but keeps the same process ID. The
      kernel loads the executable from disk, sets up its memory layout
      (Chapter 10), and starts running its `main()` function.

   4. THE PROCESS RUNS, doing whatever it does, making syscalls
      (Chapter 2) whenever it needs the kernel's help, getting scheduled
      on and off the CPU (Chapter 8) the whole time.

   5. THE PROCESS ENDS, one of three ways:
        - it returns from main() / calls exit(N) deliberately
        - it's killed by a SIGNAL (Chapter 20) -- e.g. you pressed
          Ctrl-C, or something sent SIGKILL
        - it crashes (an illegal memory access, division by zero, etc.
          -- the kernel delivers this as a signal too)
      Either way, the kernel records an EXIT STATUS: a small number, by
      convention 0 for "success" and non-zero for "something went wrong"
      (the SPECIFIC non-zero value is up to the program).

   6. THE PARENT (your shell) can call wait() to retrieve that exit
      status. Until it does, the finished process sits in the process
      table as a ZOMBIE -- it's done running, but the kernel keeps its
      exit status around until the parent asks for it. (A parent that
      never calls wait() leaks zombies -- Chapter 5 covers this properly.)
   $ ./myapp arg1 arg2
   $ echo $?              # the shell's own wait() already happened;
                          # $? holds the exit status it collected
   0

Real-world example

   Every CI/CD pipeline in existence (GitHub Actions, Jenkins, GitLab CI)
   is built on EXACTLY this mechanism: a build step is "successful" if
   and only if the process it ran exited with status 0. This is why a
   test script that finds a failure but forgets to `exit 1` will show as
   a PASSING build even though tests actually failed -- a extremely
   common, real, and costly class of CI bug. Conversely, a script that
   `exit`s non-zero for a condition that ISN'T actually a failure (a
   common one: `grep` returns exit status 1 when it finds NO matches,
   which is often not an error at all) can fail a build for no real
   reason. Understanding "the exit code IS the entire signal" explains
   both failure modes instantly, instead of them feeling like CI being
   flaky.

Try it yourself

# See exit codes in action
true; echo $?     # 0 -- the `true` command always succeeds
false; echo $?    # 1 -- the `false` command always "fails"
ls /nope; echo $?  # some non-zero number -- ls failed, file doesn't exist

# Watch a zombie process appear (harmless, in a controlled demo)
( sleep 2 & )      # subshell forks a child then exits without waiting
ps aux | grep defunct   # briefly, you may catch the zombie before
                        # your shell reaps it

# See fork+exec happen for real
strace -f -e trace=fork,execve,exit_group bash -c 'echo hi' 2>&1 | head -20

Common mistakes

  • Assuming exit code 0 always means "worked correctly." It only means "the program itself believes it finished without error" — that's only as reliable as the program's own logic.
  • Ignoring $? in scripts. If you don't check it, a failed step silently continues as if nothing went wrong (Chapter 39 covers set -e and proper error handling).
  • Confusing a zombie process with a hung process. A zombie has ALREADY finished; it's just waiting to be acknowledged. A hung process is still running (or stuck). top/ps show zombies with state Z — very different from a runaway process eating CPU.

Check yourself

  1. Walk through the steps from typing a command to seeing its output.
  2. What's the difference between fork() and exec(), at a high level?
  3. What is a zombie process, and why does it exist at all instead of just disappearing?
  4. Why can a CI pipeline report "success" even when your tests actually failed?

Further reading

  • Book (free): OSTEP, "Process API" chapter — walks through fork/exec/wait with real code.
  • Reference: man 2 exit, man 2 wait, man 1 bash (the "EXIT STATUS" section).
  • Article: "Unix process states explained" — search for a clear diagram of running/sleeping/zombie/stopped states (Chapter 5 covers this in full).

Generated from real-life-os-guide.md · 8 Oct 2026 16:36