pete > courses > CS 315 Fall 26 > lecture 11: fork, exec, and wait
Lecture 11: fork, exec, and wait
Goals
- describe the meaning and use of the PATH
- describe the purpose and behavior of execve(2)
- describe the differences between the (more-often-used) library wrappers for it (exec(3))
- describe the purpose and behavior of fork(2)
- describe the purpose and behavior of wait(2)
what’s wrong with this program: write-to-fd3.c
for starters, it’s weird that the file descriptors for write(2) and close(2) are hard-coded
in the past, in these places, we’ve used file descriptors returned by open(2)
now, we have discussed three special file descriptors available to most processes by default:
- stdin (fd 0) is where a program receives its input from (eg, typing at the keyboard)
- stdout (fd 1) is where a program’s output goes by default (eg, printf)
- stderr (fd 2) is where errors printed by a program go by default (eg, perror)
but none of these are 3, and no file is ever opened, so this seems doomed
aside: getpid(2) is a system call that tells a process its own numerical id
recall our discussion of the ps program, which shows all running processes
and that each process is identified by a number
this is the process id; getpid(2) is how a process can learn its id
back to the problem, we can confirm our hypothesis with strace:
$ strace -o strace.out ./write-to-fd3
the last few lines of strace.out:
getpid() = 20418 write(3, "Hello again from process 20418\n", 31) = -1 EBADF (Bad file descriptor) close(3) = -1 EBADF (Bad file descriptor)
we see the getpid call work
then both the write and close fail
strace helpfully tells us the value of errno at the time of their respective failures so we can gain insight into their cause
as we expected, file descriptor 3 is invalid
looking further up on the file, we see lots of syscalls that now make more sense: opening libraries, mapping libraries, setting memory permissions on these mappings (mprotect), etc
but look at the very first line:
execve("./write-to-fd3", ["./write-to-fd3"], 0x7ffce2abae60 /* 37 vars */) = 0
that’s a new one
to the manpage!
execve() executes the program pointed to by filename. This causes the program that is currently being run by the calling process to be replaced with a new program, with newly initialized stack, heap, and (initialized and uninitialized) data segments.
what on earth does this mean?
"executes the program pointed to by filename" makes sense, but what’s this talk of replacement?
it means exactly what it seems to imply: the currently-running program is replaced by a new one
the old instructions, stack, heap, and other data are completely replaced by a brand new collection of instructions, an empty stack, heap, etc
and the program counter is set to the beginning of the new program’s main function
in practice, this syscall is almost never used directly
instead, we use a variety of wrapper functions from libc that provide different ways of specifying the parameters
they are all described in the exec(3) manpage (and I will refer to them collectively as "exec" even though none of them is called precisely that)
the primary differences are how the command-line arguments are specified to the program (a "l"ist or a "v"ector) and whether the program to be executed is searched for in the "p"ath
explanation by example shortly
but first, recall the argv array that contains the command-line arguments to the program
argv[0] is the name of the program itself
argv[1] is the first argument
argv[2] is the second argument
and so on
here’s a program that demonstrates it: print-args.c
(it’s the same as the argv.c program from lecture 06, with the addition that it prints its pid, which will be useful to us later today)
this collection of arguments is one of the three bits of information that needs to be communicated to exec to do its job
the other parts are the actual program file to execute and the environment (the latter of which we will defer for a few lectures)
so with that in mind, here’s the first example:
the first parameter to execl is the name of the program to execute
the second parameter is the value of argv[0]
the third parameter signifies the end of the argument list
note that the first two parameters are the same
exec uses the first parameter to find the program to execute
exec uses the second parameter to set up argv (and is otherwise ignored by exec)
by convention they match, but they don’t have to
the first one needs to be the program to execute
this is going to fail, however
let’s use strace to see how/why:
$ strace -o strace.out ./exec1
$ tail -n 3 strace.out
execve("ls", ["ls"], 0x7ffc29978018 /* 37 vars */) = -1 ENOENT (No such file or directory)
exit_group(0) = ?
+++ exited with 0 +++
because it can’t find the program ls
if we give it the full path to find the program "ls", it works, as demonstrated by this program: exec2.c
but clearly we do not give the full, absolute path to every program we want to run (eg, gcc, gdb, valgrind), so how does this work?
when you ask Linux to run a program (eg, "ls") it looks in a collection of specific directories to find the compiled "ls" program
this collection of directories is called the PATH
it is an environment variable (more later)
and we can see its value from the shell:
$ echo $PATH /home/pete/.cabal/bin:/home/pete/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl
it’s a colon-separated list of directories
when Linux is instructed to consult the PATH, it searches each directory (in order) to see if it contains a file with the given name
if it finds such a file, it executes it
the "p" variants of exec(3) consult the PATH, so this program works: exec3.c
as the manpage says, certain aspects of the old program disappear when the new program is executed: notably, pretty much everything in memory (instructions, stack, heap, etc)
some things are not replaced, however; much of this stuff falls under the category of "kernel state"—ie, the information about the process that the kernel keeps track of
for instance, process id and open file descriptors
that’s right: when a running process calls exec, the new, replacement program has the same process id
additionally, the new program has access to all the same file descriptors of the previous
to demonstrate this, we’re going to exec a program we wrote ourselves
specifically, the print-args.c program from earlier that prints its arguments and pid
here’s the program we’ll use to exec it: simple-exec.c
if what I said earlier is true, this program should print out its own process id, then a message saying it’s about to exec, and then print-args should take over, and we should NEVER see the result of the final printf because it doesn’t exist after exec does its thing
instead, we should see the output produced by print-args
and in that case, given what I’ve claimed about exec, we should see the same pid
$ ./simple-exec This is process 22211 exec'ing ./print-args... This is process 22211 arg 0 is "./print-args" arg 1 is "hello" arg 2 is "there"
confirmed.
and now to the idea that open file descriptors are retained
here’s a program that opens a file and then execs: less-simple-exec.c
and here’s our program from earlier that writes to a (hopefully-already-open) file descriptor: write-to-fd3.c
if what I claim is true, output-file should contain a line from less-simple-exec and another line from write-to-fd3
$ ./less-simple-exec $ cat output-file Hello from process 22259 Hello again from process 22259
again, confirmed.
and note again that the pids match
exec is the only way we run new programs on UNIX
it is often said that exec is the only function that never returns
because one program calls it but then that program is replaced with the new program before it ever appears to finish
conceptually, execution "continues" at the beginning of the main function in the new program, but since there is no state carried over, "continuation" isn’t a helpful way to think about it
but that all begs the question: how can we ever have more than one process?
enter fork(2):
fork() creates a new process by duplicating the calling process. The new process is referred to as the child process. The calling process is referred to as the parent process.
this one is weirder than exec
I said earlier that exec is called but never returns
well fork is the only function that is called once and returns twice
what on earth does this mean?
it means that it returns in both the parent and the child; in the child it returns the value zero and in the parent it returns the pid of the new child
(as the manpage says, the original process is called the parent and the new process is called the child)
to make sense of this, consider the situation before fork: we have a single process, and its program counter (the register that holds the address of the instruction being executed) contains the address of the "call" instruction that calls fork
when the call instruction is run, the return address is saved in the stack
then, inside fork, the process is duplicated and we now have a child with the exact same state as the parent—including the return address
so both the original parent and the new child will return to the same address
meanwhile, the internals of fork have manipulated the stack of both parent and child such that the return value of the parent is the pid of the child, and the return value of the child is zero
thus, each process can tell whether it is the parent or the child (which is often important, as we shall see shortly)
this is what is meant by fork being called once and returning twice
let’s see it in action: fork1.c
this is the typical pattern: call fork and then check whether it returned zero or not, thus we can cause the parent and child to do different things
so here’s how the program is going to execute
we ask the shell to run the program fork1 and a single process is created, with the program counter set to the beginning of main
then this process calls fork, which creates a child
the next code run in the parent will be the if statement
and the next code run in the child will also be the if statement
in the child, the condition will be true and the first message will be printed
in the parent, the condition will be false and the second message will be printed
to wit:
$ ./fork1 I am the parent because fork returned 22932, which is the child's pid. I am the child because fork returned 0.
to make the whole process-id arrangement more clear, here’s another program: fork2.c
in this program, both the parent and child call getpid to ask the kernel for their process id
$ ./fork2 I am the parent and my child's pid (fork returned) is 22965. My (the parent) pid is 22964. I am the child and fork returned 0. Wait, no, I am the child and my pid is 22965.
note that the process id in the child (as returned by getpid) matches the value returned by fork in the parent
just like every program is run with the exec system call, so every process (except exactly one) is created using the fork system call
when the computer is turned on, the kernel is given control first and does its initialization song and dance
when that is done, the kernel creates out of thin air a single process, historically called init, with pid 1
init then forks and execs the program that prints the login prompt and processes your username and password
upon successfully logging in, that program forks (maybe) and execs (definitely) your shell
when you run startxfce4, the shell forks and execs the graphical environment
when you click the terminal button in the GUI, the GUI forks and execs the terminal program
when you click the web browser button in the GUI, the GUI forks and execs your web browser
and so on and so forth
in both the preceding examples, the parent ran first
we cannot depend on this being the case
that is, we must assume that scheduling (the kernel’s task of deciding which process gets to run on the processor) is non-deterministic
if we want one or the other process to run first, we have to make that happen ourselves
in the context of fork and exec, the primary way to do that is the wait syscall
when called, wait will… wait until any (single) child process exits, and then it will return information about that now-dead child process
if a child has already exited by the time the parent calls wait, then wait will return immediately
if there are many children (ie, a single process called fork a bunch of times) wait will gather the results of exactly one terminated child
example: fork-and-wait.c
corresponding output:
$ ./fork-and-wait I am the parent and my child's pid is 1303706. Parent is waiting for child to exit... I am the child, my pid is 1303706. I, the child just woke up. My child, pid 1303706, exited with value 42.
here’s what happened:
the parent forked, printed out its two messages, and then started to wait
this means that the child gets its turn to run
which means it prints its message, sleeps for a second, and then exits with the value 42
upon the child exiting, the parent is woken up and prints the message showing the exit value
this exposes an interesting subtlety of process management
when a process exits, the kernel keeps some information about how it exited (eg, its exit value)
that information must be kept until the parent calls wait
when a child has exited but the parent has yet to call wait, the child is referred to as a zombie process (or, much less entertainingly, a defunct process)
calling wait to process that child termination is often referred to as reaping
let’s get crazy: interleaving.c
this program will fork three times, assign a different id (unrelated to pid) to each child, and then each child will print its id 10 times, sleeping a bit between each
the purpose of sleeping a bit is to let the kernel (re)schedule the competing processes, with the goal of showing that the scheduling is indeed non-deterministic
if we run it multiple times, we see a different output order each time
nondeterminism indeed
(it’s not actually non-deterministic, but the determinism is hidden deep inside the kernel and incorporates far more factors than we can reasonably account for, so it’s best to think of it as non-deterministic)
we also sometimes see something a bit weird near the end: it looks like the shell prompt is printed out before the children have finished their output
why would this be?
if we consider the code, it seems that the parent could exit before all of the children themselves exit
when the parent exits, the shell (which has itself called wait on the program) is notified and thinks it’s time to print the next prompt
the shell cannot know that the children are still running and producing output, which is why we see the strange ordering
another creepy vocabulary word
when a parent exits while its children are still running, those children are said to be orphans
which raises the question: when those children exit, who calls wait to gather their exit info?
the answer is when a parent exits, the kernel automatically re-parents the orphans to the init process (discussed above), whose primary job is to call wait on them
and yes, this task is indeed referred to as reaping zombie orphan children
we can prevent this unpleasant output ordering by having the parent wait: interleaving-with-wait.c
note that it must wait once for every child it forked
but now all child output shows up before the next shell prompt is printed