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:

  1. 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.
  2. Load the real programexec. 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 write until 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-C
  • SIGTERM — please shut down cleanly
  • SIGKILL — 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:

  1. Command (text) vs process (living kernel object with a PID).
  2. Fork (new identity) vs exec (same identity, new program).
  3. File descriptor (the number) vs the file/pipe/terminal behind it.
  4. Parent (who created you) vs process group (who shares a job signal).
  5. 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.