A process is not a command
A guided walk through what actually happens when you run a command — processes, file descriptors, pipes, and why Ctrl-C hits the right thing — before Firecracker and microVMs.
You type grep and press Enter. Something runs. We call that something a
process, and then we talk as if the word were obvious.
It isn't. A command is the text you typed. A process is a live thing the operating system is managing — an ID, some memory, open files, a parent, a place in a family tree. The shell is the translator between those two worlds.
This lesson walks that translation top to bottom. Each step introduces a few
words, shows them in a terminal, and tells you what every column means. Keep a
real terminal open if you want; every session has a Copy button. On macOS
there is no /proc — use the macOS toggle where you see one.
By the end you should be able to answer, in plain language: what is a process, how does a command become one, what is a file descriptor, why pipes block, and why killing a parent does not kill its children.
1. Your shell is already a process
Before you type anything, your terminal is already talking to a running program: your shell (zsh, bash, fish…). That shell is a process.
Two words you need right now:
- PID — process ID. A number the kernel gives each living process so it can tell them apart. Like a ticket number.
- PPID — parent PID. Which process created this one. Your shell was started
by your terminal app (or by
login/ssh).
$$ is shell shorthand for “my own PID.”
your shell
Words you just saw
- PID
- This process’s ID — its ticket number in the kernel.
- PPID
- Parent PID — who started this process.
- PGID
- Process group ID — which job/group it belongs to. We use this later for Ctrl-C and kill.
- STAT
- Short state code. Ss here means sleeping session leader — idle shell waiting for input.
- TT / TTY
- Which terminal this process is attached to (pts/2, ttys002, …).
- COMMAND
- The program this process is currently running (zsh).
So: the thing reading your keystrokes is not “the computer.” It is one process, with a number, and a parent.
2. What you type is shell language
When you write:
grep localhost < /etc/hosts
it looks like grep is being told about < and a filename. It isn't.
- A redirect (
<,>) is an instruction to the shell: “before you start the program, attach this file as its input/output.” - A pipe (
|) is also an instruction to the shell: “connect program A's output to program B's input.”
The program only sees its arguments (the words after the command name). It
never receives < or |.
shell syntax
Words you just saw
- arguments (argv)
- The word list the program receives. For grep localhost, that is just ["grep", "localhost"].
- redirect <
- Shell instruction: open this file and attach it as the program’s stdin (fd 0) before start.
- pipe |
- Shell instruction: connect the left program’s stdout to the right program’s stdin.
Hold that idea. Next we watch the shell actually create a process to run the command.
3. One command, two steps: make a process, then load a program
In your head, “run grep” is one action. Unix does it in two:
- Make a new process — usually by fork (or a close cousin). Think: photocopy the shell. The copy is a child. It gets a new PID. It still starts life running the shell's program for a moment.
- Load the real program — exec. The child throws away the shell's code
and becomes
grep. Same PID. New program.
Two more words:
- fork — create another running identity (a child process).
- exec — replace the program inside an existing process. Does not create a new PID.
For a redirect, between fork and exec the child also opens the file and attaches it as input. That attach step uses a file descriptor — we define that fully in the next section. For now: “a numbered handle the process uses to read/write.”
The recording below is a guided trace of those steps. You do not need to memorize syscall names. Read the plain-English notes under each line.
Guided trace · fork then exec
Follow one command through the kernel
This is a teaching recording of what Linux does for grep localhost < /etc/hosts. Read the notes. On Linux you can reproduce with strace; on macOS, follow along here.
guided trace (Linux)
What to notice
- New PID appears at clone/fork — that is the child process.
- openat + dup2 happen before exec — that is the redirect.
- execve changes the program; the PID stays the same.
- wait4 is the parent collecting the child’s exit status.
Words you just saw
- clone / fork
- Create a child process. New PID.
- openat
- Open a file and get a file descriptor number back.
- dup2
- Make one fd number point at the same thing as another (here: make 0 mean the file).
- execve
- Replace this process’s program. PID unchanged.
- wait4
- Parent blocks until the child exits, then reads its status (reaps).
Checkpoint: a command is text. A process is a numbered living object. Fork makes the object. Exec changes what program it is running.
4. A file descriptor is a coat-check number
When any program opens a file (or a pipe, or a network connection), the kernel does not hand it the file itself. It hands back a small integer: 0, 1, 2, 3…
That integer is a file descriptor (often shortened to fd).
Coat-check metaphor:
- the number is your tag (the fd);
- the cloakroom ledger remembers what you checked in;
- the coat is the real thing (file, pipe, terminal).
Every process starts with three tags already issued:
- fd 0 — stdin — where the process reads input
- fd 1 — stdout — where it writes normal output
- fd 2 — stderr — where it writes errors
This is why redirects work. grep always writes to fd 1. The shell can point
fd 1 at a file, a pipe, or your terminal before grep starts. grep does
not care.
Session · file descriptors
See the three starter tags
List the open files for your shell. You should recognize 0/1/2 pointing at your terminal.
Linux — /proc
Words you just saw
- fd 0 / 1 / 2
- stdin / stdout / stderr — the three tags every process starts with.
- /dev/pts… or /dev/ttys…
- Your terminal device — keyboard in, screen out.
- lsof
- “List open files” — macOS/Linux tool to see what a process’s fds point at.
Checkpoint: programs talk to the world through small integers. The shell can rewire those integers without changing the program.
5. A pipe is a small waiting room for bytes
A pipe is a kernel-managed buffer between two processes:
writer → [ small buffer ] → reader
Rules you can feel:
- If the buffer is full, the writer pauses inside
writeuntil space appears. - If the buffer is empty but a writer still exists, the reader pauses inside
read. - If the buffer is empty and every writer has closed its end, the reader gets end-of-file (EOF) — “no more data is coming.”
That automatic pause is backpressure. Nobody sends a “slow down” message.
The classic footgun: if some process accidentally keeps the write end open, the reader may wait forever for EOF that never comes.
Session · pipes
Watch stdout become a pipe
First see fd 1 change under a pipe. Then, if you want the “aha,” use two terminals with a named pipe.
1 — descriptors
What to notice
- Same ls binary both times. Only the shell’s wiring of fd 1 changed.
- Named pipes (mkfifo) let you feel “reader waits until writer appears,” then EOF when the writer closes.
Words you just saw
- pipe:[…] / TYPE=PIPE
- This file descriptor points at a kernel pipe buffer, not a file on disk.
- EOF
- End of file — “no more bytes coming,” which happens when every write end is closed.
- mkfifo
- Create a named pipe as a path on disk so two terminals can meet.
Checkpoint: pipes are not teleports. They are small buffers with blocking and a clear “everyone closed” signal.
6. Exiting is only half of dying
When a process finishes, the kernel frees almost everything — but keeps a tiny
record: “this PID exited, here is its status.” That leftover record is a
zombie. It runs no code. It just waits for the parent to read the status
(via wait / waitpid). That pickup is called reaping.
If the parent dies first, the child is reparented (often to PID 1), and someone else becomes responsible for reaping.
You do not need to hunt zombies day to day. You need the idea: exit ≠ gone from the kernel's books until a parent collects the status.
7. Signals, and why your kids don't die when you do
A signal is a short kernel message to a process: “something happened.”
The ones you will actually meet:
SIGINT— you pressed Ctrl-CSIGTERM— please shut down cleanlySIGKILL— die immediately (cannot be caught)
SIGTERM first, SIGKILL only if it will not leave. Always collect the exit
status afterward when you are the parent.
Now the trap. Suppose:
your shell
└── build (PID 901)
└── worker (PID 902)
You kill PID 901. Does 902 die? No. Parentage is a birth record, not a cascading delete. 902 keeps running and gets a new parent.
To stop a whole job, the shell puts related processes in a process
group (shared PGID). Then you can signal the group at once. In the kill
command, a negative number means “the group,” not “one PID.”
Session · jobs and signals
Kill one PID vs kill a whole job
Start a tiny job, kill only the parent, watch children survive, then signal the process group.
start a job
Words you just saw
- PGID
- Process group ID — shared by members of one job.
- kill -TERM PID
- Politely ask one process to exit.
- kill -TERM -PGID
- Negative number = address the whole process group, not one PID.
- reparented
- When a parent dies, children get a new parent (often PID 1) and keep running.
Why Ctrl-C hits the right program
Your shell is still alive when you hit Ctrl-C during a command. Why doesn't the shell die?
Because the terminal tracks a foreground process group — whoever currently
“has the mic.” Ctrl-C becomes SIGINT delivered to that group, not to every
process on the machine. The shell hands the mic to the job, and takes it back
when the job finishes.
8. A PTY is a fake terminal (short version)
Long ago a terminal was physical hardware. Today your Terminal.app / iTerm / SSH session pretends to be one using a pseudo-terminal (PTY):
- master side — owned by the terminal app;
- slave side — what the shell thinks is “the terminal.”
Programs often ask “am I talking to a real terminal?” (isatty). If the answer
is no (for example, input is a pipe), they change behavior: less color, no
interactive editing, and so on.
So if you want a program to behave interactively inside a sandbox, two plain pipes are not enough. You need a PTY. That detail matters later for command runners and microVMs — file it away.
terminal vs pipe
Words you just saw
- tty
- The terminal device path your shell is attached to.
- exit status 130
- 128 + 2. Signal 2 is SIGINT (Ctrl-C). The foreground job got interrupted.
- isatty
- True when stdin is a terminal; false when stdin is a pipe/file.
Why this comes before microVMs
A virtual machine does not replace any of this.
Inside the guest, Linux still creates processes, hands out file descriptors,
blocks on pipes, delivers signals, and reaps children. The host does not
waitpid for programs inside the guest — the guest kernel does.
Later, when Firecracker shows up, the useful question is:
Which of these jobs belong to the guest, which belong to the host, and what crosses the boundary?
Without today's picture, a microVM looks like one magic box. With it, you see two ordinary layers using the same rules.
What to keep
If only five distinctions stick, make them these:
- Command (text) vs process (living kernel object with a PID).
- Fork (new identity) vs exec (same identity, new program).
- File descriptor (the number) vs the file/pipe/terminal behind it.
- Parent (who created you) vs process group (who shares a job signal).
- SIGTERM (please leave) vs SIGKILL (you are gone).
Next lesson: memory that does not exist yet — why an address is not the same thing as RAM.