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 shell An ordinary user program (bash, zsh, β¦) that reads commands and runs them via fork β exec β wait, printing a prompt in between.
defined in ch. 5 β open in glossary
β 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 redirection Pointing a process's standard input/output at a file β the shell's child closes a descriptor and opens the file before exec (UNIX assigns the lowest free fd).
defined in ch. 5 β open in glossary
: wcβs output lands in
newfile.txt instead of the screen. Step through how the shell pulls it
off:
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.
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 pipe An in-kernel queue connecting one process's output to another's input, created with pipe(); the plumbing behind cmd | cmd chains.
defined in ch. 5 β open in glossary
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:
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 signal The UNIX mechanism for delivering external events to processes (SIGINT from control-c, SIGTSTP from control-z, β¦); sent with kill(), caught with signal().
defined in ch. 5 β open in glossary
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 user A logged-in identity that may launch and control only its own processes; the OS parcels resources among users. defined in ch. 5 β open in glossary : 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 superuser The administrative user (root) who can control any process and run powerful commands; use sparingly. defined in ch. 5 β open in glossary (root):
Aside: The superuser (root)
Someone must be able to kill an abusive process no matter who started it, and run commands likeshutdown. 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?