Step 68. The True Nature of a Process — The fork Experiment

Step 68. The True Nature of a Process — The fork Experiment

Level 1 — Programming and the Computer’s Interior | Difficulty ★★★☆☆ | Estimated time: 3 hours

Prerequisites: Steps 56–67 complete; you can write a simple program in C, compile it with gcc, and know what a system call is.

  • What you need: one Linux terminal (WSL or Ubuntu) and gcc. Today has little code and many experiments.
  • Caution: today’s practice is safe. However, each run will show slightly different numbers (PIDs) and line orders — and that is not a bug but the core of what we learn today.

Until now we’ve divided programs into "files stored on disk" and "something running." The formal name of that running something is a process. But where does this process come from, anyway? The answer is a bit strange. When the computer boots, process number 1 comes up, and every process after that is made by an existing process copying itself. That copy command is today’s protagonist, fork(). Even at the moment you type ls in a terminal, the shell forks — today you see this invisible cloning with your own eyes.


1. Learning Objectives

By the end of this chapter, you will be able to:

  • Explain processes, PIDs, and PPIDs, and observe them with ps and pstree
  • State fork()’s three rules (cloning, memory copy, return-value distinction)
  • Confirm by experiment why calling fork n times yields 2-to-the-n processes
  • Explain the procedure by which the shell runs commands with fork and exec
  • Explain why process isolation is a cornerstone of security

2. Background Knowledge — Today’s Tools and Concepts

Today’s Tools at a Glance

Category Details
Language/environment C language + Linux terminal (verified on WSL Ubuntu 24.04)
Today’s commands ps aux (process list), pstree -p (process family tree), sleep N & (run in background), jobs (jobs I launched), kill PID (send a signal to a process)
Today’s C functions fork(), getpid() (my PID), getppid() (parent PID)
Concepts needed Processes, PID/PPID, cloning and memory copies, COW (Copy-On-Write), fork + exec
Today’s deliverable A process observation log — ps/pstree observations and fork experiment records

2-1. Processes and PIDs

Every process carries a unique number called a PID (Process ID). Like a hospital check-in number tag, the operating system distinguishes and manages each process by this number. And each process also remembers the number of the parent that made it — the PPID (Parent PID). With just these two numbers you can draw a family tree of all processes, and the tool that shows that tree is pstree.

2-2. fork’s Rules — Exactly Three

fork() is the C function that creates a new process, and its rules are surprisingly simple.

  1. Call it, and the process becomes two. The parent (original) and the child (copy) each continue executing from the point right after the fork call.
  2. Memory is copied wholesale. The child receives a copy that duplicates the parent’s variables, arrays, and even the heap. Because it’s a copy, even if the child later changes its own variables, the parent’s variables stay the same.
  3. The return value tells who’s who. This is the strangest part. You called one function, but the return happens twice. The parent receives the child’s PID (a number greater than 0); the child receives 0. In code, you use this return value to decide "am I the parent or the child?" and assign different work to each.

2-3. Why Copy First?

"To launch a new program, why not just make it from scratch — why bother copying?" is a natural question. The reasons are efficiency and custom. Copying is astonishingly fast — Linux doesn’t duplicate memory immediately; it uses COW (Copy-On-Write), copying a part only at the moment someone changes it. On top of that, the copy inherits the parent’s open files, current directory, and environment variables as-is, so the shell doesn’t need to set up a fresh work environment every time it runs a command. Copying, then swapping in the desired program with an exec-family function, is Linux’s standard procedure.


3. Follow Along

3-1. Watching Processes — ps and pstree

First, let’s look at the processes running at this very moment.

Input

ps aux | head -n 6

Output (verified 2026-09-09):

USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.1  21776 13120 ?        Ss   13:32   0:00 /sbin/init
root           2  0.0  0.0   3180  2200 hvc0     Sl+  13:32   0:00 /init
root           6  0.0  0.0   3224  2152 hvc0     Sl+  13:32   0:00 plan9 --control-socket 7 ...
root          49  0.0  0.1  34044 12748 ?        S<s  13:32   0:00 /usr/lib/systemd/systemd-journald
root          98  0.0  0.0  25012  6316 ?        Ss   13:32   0:00 /usr/lib/systemd/systemd-udevd

How to read it: the second column is the PID. Process number 1 (/sbin/init) is the very first process to come up when the system boots. Everything else is a direct or indirect descendant of this number 1.

Why: getting a feel for "how many processes are running right now" is the first step. You’ll soon confirm that this entire list started with fork.

Next, the family tree.

Input

pstree -p | head -n 8

