§5.1–5.3The fork() System Call … The exec() System Call

Part I OSTEP pp. 37–40 · ~9 min read

  • fork
  • wait
  • exec
  • nondeterminism

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() 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:

  1. The child does not start at main(). Note “hello world” printed only once. The child comes into life as if it had called fork() itself — both processes return from the same call. The child has its own copy of the address space, its own registers, its own PC…
  2. …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:

p1.c, run 1: the scheduler happens to run the parent first
t =
Parent 29146
Child 29147
tick 1 / 4 · one scheduling turn per tickrunningreadyexited

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 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>
p1.c, run 2: same program — but this time the child goes first
t =
Parent 29146
Child 29147
tick 1 / 4 · one scheduling turn per tickrunningreadyexited

Same start: "hello world" prints once.

This nondeterminism 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() (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:

p2.c: wait() makes the order deterministic — even when the parent runs first
t =
Parent 29266
Child 29267
tick 1 / 6 · one scheduling turn per tickrunningreadyblocked in wait()exited

"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() . 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:

before: child (PID 29384) running p3code: p3static data: p3heap (in use)stack (in use)about to call execvp(“wc”, …)exec()no new process is createdafter: SAME PID 29384 — now running wccode: wc (overwritten!)static data: wcheap — re-initializedstack — re-initialized (argv = “wc”, “p3.c”)as if p3 never ran — a successful exec() never returns

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?

5 questions