Skip to content

The Process Model

A program on disk is just a file: inert bytes. A process is that program running — code loaded into memory, with its own slice of CPU time, its own open files, and an identity the kernel tracks. The gap between “a file exists” and “a process is running” is the gap the whole rest of DevOps operates in. Deploying is, at bottom, the act of turning the right file into the right process on the right machine, reliably. So we start here.

When the kernel starts a process, it hands out a PID (process ID), a unique number. You use the PID to ask about a process or to signal it. Every process also records its parent’s PID (the PPID), because processes don’t appear from nowhere — they are spawned by other processes.

PID 1 (systemd) ── the ancestor of everything
├── sshd (PID 812)
│ └── bash (PID 1340) ← your shell
│ └── python app.py (PID 1502) ← you started this
└── nginx (PID 905)

That tree is real: run pstree -p and you’ll see it. PID 1 is special — it’s the first process the kernel starts, and on a modern server that’s systemd (covered later). It’s the root of the whole tree and the eventual adopter of any orphan.

This is one of those ideas that looks strange until it clicks, and then a lot of things make sense. Unix creates a new process in two steps:

  1. fork() — a process clones itself. You now have two near-identical processes (parent and child) that differ mainly in their PID.
  2. exec() — the child then replaces its own program with a different one, keeping the same PID.
bash ──fork()──► bash (child copy) ──exec("python")──► python app.py
(parent stays) (same PID, now running python)

Why split it in two? Because that gap between fork and exec is where the child can adjust its environment — redirect output to a file, drop privileges, set variables — before the new program starts. That single design choice is what makes shell redirection and sudo and container startup possible. Hold onto it; it explains a lot in The Shell and Containers.

When a process finishes, it returns an exit code (a.k.a. status): an integer from 0 to 255. The convention is rigid and load-bearing:

  • 0 means success.
  • Any non-zero value means failure (and the number can hint which failure).
Terminal window
ls /etc; echo "exit code: $?" # 0 — the directory exists
ls /nope; echo "exit code: $?" # 2 — ls couldn't find it

$? holds the exit code of the last command. This tiny number is the bedrock of automation: a CI pipeline decides “did the tests pass?” by checking whether the test command exited 0. Scripts chain on it (build && deploy only deploys if the build returned 0). Get exit codes right in your own programs and the entire automation stack above you can trust them.

You don’t usually share memory with a running process — you send it a signal, a small asynchronous nudge from the kernel. The two that matter most for operations:

SignalNumberMeaningCan the process catch it?
SIGTERM15”Please shut down.”Yes — clean up, then exit
SIGKILL9”Die now.”No — kernel kills it instantly
SIGINT2Ctrl-C in the terminalYes
SIGHUP1Terminal closed / “reload config”Yes

The distinction between SIGTERM and SIGKILL is one of the most important operational facts in this book. SIGTERM is polite: it lets the process finish in-flight requests, flush buffers, and close database connections — a graceful shutdown. SIGKILL is the kill switch: instant, unstoppable, and it gives the process no chance to clean up, which can mean dropped requests or corrupted state.

Terminal window
kill 1502 # sends SIGTERM (the default) to PID 1502 — polite
kill -TERM 1502 # the same thing, explicit
kill -9 1502 # SIGKILL — last resort
pkill -f app.py # signal by matching the command line, not the PID

Foreground, background, and the logout problem

Section titled “Foreground, background, and the logout problem”

Start a program in a terminal and it runs in the foreground: it owns your terminal until it exits. Append & and it runs in the background, handing your prompt back.

Terminal window
python app.py # foreground — terminal is busy
python app.py & # background — you get your prompt back
jobs # list background jobs in this shell

But there’s a trap that bites every beginner: a backgrounded process is still tied to your shell session. Close the terminal and it usually dies. Running production software with & and nohup and crossing your fingers is precisely the manual, fragile habit this part exists to kill — it doesn’t survive logout, doesn’t restart on crash, and doesn’t come back after a reboot. The real fix isn’t a trick; it’s handing the process to a service manager, which is why systemd & Services exists.