Output (verified 2026-09-09):

systemd(1)-+-Relay(683)(678)---python3(686)
           |-agetty(201)
           |-containerd(190)-+-{containerd}(224)
           |                 |-{containerd}(225)
           |                 `- ...
           ...

How to read it: the branches (─, ┬) and indentation are parent-child relationships, and the numbers in parentheses are PIDs. systemd(1) at the top is the ancestor of all processes. Even the pstree you just ran appears in this picture as a child of the shell (bash).

Why: the step of visually confirming that processes form a tree structure, not a flat list — and that the shell becomes the parent of commands.

3-2. Predict — What If You Call fork Once?

Before the real experiment, predict first. How many lines will the program below print? What will the value of variable x be in each? Write it on paper.

#include <stdio.h>
#include <unistd.h>

int main(void) {
    int x = 100;
    pid_t pid = fork();
    if (pid == 0) {
        x = 999;
        printf("child: x=%d, my PID=%d, parent PID=%d\n", x, getpid(), getppid());
    } else {
        printf("parent: x=%d, my PID=%d, child PID=%d\n", x, getpid(), pid);
    }
    return 0;
}

pid_t is the name of the integer type that holds a PID; getpid() tells you "my own PID," and getppid() tells you "my parent’s PID." if (pid == 0) is exactly the fork in the road asking "am I the child?"

3-3. Confirm Yourself — The Moment of Cloning

Save the code above as forktest.c, compile and run it. Run it three times in a row.

Input

gcc -o forktest forktest.c
./forktest; ./forktest; ./forktest

Output (verified 2026-09-09):

parent: x=100, my PID=1143, child PID=1144
child: x=999, my PID=1144, parent PID=1143
parent: x=100, my PID=1146, child PID=1147
child: x=999, my PID=1147, parent PID=1138
parent: x=100, my PID=1149, child PID=1150
child: x=999, my PID=1150, parent PID=1149

How to read it: there’s only one printf, yet each run prints two lines. At the fork point the execution flow split in two — the parent passed through the else branch, the child through the if branch. The parent’s x is 100, the child’s x is 999 — they differ. The child changed x, but the parent’s x was unaffected, because memory was copied. The PID numbers change on every run.

Now look closely at the second run. The parent PID the child printed is 1138, different from the real parent (1146) — even though the child number the parent printed (1147) and the child’s own number (1147) match. If the parent exits first, the orphaned child gets adopted by another process, and calling getppid() after that returns the new foster parent’s number. It’s normal for results to differ subtly each run, and being able to explain why is real understanding.

Why: this output proves all three of fork’s rules. The output order sometimes comes out flipped top-to-bottom — also normal, because which of the two gets the CPU first differs each time.

3-4. Predict — What If You Call fork Three Times in a Row?

Raising the difficulty. If you run the code below, how many processes will there be in total?

#include <stdio.h>
#include <unistd.h>

int main(void) {
    fork();
    fork();
    fork();
    printf("PID=%d\n", getpid());
    return 0;
}

Hint: at the first fork, 1 becomes 2; at the second fork, each of those 2 clones, making 4; at the third, each of those 4 clones. Save it as fork3.c and confirm yourself.

Input

gcc -o fork3 fork3.c
./fork3 | wc -l
./fork3 | sort -u | wc -l

Output (verified 2026-09-09):

8
8

How to read it: wc -l counts lines. Exactly 8 lines of output — 2 to the 3rd power. The second command confirms that even after sort -u (removing duplicate lines) it’s still 8 lines — meaning all 8 lines’ PIDs are different numbers. Evidence that 8 processes each existed.

Why: the sense that fork grows "exponentially" matters in security. There’s a classic attack called the fork bomb — calling fork in an infinite loop — simple and scary code that paralyzes a system by exploding the process count. You who saw 8 today now understand in principle why that bomb is scary. (Never run a fork bomb, even in your own lab. Understanding the principle is enough.)

3-5. Making and Observing Processes Directly in the Shell

Finally, make and observe processes in the shell, with no code.

Input

sleep 30 &
jobs
ps aux | grep "sleep 30" | grep -v grep

Output (verified 2026-09-09):

[1] 1188
[1]+  Running                 sleep 30 &
root        1188  0.0  0.0   3132  1900 pts/2    S+   13:37   0:00 sleep 30

How to read it: sleep 30 is a program that does nothing for 30 seconds, and the & at the end means "run it in the background." In the first line’s [1] 1188, 1188 is this process’s PID, and it appears with the same number in both jobs (the list of background jobs I launched) and ps. Even at the moment you type this command, the shell makes a child with fork, and that child transforms (exec) into sleep.

The command to kill this process is kill 1188. Despite the name, kill is a command that "sends a signal to a process," and since the default signal is a termination request, it ends up killing the process. (Per this book’s verified-lab rules, I didn’t run kill, so I show it only as a screen example: [1]+ Terminated sleep 30.) In your terminal it ends by itself after 30 seconds, so you can wait — or finish it yourself with kill.

Why: the step of physically learning that a PID isn’t a mere number but a handle by which the operating system commands processes. Later, in incident analysis, the work of "finding a strange process and killing it by PID" will be done with exactly this hand movement.

3-6. Seeing fork’s True Form with strace (Screen example)

Remember strace from Step 67? fork, too, is ultimately a request to the kernel — a system call. If you’re in an environment with strace installed, confirm it yourself. (Since this book’s verified lab has no strace, the below is a screen example.)

Input (example)

strace -f -e trace=clone ./forktest 2>&1 | head -n 4

Screen example (how it looks in a typical Ubuntu environment):

clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|...) = 1144
parent: x=100, my PID=1143, child PID=1144
child: x=999, my PID=1144, parent PID=1143
+++ exited with 0 +++

How to read it: in Linux, fork is implemented internally with a system call called clone. -f means "follow and trace the children too," and -e trace=clone means "pick out and show only clone requests." Confirm that clone’s return value matches exactly the child PID the parent received.

Why: the three-stage flow "library function → system call → kernel" applies to fork as-is. Even process creation is a request passing through the kernel’s threshold — and so monitoring tools can see the birth of every new process.


4. Missions & Exercises

Mission — A Process Observation Log

  1. Find the PID 1 process in ps aux and write down its name
  2. Find your own shell (bash) in pstree -p and climb upward to see who its parent is
  3. Run forktest.c three times in a row, recording how the output order and PIDs differ each time. If a case appears where the parent PID the child printed differs from the real parent, reason out why and write it down
  4. Run sleep 60 & twice to launch two processes, confirm both PIDs with ps aux | grep sleep, then kill one with kill and confirm via jobs that the other ends by itself after a minute
  5. To finish, write an explanation of "why does fork have two return values?" in your own words, within three sentences

Exercises

Q1. Explain why one line of fork() produces two lines of output, from the perspective of "script and actors."

Q2. Explain step by step how calling fork three times in a row yields 8 processes.

Q3. Explain why the parent’s x stays 100 even though the child changed x to 999, and why this property matters in security.

Q4. Explain what happens when you type ls in the shell, using the three words fork, exec, and wait.


5. Model Answers & Completion Criteria

Mission Model Answer

An example skeleton of the observation log (based on the 2026-09-09 verification — your PIDs and tree shape may differ):

1. PID 1 = /sbin/init (shown as systemd(1) in pstree) — the ancestor of all processes
2. My bash's parent is ... climbing up eventually reaches number 1
3. forktest 3 runs: PIDs are new, increasing numbers each time (1143/1144 → 1146/1147 → 1149/1150).
   On run 2, the child's parent PID printed as 1138, different → the parent exited first
   and the child was adopted by another process (re-adoption of an orphan process)
4. Confirmed both sleeps' PIDs, killed one → Terminated confirmed in jobs,
   the remaining one changed to Done after 60 seconds
5. After a fork call, two processes exist in the world. It's not that the return
   happens twice — two processes each receive one return. So from the same call,
   the parent gets the child's PID and the child gets 0.

How to verify: ① Was fork3’s | wc -l equal to 8? ② Did you confirm it’s still 8 even after sort -u (8 distinct PIDs)? ③ Did the observation log record that "the order can differ each time"? If all three are "yes," it’s complete.

Exercise Solutions

Q1 solution. From the line below fork, two actors (the parent and child processes) read the same script (code). One script, two actors. printf is one line in the script, but since two actors each read it, the output is two lines. The return value (parent: child PID; child: 0) is the point where each one’s lines diverge.

Q2 solution. At the first fork, 1 splits into 2. What reaches the second fork line is those 2, and each clones, making 4. The 4 that reach the third fork line each clone again, making 8. 2×2×2 = 2³ = 8. printf is one line, but 8 processes each execute it, so the output is also 8 lines (confirmed in the 2026-09-09 verification).

Q3 solution. Because the child receives a copy of the parent’s memory. Original and copy are completely separate after the moment of cloning. Why this property matters: put conversely, it means no process can freely peek into another process’s memory. Without this isolation, you could just read a banking app process’s password from a browser process. Process isolation is the starting point of operating system security.

Q4 solution. ① The shell makes a copy of itself (a child) with fork. ② The child swaps its own body for the requested program (ls) with an exec-family function. ③ The parent shell waits with wait for the child to finish, then shows the prompt again. Adding & merely skips the waiting in ③. Every command line was a ritual of cloning and transformation all along.

Completion Criteria Checklist

  • [ ] I can explain processes, PIDs, and PPIDs
  • [ ] I can state fork()’s three rules (cloning, memory copy, return-value distinction)
  • [ ] I can explain why calling fork three times yields 8 processes
  • [ ] I can explain why the parent PID a child prints can differ (re-adoption)
  • [ ] I can explain the procedure by which the shell runs commands with fork, exec, and wait
  • [ ] I can read process lists and family trees with ps and pstree
  • [ ] Mission: I completed the process observation log

6. Common Pitfalls & Fixes

Wall 1. The output order differs every time

Symptom: each run of forktest, the parent comes first, then the child comes first.
Cause: the operating system’s scheduler (the manager deciding which process gets the CPU) sets the order according to the circumstances of the moment.
Fix: not broken — normal. "Order is not guaranteed" is itself a property of fork. If you need order, use the wait() function, which makes the parent wait for the child.

Wall 2. I don’t get why fork()’s return value comes out twice

Symptom: you’re stuck on "how can one function return a value twice?"
Cause: it’s not that the return happens twice — after the call, two processes exist in the world, and each receives one return.
Fix: imagine "from the line below fork, two actors read the same script." One script, two actors. The return value is why each one’s lines differ.

Wall 3. The value the child changed isn’t reflected in the parent

Symptom: you changed a variable in the child, but it’s unchanged in the parent (x=100 in the 3-3 verification).
Cause: the child receives a copy of the parent’s memory. Original and copy are completely separate after the moment of cloning.
Fix: this isn’t an error — it’s process isolation, a cornerstone of security. To pass values between processes, you need a separate communication channel (IPC) like a pipe.

Wall 4. The parent PID the child printed differs from the real parent

Symptom (verified 2026-09-09): child: x=999, my PID=1147, parent PID=1138 — the parent is clearly 1146, yet 1138 is printed.
Cause: if the parent exits before the child, the orphaned child is adopted by another process designated by the operating system. After that, getppid() returns the new foster parent’s number.
Fix: not a bug — Linux’s normal behavior. To keep the parent alive longer than the child, put wait(NULL); on the parent side so it waits for the child’s exit.

Wall 5. It compiled, but the output looks weird when run

Symptom: printf exists, but there’s no output or it looks duplicated.
Cause: printf’s output is batched in a buffer (a temporary store) and released all at once. If the buffer is copied wholesale right after fork, contents can come out twice or timing can be off.
Fix: check that your printf strings end with \n (newline); if it’s still weird, put fflush(stdout); before fork so the buffer is emptied before cloning.


7. Summary

Today’s Concepts

Concept One-line description
Process The substance of a running program — carries a unique number, the PID
PID / PPID Process number / parent process number — the two axes for drawing the family tree
fork() The process-cloning function — the parent gets the child’s PID, the child gets 0
COW (Copy-On-Write) The technique of deferring copies and duplicating only at the moment of writing — why fork is fast
fork + exec The shell’s command-execution procedure: after cloning, the child transforms into the program
Fork bomb An attack paralyzing a system with infinite fork repetition — understand the principle only; never run it

Today’s Commands/Functions

Command/function What it does
ps aux List of currently running processes (second column is PID)
pstree -p Process family tree (parentheses hold PIDs)
command & / jobs Run in background / list of jobs I launched
kill PID Send a signal to a process (default: termination request)
fork() Clone a process (returns: child PID to the parent, 0 to the child)
getpid() / getppid() Find out my PID / my parent’s PID

The Instinct That Matters More Than Commands

The substance of the word "execution" has become vivid. The threshold where a file becomes a process, and even the way processes give birth to processes — the operating system’s daily life was reproduced at your fingertips. One number to remember: n forks = 2ⁿ processes. One sentence to remember too: "even if the child changes it, the parent doesn’t know." Without this isolation, any program could see others’ memory; the operating system guards this isolation with its life, and attackers develop every technique to cross it.

The security connection: the processes you launched and observed in the experiments were all things you made, in your own lab. This principle — things I made, only in my lab — holds for every experiment ahead. ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.


Once every box is checked, Step 68 is complete. Click the checkbox in the sidebar to save your progress.