Reading — step 1 of 5
Read
Processes & Threads
A program is a file. A process is that file caught in the act: an address space, at least one thread of execution, and a pile of kernel bookkeeping that exists only while it runs. The kernel's entire job description — isolate, schedule, mediate — is phrased in terms of processes, so before we can schedule them we need to know exactly what one is and where they come from.
What the kernel remembers about you
For every process, the kernel keeps a control block (Linux calls it task_struct — thousands of bytes of truth):
- Identity: the PID, and crucially the parent PID — who created it.
- Address space: the root of its page tables (the physical address the kernel loads into
CR3when this process runs). Two processes are isolated because this pointer differs. - Saved CPU state: when the process is off-CPU, its registers, stack pointer, and program counter live here. A "process that isn't running" is precisely this frozen snapshot.
- State: running, ready (wants CPU), blocked (waiting on I/O — no CPU desired), or zombie (dead, but exit status not yet collected).
- Resources: open file descriptors, working directory, signal handlers, credentials.
fork(): the only way in
Unix has exactly one way to create a process: an existing one forks. fork() clones the caller — same code, same (copy-on-write) memory, same open files — and returns twice: the parent gets the child's PID, the child gets 0. Everything that has ever run on a Unix machine descends from PID 1 (init), forked into existence by the kernel at boot.
That gives processes a shape: a tree.
init(1)
├── sshd(800)
│ └── bash(2001)
│ ├── vim(2100)
│ └── make(2101)
│ └── cc(2102)
└── cron(801)
The tree isn't decorative — the kernel uses it constantly. wait() lets a parent collect a child's exit status (until then the child lingers as a zombie). Kill a parent and its children get reparented to init (orphans always have a guardian). Process groups and sessions — the machinery behind Ctrl-C killing your pipeline but not your shell — are subtrees.
Your exercise builds exactly this structure: consume FORK child parent events, assemble the tree, and print a subtree in preorder (node, then children) — the same walk pstree does when it draws the picture above, and the same order a kernel walks to deliver a signal to "this process and everything under it."
Threads: processes that share
A thread is an execution context — its own registers, stack, and program counter — inside a shared address space. Two threads of one process see the same globals and heap; two processes never do (unless they explicitly ask).
That single design choice cuts both ways:
- Cheap: creating a thread doesn't copy an address space; switching between threads of one process doesn't even change
CR3(the page tables are the same — this matters in the context-switch lesson). - Dangerous: shared-everything means every write to shared data is a potential race. Isolation was the whole point of processes; threads deliberately trade it away for speed.
Linux blurs the boundary elegantly: internally everything is a task, and fork() vs pthread_create() are the same clone() syscall with different sharing flags — "share the address space? the file table? the signal handlers?" A process is just a task that shares nothing.
The state machine every scheduler runs on
┌────────── preempted ──────────┐
▼ │
READY ───── scheduled ────▶ RUNNING ── exit ──▶ ZOMBIE ── wait() ──▶ gone
▲ │
└── I/O done ── BLOCKED ◀── I/O wait
Only ready tasks compete for the CPU. Blocked tasks cost nothing until their event fires — a machine with 400 processes typically has 397 blocked, 2 ready, 1 running. Keep this diagram in your head: the next three lessons (scheduling policy, the context switch itself, and MLFQ) are all just arguments about which READY task gets promoted, when, and how much the promotion costs.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…