Β§5.4–5.7Why? Motivating The API … Summary

Part I OSTEP pp. 41–46 Β· ~9 min read

  • shell
  • redirection
  • pipe
  • signal
  • user
  • superuser

Why would anyone design process creation as fork()β€˜s copy followed by exec()β€˜s transform, instead of one simple β€œrun this program” call? The answer is the most-used program on any UNIX box:

5.4 Why? Motivating The API

The separation of fork() and exec() is essential to the shell β€” which is just a user program. Its whole life: show a prompt; read your command; find the executable; fork() a child; exec() the command in that child; wait() for it; prompt again.

The magic lives in the gap: the shell gets to run code after fork() but before exec() β€” code that alters the environment of the about-to-run program. That’s what makes features like this a one-liner:

prompt> wc p3.c > newfile.txt

Redirection : wc’s output lands in newfile.txt instead of the screen. Step through how the shell pulls it off:

How the shell runs "wc p3.c > newfile.txt" β€” fork, redirect, exec, wait
shell (PID 100) prompt> wc p3.c > newfile.txt parsing command… screen the usual stdout newfile.txt (created, empty)

1The prompt

The shell is just a user program (bash, zsh, tcsh…). It prints a prompt, you type a command β€” the name of an executable plus arguments β€” and it figures out where in the file system that executable lives.

step 1 / 6

The reason this works is an assumption about descriptor management: UNIX starts looking for free file descriptors at zero, so after close(STDOUT_FILENO), the very next open() receives fd 1 β€” and everything wc prints via standard output flows into the file. Figure 5.4 is a program doing exactly this:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
  int rc = fork();
  if (rc < 0) {
    // fork failed
    fprintf(stderr, "fork failed\n");
    exit(1);
  } else if (rc == 0) {
    // child: redirect standard output to a file
    close(STDOUT_FILENO);
    open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);

    // now exec "wc"...
    char *myargs[3];
    myargs[0] = strdup("wc");   // program: wc (word count)
    myargs[1] = strdup("p4.c"); // arg: file to count
    myargs[2] = NULL;           // mark end of array
    execvp(myargs[0], myargs); // runs word count
  } else {
    // parent goes down this path (main)
    int rc_wait = wait(NULL);
  }
  return 0;
}

Figure 5.4: All Of The Above With Redirection (p4.c)

prompt> ./p4
prompt> cat p4.output
      32     109     846 p4.c
prompt>

Two tidbits: running p4 looks like nothing happened β€” but fork, exec, and wc all ran; the output just went to the file. And cat proves it got there. Cool, right?

Pipes work the same way, via the pipe() system call: one process’s output is connected to an in-kernel queue, and another process’s input to that same queue:

grep -o foo filestdout β†’ the pipein-kernel pipea queue: foo Β· foo Β· foo …wc -lstdin ← the pipeprompt> grep -o foo file | wc -l β€” count occurrences of a word, by chaining two tools that never heard of each other

A pipe: the output of one process becomes the input of the next through a kernel queue. Long, useful chains of commands follow β€” the UNIX philosophy from chapter 2’s aside, mechanized.

Tip: Getting it right (Lampson’s Law)

β€œGet it right. Neither abstraction nor simplicity is a substitute for getting it right.” β€” Butler Lampson. There are many ways to design process-creation APIs; fork()+exec() is simple and immensely powerful, a case of the UNIX designers simply getting it right. (Though not everyone agrees forever: a recent paper β€” β€œA fork() in the road” β€” argues for simpler APIs like spawn(). Even this book’s opinions are opinions.)

5.5 Process Control And Users

Beyond fork/exec/wait, UNIX offers a wider control surface. The kill() system call sends signals to a process β€” pause, die, and other imperatives. Shell keystrokes are wired to signals: control-c sends SIGINT (interrupt β€” normally terminates), control-z sends SIGTSTP (stop β€” pause, resume later with fg). A process can signal() to catch signals and run a handler when one arrives; whole process groups can be signaled at once.

Which raises a security question: who may signal whom? Enter the user : you log in with a password, gain access to your resources, and can control (pause, kill…) only your own processes; the OS parcels CPU, memory, and disk among users. And for administration there is the superuser (root):

Aside: The superuser (root)

Someone must be able to kill an abusive process no matter who started it, and run commands like shutdown. In UNIX those powers belong to the superuser. Being root is like being Spider-Man: with great power comes great responsibility β€” so it’s usually better to be a regular user, and to tread carefully when you must be root.

5.6 Useful Tools

Get to know: ps (which processes are running), top (processes and the resources they eat β€” comically, top often reports itself as the top resource hog), kill and the friendlier killall (send signals from the command line β€” careful not to kill your window manager), and a CPU meter of your choice for a glanceable view of system load.

Aside: RTFM β€” read the man pages

Man pages are UNIX’s original documentation, older than the web. The man pages for your shell and for every system call you use (return values! error cases!) repay the time spent; reading them is a rite of passage for systems programmers. When a colleague answers your fork() question with β€œRTFM,” they are gently pointing you at the same place. (The F adds color.)

5.7 Summary

fork(), exec(), and wait() create and control UNIX processes; signals and users govern them; the shell composes them into redirection, pipes, and every command you’ve ever typed. This only skims the surface β€” Stevens & Rago’s Advanced Programming in the UNIX Environment is the book for the nuances (Process Control, Process Relationships, Signals).

Homework (code): go fork around

This chapter’s homework is real C, not a simulator: fork and watch a variable diverge; share an open file’s descriptor across fork; make the child print first without wait(); try all six exec variants; see what wait() returns (and what it does in the child); use waitpid(); printf after closing stdout; connect two children with pipe(). Details at ostep-homework.

Check yourself

1.Why is the SEPARATION of fork() and exec() (rather than one combined call) so valuable?

2.In p4.c the child calls close(STDOUT_FILENO) then open("./p4.output", …). Why does this redirect wc's output?

3.Predict: you run ./p4 and the shell instantly shows a new prompt with no output. Did anything happen?

4.What actually connects grep and wc in "grep -o foo file | wc -l"?

5.Why can't any logged-in person send SIGINT to any process on the system?

5 questions