Two tools you’ll use constantly. ps is a snapshot; top (or the friendlier htop) is a live view.

Terminal window
ps aux # every process: user, PID, %CPU, %MEM, command
ps aux | grep nginx # narrow to what you care about
ps -ef --forest # show the parent/child tree
top # live, sorted by CPU; press 'M' to sort by memory, 'q' to quit

In top, the columns that earn their keep are %CPU, %MEM, and RES (resident memory — actual RAM used). A process pinned at 100% CPU or steadily climbing memory is your first clue in an incident; we’ll build a full triage routine in Logs & Troubleshooting.

Notice what just happened: “deploy my app” decomposes into start a process (exec), supervise it (PID + signals), shut it down gracefully (SIGTERM), and know whether it succeeded (exit code). Every higher-level tool — systemd, Docker, Kubernetes — is an increasingly automated way of doing exactly these four things so a human doesn’t have to do them by hand on a live server. The manual version is slow and forgettable; the automated version is the rest of this book.

The process model isn’t a tool you adopt — it’s the substrate — but the same five questions reveal why it’s shaped the way it is:

  • Why does it exist? Because a program on disk is inert bytes; the kernel needs a way to turn a file into a running thing with an identity (a PID), a parent (PPID), CPU time, and open files — and “deploy” is precisely the act of turning the right file into the right process, reliably.
  • What problem does it solve? It gives operations one universal vocabulary — start (exec), supervise (PID + signals), stop gracefully (SIGTERM), and know whether it worked (exit code 0) — that every higher tool, systemd through Kubernetes, simply automates.
  • What are the trade-offs? The model is deliberately minimal: you don’t share memory with a process, you nudge it with asynchronous signals, and SIGKILL (9) cannot be caught — so a reflexive kill -9 costs in-flight requests, unflushed buffers, and risks corrupt state.
  • When should I work around it? Running real software with &/nohup is the trap — it dies on logout, won’t restart on crash, and won’t survive a reboot; hand it to a service manager (systemd & Services) instead.
  • What breaks if I get it wrong? Exit-code discipline (0 vs non-zero) is load-bearing for && chains and CI gates, and clean SIGTERM handling is what makes a rolling deploy lossless — botch either and pipelines can’t tell pass from fail and restarts drop requests.
  1. What is the difference between a program and a process, and why does that distinction matter when you “deploy”?
  2. Explain fork/exec in your own words. Why is splitting process creation into two steps useful rather than wasteful?
  3. Your script runs run-tests.sh && deploy.sh. What exit code must run-tests.sh return for deploy.sh to run, and why is that convention safe?
  4. A teammate kills a stuck app with kill -9. What did the app lose the chance to do, and what should they have tried first?
  5. Why does python app.py & fail as a way to run software in production, and which later page solves it properly?
Show answers
  1. A program is inert bytes on disk (a file); a process is that program running — code in memory with CPU time, open files, and a kernel-tracked identity (a PID). Deploying is exactly the act of turning the right file into the right process on the right machine, reliably — so the gap between file and process is the gap deployment lives in.
  2. fork() clones the current process (two near-identical copies differing mainly in PID); exec() then replaces the child’s program with a new one, keeping the same PID. Splitting it is useful because the gap between fork and exec is where the child can adjust its environment — redirect output, drop privileges, set variables — before the new program starts. That single design makes shell redirection, sudo, and container startup possible.
  3. run-tests.sh must return 0 (success) for deploy.sh to run, because && runs the right side only if the left exited 0. It’s safe because the convention is rigid — 0 = success, non-zero = failure — so the chain deploys only when tests genuinely passed.
  4. kill -9 sends SIGKILL, which the kernel applies instantly and the process cannot catch — so it loses the chance to gracefully shut down: finish in-flight requests, flush buffers, close DB connections. They should have sent the default SIGTERM first, given it a few seconds, and only escalated to SIGKILL if ignored — exactly how Kubernetes behaves.
  5. A backgrounded process is still tied to your shell session, so it usually dies on logout, doesn’t restart on crash, and doesn’t come back after a reboot. The proper fix is handing it to a service manager — systemd & Services.