The first interlude: a practical chapter about real OS APIs. (The book’s aside: if you don’t like practical things, you could skip these — but you shouldn’t, because “companies don’t usually hire you for your non-practical skills.”) The subject is how UNIX creates processes, with one of the most intriguing designs in systems:
The Crux: How To Create And Control Processes
What interfaces should the OS present for process creation and control? How should they be designed to enable powerful functionality, ease of use, and high performance?UNIX’s answer splits the job across a strange pair — fork() and
exec() — plus wait() for a parent that wants to wait for a child it
created.
5.1 The fork() System Call
fork() fork UNIX call creating a new process as an (almost) exact copy of the caller; the parent receives the child's PID, the child receives 0. defined in ch. 5 — open in glossary creates a new process — and it is, the book warns, “certainly the strangest routine you will ever call.” Figure 5.1:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
printf("hello world (pid:%d)\n", (int) getpid());
int rc = fork();
if (rc < 0) {
// fork failed
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) {
// child (new process)
printf("hello, I am child (pid:%d)\n", (int) getpid());
} else {
// parent goes down this path (main)
printf("hello, I am parent of %d (pid:%d)\n",
rc, (int) getpid());
}
return 0;
}
Figure 5.1: Calling fork() (p1.c)
prompt> ./p1
hello world (pid:29146)
hello, I am parent of 29147 (pid:29146)
hello, I am child (pid:29147)
prompt>
What happened: the process printed hello (with its PID, 29146 — the name
UNIX uses when you want to do something to a process). Then it called
fork(), and the process the OS created is an (almost) exact copy of
the caller. Two oddities follow:
- The child does not start at
main(). Note “hello world” printed only once. The child comes into life as if it had calledfork()itself — both processes return from the same call. The child has its own copy of the address space, its own registers, its own PC… - …but a different return value. The parent receives the child’s PID (29147); the child receives 0. That one difference is what lets a single program branch cleanly into the two roles, exactly as p1.c’s if/else does.
Step through the run above:
The process prints "hello world (pid:29146)". One process exists.
Now the third surprise: run it again and you may get different output. After fork there are two active processes; on a single CPU, either might run next — the CPU scheduler scheduler The OS component whose policy decides which ready process runs next, using history, workload knowledge, and metrics. defined in ch. 4 — open in glossary decides, and we can’t usually assume what it will choose:
prompt> ./p1
hello world (pid:29146)
hello, I am child (pid:29147)
hello, I am parent of 29147 (pid:29146)
prompt>
Same start: "hello world" prints once.
This nondeterminism nondeterminism When output/behavior depends on scheduling timing — e.g., whether parent or child prints first after fork(). defined in ch. 5 — open in glossary looks harmless here — two lines swap. It becomes genuinely dangerous in multi-threaded programs, where you already saw it corrupt a counter in chapter 2; Part II is full of it.
5.2 The wait() System Call
Sometimes a parent should pause until its child finishes. That’s
wait() wait UNIX call letting a parent pause until a child finishes (waitpid() is the fuller sibling); also reaps the zombie.
defined in ch. 5 — open in glossary
(or its more complete sibling waitpid()).
Figure 5.2 adds one line to the parent’s branch:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[]) {
printf("hello world (pid:%d)\n", (int) getpid());
int rc = fork();
if (rc < 0) { // fork failed; exit
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) { // child (new process)
printf("hello, I am child (pid:%d)\n", (int) getpid());
} else { // parent goes down this path (main)
int rc_wait = wait(NULL);
printf("hello, I am parent of %d (rc_wait:%d) (pid:%d)\n",
rc, rc_wait, (int) getpid());
}
return 0;
}
Figure 5.2: Calling fork() And wait() (p2.c)
The book invites you to predict: why is the output now deterministic? Think first — then step:
"hello world" prints once, as before.
prompt> ./p2
hello world (pid:29266)
hello, I am child (pid:29267)
hello, I am parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
The child always prints first. If the child runs first, that’s obvious. If
the parent runs first, it immediately calls wait(), which won’t return
until the child has run and exited — so the parent politely blocks either
way. (Footnote fine print: there are a few cases where wait() returns
before the child exits — read the man page. And beware any absolute,
unqualified claims this book makes, such as “the child will always print
first” or “UNIX is better than ice cream.”)
5.3 Finally, The exec() System Call
fork() only makes copies of the same program. To run a different
program, the final piece: exec() exec UNIX call family that transforms the current process into a different program — loads new code/static data over the old, re-initializes heap and stack; never returns on success.
defined in ch. 5 — open in glossary
. In Figure 5.3 the
child uses it to run wc — the word-count utility — on p3.c’s own source:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main(int argc, char *argv[]) {
printf("hello world (pid:%d)\n", (int) getpid());
int rc = fork();
if (rc < 0) { // fork failed; exit
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) { // child (new process)
printf("hello, I am child (pid:%d)\n", (int) getpid());
char *myargs[3];
myargs[0] = strdup("wc"); // program: "wc" (word count)
myargs[1] = strdup("p3.c"); // argument: file to count
myargs[2] = NULL; // marks end of array
execvp(myargs[0], myargs); // runs word count
printf("this shouldn't print out");
} else { // parent goes down this path (main)
int rc_wait = wait(NULL);
printf("hello, I am parent of %d (rc_wait:%d) (pid:%d)\n",
rc, rc_wait, (int) getpid());
}
return 0;
}
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
prompt> ./p3
hello world (pid:29383)
hello, I am child (pid:29384)
29 107 1030 p3.c
hello, I am parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
What exec() actually does is as odd as fork(), in the opposite direction:
exec() transforms, rather than creates: it loads wc’s code and static data over the current program’s, re-initializes heap and stack, and starts at wc’s entry point with the given argv. That’s why line 20’s “this shouldn’t print out” indeed never prints.
Given an executable’s name and arguments, exec() loads that program’s
code and static data over the caller’s own, re-initializes the heap,
stack, and the rest of the address space, and runs it with your arguments
as its argv. No new process; the running p3 becomes wc. And so a
successful exec() never returns — which is precisely why the printf
after execvp() in p3.c can promise “this shouldn’t print out.”
Why on earth design creation as this odd copy-then-transform two-step?
That question has a genuinely satisfying answer — the next section builds
a shell out of it. (On Linux there are six exec variants — execl,
execlp, execle, execv, execvp, execvpe; as always, RTFM.)
Check yourself
1.fork() is called once but returns twice. What does each process receive?
2.Why does "hello world" print exactly once in p1.c, even though two processes run?
3.Two runs of p1.c print the parent's and child's lines in different orders. Why?
4.In p2.c the child always prints first — even if the parent is scheduled first. Why?
5.p3.c's child calls execvp("wc", …), and the printf right after it never runs. What did exec() do?